Creating custom Scenes with Unity

Introduction

The process behind creating levels is equally diverse with the simplest methodology requiring you to create and position everything manually with code. This approach will have you create your levels in the game engine Unity, and import your new level back into Hollow Knight. By the end of this tutorial, you should be able to accomplish all of these tasks with limited extra help needed. With all of that said, this tutorial will be geared towards those who have previously modded Hollow Knight and furthermore, it will assume you know how to assetbundle new scenes into Hollow Knight.

Background

You cannot create scenes without having a good understanding of the various aspects that run Hollow Knight and its mods. Therefore, this guide recommends and expects that you have already worked with the following topics:

  • Understanding the structure of a basic mod.
  • Having basic Unity knowledge.

Note

You will also need the ability to choose between reimplementing certain behaviors/object yourself or copying them using preloading.

Note

With the exception of basic Unity knowledge, all other topics listed are explained, either through video or text, on this site. If you still need assistance, don’t be afraid to ask on Discord.

What you will need

And now, here are the steps to create custom Hollow Knight scenes.

Mod project setup

  1. Create a new C# class library project. Use the one labelled “Class library (.NET Framework)”.
  2. Give it a name and select .NET Framework 3.5
  3. Add the modding api and the SFCore library mod as a reference.

Note

SFCore is needed because of the MonoBehaviours we will use in the custom scene.

  1. Add GetPlayerBoolHook, LanguageGetHook and UnityEngine.SceneManagement.SceneManager.activeSceneChanged.
  2. The GetPlayerBoolHook will only listen to a bool that we will call CustomSceneTutorial_VisitedArea and will always return false.
private bool OnGetPlayerBoolHook(string target)
{
   if (target == "CustomSceneTutorial_VisitedArea") return false;
   return PlayerData.instance.GetBoolInternal(target);
}
  1. The LanguageGetHook will only listen to 3 strings that start with "CustomSceneTutorial_AreaTitle_".
private string OnLanguageGetHook(string key, string sheet)
{
   if (sheet == "Titles" && key == "CustomSceneTutorial_AreaTitle_SUPER") return "Testing";
   else if (sheet == "Titles" && key == "CustomSceneTutorial_AreaTitle_MAIN") return "area";
   else if (sheet == "Titles" && key == "CustomSceneTutorial_AreaTitle_SUB") return "of doom";
   return Language.Language.GetInternal(key, sheet);
}
  1. The UnityEngine.SceneManagement.SceneManager.activeSceneChanged will do nothing for now.
private void OnSceneChanged(Scene from, Scene to)
{
}
  1. For the sake of simplicity add the preloads “Area Title Controller” and “_SceneManager” from “White_Palace_18”.
public override List<ValueTuple<string, string>> GetPreloadNames()
{
   return new List<ValueTuple<string, string>>
   {
      new ValueTuple<string, string>("White_Palace_18", "Area Title Controller"),
      new ValueTuple<string, string>("White_Palace_18", "_SceneManager"),
      new ValueTuple<string, string>("White_Palace_18", "_Managers/PlayMaker Unity 2D")
   };
}
  1. This also means storing these preloaded GOs in your mod class.

10) For later use in Unity, also add a script for inserting an AreaTitleController, a script to insert a SceneManager, and a script to insert a PlayMaker Manager.

  1. Also for later, add a script for setting the correct width and height of the scene.
class PatchTilemapSize : MonoBehaviour
{
   public int width = 30;
   public int height = 17;

   public void Awake()
   {
      On.GameManager.RefreshTilemapInfo += OnGameManagerRefreshTilemapInfo;
   }

   public void OnDestroy()
   {
      On.GameManager.RefreshTilemapInfo -= OnGameManagerRefreshTilemapInfo;
   }

   private void OnGameManagerRefreshTilemapInfo(On.GameManager.orig_RefreshTilemapInfo orig, GameManager self, string targetScene)
   {
      orig(self, targetScene);
      if (targetScene == gameObject.scene.name)
      {
            self.tilemap.width = width;
            self.tilemap.height = height;
            self.sceneWidth = width;
            self.sceneHeight = height;
            FindObjectOfType<GameMap>().SetManualTilemap(0, 0, width, height);
      }
   }
}

Preparation for Unity

  1. Create a new C# class library project using Unity. Use the one labelled “Class library (.NET Framework)”.
  2. Give it a name (I suggest the same from before, but with "Scripts" behind it) and select .NET Framework 3.5
  3. Add ONLY the required Unity assemblies as references.
  4. Copy only the MonoBehaviour classes from before into this new project.
  5. You can remove all functions from these classes, only the member variables are important.

Note

For member variables that are of enum types, you can use other enums that have the same ranges covered as seen in the PatchSceneManager class.

  1. Build this MonoBehaviour (only project) and copy the DLL.

Unity project setup

  1. Create a new project using Unity. As a template choose the 3D template. The name is irrelevant.
  2. Make a few folders for organizing assets.
_images/unityfolders.png

Figure 1: My personal assortment of folders to organize assets.

  1. Paste the copied DLL from before into the Assemblies folder, also add the SFCoreUnity.dll assembly.

Note

Don’t forget to rename SFCoreUnity.dll to SFCore.dll

  1. Create a scene in Unity.
  2. Change the lighting of the scene.
_images/unitylighting.png

Figure 2: Lighting settings that are good to use

  1. Add your terrain meshes, put colliders on them (either EdgeCollider2D or PolygonCollider2D) and put them on layer 8 (aka the terrain layer).

Note

This can utilize custom made meshes in programs like Blender.

  1. On these mesh GameObjects, add the SceneMapPatcher component from SFCore and give it a black texture to use.
  2. Behind everything (with a global Z position of around 7), it is good to add a BlurPlane with a MeshFilter, MeshRenderer, and BluePlanePatcher component.
  3. Add decorations, sprites should have the SpritePatcher component on them or above them in the hierarchy.
  4. Add a GameObject called __Initializer with a PatchAreaTitleController, PatchSceneManager, PatchPlayMakerManager, and PatchTilemapSize component.
  5. The PatchAreaTitleController will be set as a sub area with the area event CustomSceneTutorial_AreaTitle and the visited bool CustomSceneTutorial_VisitedArea.
  6. The PatchSceneManager can be adjusted to one’s liking.
  7. The PatchPlayMakerManager should be given a transform of a GameObject called _Managers.
  8. The PatchTilemapSize should be given the width & height of the custom scene.
  9. Add an entry & exit by adding a GameObject with a Collder set as a trigger and a TransitionPoint component, which can be added as a simple .cs file in the _MonoBehaviours folder in the assets.
  10. Build the assetbundles and include them in the first C# project as embedded resources.
  11. In the mod, load the assetbundles in either the constructor or the Initialize function.

Accessing the custom scene

  1. In UnityEngine.SceneManagement.SceneManager.activeSceneChanged that did nothing until now, add code to create a GameObject with a TransitionPoint wherever you want to access your scene from.
  2. Build the mod.
  3. Enjoy your first empty custom scene!