About the Layout Demo
Save Files
There are two different types of save files:
- CdemoSaveFile.txt: contains the GUID of the current TScene (the GUID in the left column of the TSceneList asset’s custom editor window).
- CdemoSaveFile_guidstring.txt: contains JSON representing the data in the ChunkingDemoSavedData for a particular scene. The ‘guidstring’ is the GUID of the TScene that the data is for.
When the demo starts for the very first time, the first scene in the TSceneList is loaded since there aren't any save files. When a waypoint is reached the scene’s data is saved, and the guidstring part of the filename is the same as the currently-loaded scene’s GUID.
After that the CdemoSaveFile.txt does exist, and the GUID found there is used to load the corresponding TScene.
Start
The Start
method does all the setup for the demo game. It first calls VerifySetup which validates some of the fields and caches a few references.
Then Start waits for TpLib to be ready. Now let's load and set up our Services.
For simplicity this demo uses a SRS (Scriptable Runtime Service) as a global game state singleton. This demo is complex enough! A number of references are added to the GameState service. Note that this Service is basically just a Scriptable Object attcached to TpServiceManager.
Some of the initialization is performed when TScenes are loaded using TSceneInitializers. Here we just set up a list of all the Tilemaps in the Unity scene, some other references and initial values such as a ChunkedSceneManager and CompositeCollider references, and a reference to the Player Prefab. The Player is added to a list of Persistent Game Objects. This list contains GameObjects that the Layout system should never remove.
The ChunkedSceneManager callbacks are set up, then the FileAccess and Layout services are loaded. The handles (references) are kept as local variables since we'll be using them repeatedly in the demo.
The last Service to load requires a bit of initialization: TpTilePositionDb
. The initializaton is done via a callback. The callback adds the maps that the PositionDb should monitor.
Next, the FileAccess Service is used to try to get the guidKey
. That value is the GUID of the TScene to load. If this is the first time that this minigame starts then that ends up being an empty string.
The next line initializes the SceneManager Service. This actually loads the appropriate TScene from the TSceneList reference of the SceneManager Monobehaviour component. If the passed-in GUID is string.Empty or if the passed-in GIUD isn't found at all then the very first TScene is loaded. If the GUID matches a different TScene then that TScene is loaded.
In other words: Determining the location and GUID of the waypoint to use for placing the Player requires reading the save files. If found, the location and GUID of the current waypoint for the TScene are available. If not, the zeroth TScene from the TSceneList is used, and the single waypoint with the m_IsStartWaypoint field = true is used as the start position.
The SceneMaanger callbacks set up earlier:
sceneManager.OnBeforeTSceneChange += OnBeforeSceneChange;
sceneManager.OnAfterTSceneChange += OnAfterTSceneChange;
sceneManager.OnNewTSceneChosen += OnNewTSceneChosen;
are used when the initial TScene is loaded and every time the TScene is changed.
-
OnBeforeTSceneChange: called just after SceneManager.SetScene is called. This is used to clean up the current Unity scene, mask the camera during a scene change, or any other housekeeping required before a TScene change commences.
-
OnNewTSceneChosen: The new scene was correctly chosen. In the demo this callback saves the GUID of the new TScene in the filesystem. That way the next time that the demo begins it will use that TScene as the starting TScene.
-
OnAfterTSceneChange: The load is complete. All TSceneInitializers have run. Here one performs any final setup. For the demo this includes:
Once we know where to place the Player, we create one if necessary and/or place it in the proper position. When creating a Player we also set up the Camera follower component (placed on the Camera GO).
Finally, we update all the TpZoneLayout components, which will be discussed next.
It’s important to note that while we know where the initial waypoint location is prior to updating the TpZoneLayouts, the actual TilePlus tile used for the waypoint is NOT actually on any Tilemap until after the TpZoneLayout updating is complete and the tiles are actually loaded.
That’s the reason for this:
if (startWaypointGuid != string.Empty)
{
var guid = new Guid(startWaypointGuid);
while (!TpLib.HasGuid(guid))
await Awaitable.NextFrameAsync();
}
What’s happening here?
Once the waypoint is loaded its GUID will appear in TpLib’s GUID list. This is very conservative and probably redundant but is a good practice if you want to absolutely ensure against a race condition.
Finally, if a save file was located then use the JSON data from the save files to update loaded TilePlus tiles with the current state that they should be using.
Update
There's actually NO Update method in ChunkingGameController. User input is done in ChunkingDemoPlayerController. It's pretty simple:
- Set up and accept input from the New Input System
- Use the PositionDb Service to see if the Player character prefab can move in the desired direction.
- Ensure that the Player prefab isn't going to move outside of the TScene boundary.
Update's local method TestForMove
is used to intereact with the PositionDb Service and do these and other tests.
- Convert from the Vector2 obtained from the New Input System callback into an enum value which describes what direction to move in.
- Do some conversions and obtain the Grid and World position of the Player prefab.
- Use the PositionDb Service to ensure that the Player stays on the road tiles.
- Use the PositionDb Service to see if there are any blocking tiles.
- Test the set of spawned GameObjects to see if there are any collider intersections.
- Determine what direction the Player should rotate to and perform the rotation.
This shows the hybrid approach used by the Layout system: PositionDb for tile collisions and colliders for GameObjects.
One advantage using the PositionDb for tiles is that the PositionDb responds to changes within one frame with low compute overhead. This is important when using the tweener since you want to be able to collide with the sprite and not the tile.
Update in this demo is simple. It moves the Player based on what keys are pressed and calls UpdateLayout. UpdateLayout UpdateLayout checks to see if the Player has moved to a new Grid position. If not, there’s no need to re-layout the Tilemap chunks. Then for each TpZoneLayout which is currently in use a Task is created which invokes the TpZoneLayout’s UpdateTickAsync, which we’ll talk about next. Then we wait for all these tasks to be completed. Finally, UpdateTiles is called. That method messages the new position to all of the TilePlus tiles that want to know the new position. At this stage, TilePlus tiles that are messaged may post Save or Trigger events, which are handled by TpEventsOnTileEvent. If TpEventsOnTileEvent had set certain flag variables, they’re handled by HandleSaveEvent or HandleTriggerEvent. Waypoints Waypoints can just save game data and the current waypoint position, or it can do that AND change to a new TScene. That’s controlled by the m_IsLevelChange field in the tile. If that’s FALSE then a Save event is emitted when the Player encounters a waypoint. If that field is TRUE then a Trigger event is emitted instead. • SaveDataEvent: Save the TScene’s data (ChunkingDemoSavedData class is JSONized). • TriggerEvent: Save the TScene’s data and load a new TScene based on the waypoint’s m_NextLevelGuid field. HandleSaveEvent This updates the save data: if the Event was from a waypoint then we record the waypoint’s position and GUID in the ChunkingDemoSavedData instance, then disable all other waypoints in the TScene. Then the GUID of the scene is saved and the overall game data are saved. Note that game data are not saved until the Player encounters a waypoint. HandleTriggerEvent This is an example of customization that can’t be handled by a TSceneInitializer. It looks at trigger events as cached in the TpEvents static class instance. If the tile is a TreasureChest then the count of encountered treasure chests is incremented for UI display only. Note that in this demo, TreasureChest tiles clear their sprite when encountered. That state is saved in the save data file and restored when the TScene is loaded again. If the tile is a waypoint then all other waypoints are disabled and the game state is saved. Then the m_NextLevelGuid field is passed to the TpChunkedSceneManager instance’s SetSceneFromSceneNameOrGuid and a new TScene is loaded. TpZoneLayout.UpdateTickAsync UpdateTickAsync uses information from the method’s parameter list and from the TpZoneLayout’s serialized fields in order to determine what to unload and load. Unloading means removing Chunks of tiles and Loading means adding Chunks of tiles. Chunks can be any size from 4x4 and higher. Note that the Chunk Size is specified as a single integer and not a Vector2Int: this means that chunks are always square. Chunk Size can’t be less than 4 and must be an even number. Basically, the area in the Camera view area (plus padding) is examined for any empty Chunks using data about already-loaded chunks which are maintained in the TpZoneLayout’s spawned TpZoneManager. As the Camera moves around, new empty chunks will be encountered, and they’re loaded. Additionally, already-loaded chunks will pass out of the Camera view area (plus padding) and such chunks are deleted. It’s deceptively simple; most of the actual loading and unloading is performed in TileFabLib and the “Chunkifying” of TileBundles is performed in the TileBundle assets themselves.