2023-10-23 - Switching zones
New week, new adventures.
Let's take two correlated tasks from our TODO list from Friday:
allow our "mock" player to move between zones
stop hardcoding the start platform coordinates to 0, 0, 0
Moving a player to another zone basically means updating its current zone and current platform coordinates. So, at initialization time, we are basically doing the same thing that we do when changing zone, but with no source zone.
It's always good, when possible, to collapse a secondary case into a more generic solution.
Let's try to do that.
I removed the `getStartCoords()
` method from `Zone
`, as it didn't fit this "change of perspective". Any platform with a string identifier can now be a start position for the player: we don't care if it's the unique first platform at the beginning of the game, or a platform where we arrived using a bridge platform from another zone.
So, as startup data we need a pair of string identifiers which are the string identifiers of zone and platform where to put the player at startup.
At some point this is definitely going to be refactored, but to keep things simple let's put this data into `ZoneManagerBhv
`. We're going to set the initial player position after the first zone has been setup.
namespace BinaryCharm.ParticularReality.LevelManagement {
public class ZoneManagerBhv : ManagerBhv<ZoneManagerBhv> {
//[...]
private const string STARTUP_ZONE_ID = "corridorZone";
private const string STARTUP_PLATFORM_ID = "start";
//[...]
private void Awake() {
registerInstance(this);
initZones();
}
private void Start() {
initPlayer();
}
private void Update() {
updateZones();
}
private void initZones() {
m_rZoneDescs = PersistenceUtils.getZoneDescs();
ZoneDesc startupZone = m_rZoneDescs[STARTUP_ZONE_ID];
m_playerStartCoords =
startupZone.getPlatformCoordsById(STARTUP_PLATFORM_ID);
m_rCurrentZone = Zone.Instantiate(
startupZone,
s_ZONE_PLATFORMS_SETTINGS,
Vector3.zero,
Hex.zero
);
m_rAdjacentZone = null;
}
private void initPlayer() {
MockPlayerManagerBhv.i().setCoords(m_playerStartCoords);
}
//[...]
}
}
Notice that we added to our `MockPlayerMangerBhv
` a method to set the coordinates of the current platform.
For the occasion, we clean it up a little, and we add a new test input: when we press `KeyCode.Keypad0
`, we're going to have the player "cross the bridge" and step into the adjacent zone.
namespace BinaryCharm.ParticularReality.Mock {
public class MockPlayerManagerBhv : ManagerBhv<MockPlayerManagerBhv> {
private Transform m_rDebugSphereTr;
private Hex m_playerCoords;
private void initSphere() {
GameObject rTestSphereGO =
GameObject.CreatePrimitive(PrimitiveType.Sphere);
m_rDebugSphereTr = rTestSphereGO.transform;
m_rDebugSphereTr.localScale = Vector3.one * 0.5f;
rTestSphereGO.GetComponentInChildren<Renderer>().material =
ResourcesManagerBhv.i().test_m_rDebugRedMat;
}
private void updateSphere() {
m_rDebugSphereTr.position = ZoneManagerBhv.i()
.getCurrZone().getPlatformPosByCoords(m_playerCoords);
}
public void setCoords(Hex hexCoords) {
m_playerCoords = hexCoords;
updateSphere();
}
public Hex getCoords() {
return m_playerCoords;
}
private void Awake() {
registerInstance(this);
initSphere();
}
private void Update() {
Action<Hex.EDir> movePlayerPlaceholder = (Hex.EDir dir) => {
Hex? hex = ZoneManagerBhv.i().getCurrZone()
.getAdjacentPlatformCoords(m_playerCoords, dir);
if (hex.HasValue) {
setCoords(hex.Value);
}
};
if (Input.GetKeyDown(KeyCode.Keypad7)) {
movePlayerPlaceholder(Hex.EDir.NW);
} else if (Input.GetKeyDown(KeyCode.Keypad8)) {
movePlayerPlaceholder(Hex.EDir.N);
} else if (Input.GetKeyDown(KeyCode.Keypad9)) {
movePlayerPlaceholder(Hex.EDir.NE);
} else if (Input.GetKeyDown(KeyCode.Keypad4)) {
movePlayerPlaceholder(Hex.EDir.SW);
} else if (Input.GetKeyDown(KeyCode.Keypad5)) {
movePlayerPlaceholder(Hex.EDir.S);
} else if (Input.GetKeyDown(KeyCode.Keypad6)) {
movePlayerPlaceholder(Hex.EDir.SE);
}
if (Input.GetKeyDown(KeyCode.Keypad0)) {
ZoneManagerBhv.i().crossBridge();
}
}
}
}
Of course, we must then implement this `crossBridge()
` method, which is very simple:
if the player is on a bridge platform when it's called, we move it to the target zone and platform.
namespace BinaryCharm.ParticularReality.LevelManagement {
public class ZoneManagerBhv : ManagerBhv<ZoneManagerBhv> {
//[...]
public void crossBridge() {
Hex currPlatformCoords = MockPlayerManagerBhv.i().getCoords();
Platform rCurrPlatform =
getCurrZone().getPlatformByCoords(currPlatformCoords);
ZoneBridgeDest? zbd = rCurrPlatform.m_zoneBridgeDest;
if (zbd.HasValue) {
Zone rPrev = m_rCurrentZone;
m_rCurrentZone = m_rAdjacentZone;
m_rAdjacentZone = rPrev;
ZoneDesc zd = m_rZoneDescs[zbd.Value.m_sDestZoneId];
Hex destPlatformCoords =
zd.getPlatformCoordsById(zbd.Value.m_sDestPlatformId);
MockPlayerManagerBhv.i().setCoords(destPlatformCoords);
}
}
//[...]
}
}
Let's see the new code in action. Notice that it is also possible to go back from `triLeftZone
` to `corridorZone
`.
It's not possible, instead, to proceed to `triZoneRight
`: we're going to need to improve the `updateZones()
` method for that, and that's a job for tomorrow.
2023-10-24 - Adding bridge directions
We have something to fix before proceeding: we hardcoded the "bridge direction" used to calculate the position of the target platform at the other side of a bridge.
//[...]
private void updateZones() {
//[...]
// for now, let's hardcode on which direction the zones
// are "bound" by the bridge platform (north east)
Hex destVirtualCoords =
currPlatformCoords.getNeighbour(Hex.EDir.NE);
//[...]
I'm fine with the restriction that there is a specific direction along which the bridge platform portal can be crossed. We will visualize these special platforms differently than the others, and we'll take that into consideration. But we must be able to specify this specific direction. In terms of validation, we are going to enforce that the direction is "available", meaning that, from the perspective of the bridge platform, there's not already another platform of the same zone along that direction.
So I'm going to modify the `PlatformDesc
`, `ZoneBridgeDest
` and the JSON files of our test zones so that they contain this additional information.
It's boring, but it must be done.
I updated everything, which is not worth showing the code for, and also added an additional check to `getZoneDescs()
`:
In the JSON files, I defined the directions so that the three test zones could be nicely put together one next to each other, as if they were on the same hexagonal grid.
This is a compact view of the updated bridge definitions:
`corridorZone.exit` -> `NE` -> `leftTriZone.left_mid`
`leftTriZone.left_mid` -> `SW` -> `corridorZone.exit`
`leftTriZone.right_bottom` -> `SE` -> `rightTriZone.left_bottom`
`leftTriZone.right_top` -> `SE` -> `rightTriZone.left_top`
`rightTriZone.left_bottom` -> `NW` -> `leftTriZone.right_bottom`
`rightTriZone.left_top` -> `NW` -> `leftTriZone.right_top`
`rightTriZone.right_mid` -> `NE` -> `corridorZone.start`
And of course, I ultimately went and fixed `updateZones()` so that it actually uses the new data:
//[...]
private void updateZones() {
//[...]
// "virtual" coords of target platform
// (based on bridge dir)
Hex destVirtualCoords =
currPlatformCoords.getNeighbour(zbd.Value.m_eDir);
//[...]
Tomorrow, with a little luck, we should be able to complete the "platform switching".
2023-10-25 - Zone unloading
Obviously, we can't just keep loading zones, we must unload them too.
Specifically, we want this behaviour:
there is at least one zone loaded, and at most two
the player is always in a zone (which defines the current zone)
when the player is on a bridge platform, the secondary zone must be ready too (let's call it the adjacent zone)
when the player "crosses" a bridge platform, current and adjacent zones get swapped
We're almost there, the missing part of the puzzle is the replacement of the previous adjacent platform with a "new" adjacent platform, when appropriate.
And part of "replacing" is being able to unload a zone.
We implemented `Platform.Instantiate
` and `Zone.Instantiate
`, not it's time to also add `Platform.Destroy
` and `Zone.Destroy
`.
// Platform.cs
//[...]
public static void Destroy(Platform rPlatform) {
GameObject.Destroy(rPlatform.m_rBhv.gameObject);
}
//[...]
// Zone.cs
//[...]
public static void Destroy(Zone rZone) {
foreach (var kv in rZone.m_rPlatformsMap) {
Platform.Destroy(kv.Value);
}
rZone.m_rPlatformsMap.Clear();
GameObject.Destroy(rZone.m_rParentTr.gameObject);
}
//[...]
I adjusted a bit the `Zone
` class so that it holds the string id of the `ZoneDesc
` from which it was spawned, and a reference to the node that acts as parent of the zone platforms, so that we can also destroy that node when appropriate.
Why I also added the string id? To check if the currently loaded "adjacent zone", if any, is the correct one, or we need to destroy it and load another one.
It's a temporary fix, because we should be able to do this kind of reasoning without accessing `Zone
` but only `ZoneDesc
`, and without doing any string comparisons.
At some point I'm going to smooth things out, but at the moment I have other priorities.
Ultimately, the revised logic for `updateZones()
` looks like this:
private void updateZones() {
Hex currPlatformCoords = MockPlayerManagerBhv.i().getCoords();
Platform rCurrPlatform =
getCurrZone().getPlatformByCoords(currPlatformCoords);
ZoneBridgeDest? zbd = rCurrPlatform.m_zoneBridgeDest;
if (zbd.HasValue) {
// fetch destination zone data
ZoneDesc zd = m_rZoneDescs[zbd.Value.m_sDestZoneId];
if (m_rAdjacentZone != null) {
// is another zone? destroy it
if (zd.m_sId != m_rAdjacentZone.m_sId) {
Zone.Destroy(m_rAdjacentZone);
m_rAdjacentZone = null;
}
}
if (m_rAdjacentZone == null) {
//[...] load and setup the destination zone
}
}
}
So if there's already a secondary zone loaded, it gets destroyed unless it's the right one, which is, the destination of the current bridge platform.
Notice that this setup works for our strange setup where the two triangular zones are linked by two bridge platforms: you can go from one zone to another, using both platforms, with no useless unloading/reloading.
I also did a bit of work to adjust the offset of the zones so that the current zone always have its origin point at the origin of the world (so at coordinates `Vector3.zero
`).
Let's see the system in action.
For "visual debugging" purposes while figuring out the offset maths, I added a red cube that stays at the origin point of each zone, and a blue sphere on the bridge platforms, on the edge associated to the bridge direction.
Notice that when I press `0
` to cross the bridge platform, both zones are moved so that the "new" current zone has its origin at the origin of the world.
The origin of a zone may be distant from any of its platform, depending on the hex coordinates specified in the `ZoneDesc
`: this is the case of `leftTriZone
`, for example. If there's platform at cube coordinates `0, 0, 0
`, the centre of that platform will be at the zone origin point).
Well, I'm calling it a day!
2023-10-26 - Player and portals
Now that we have the zone loading/unloading working with the "mock" player, moved with the keypad, it's time to bind the zone management to the position of the actual player.
It's also a good moment to start cleaning up the player and portal management code, which is almost untouched since the and of the initial PoC development.
I won't do a total refactoring of both subsystems, because I don't want to get too distracted from the current "core goal": being able to roam freely between multiple zones while playing.
Even in this case, it's crucial to separate between player data and the scene nodes related to the player representation in the scene.
Part of managing the player representation is tightly correlated with the input handling, which is in our context the processing of data coming from the headset SDK (head position and orientation, hands tracking data).
It's critical to properly design this part of the game so that it's robust, flexible and effective.
Nothing's worse that a VR application where the interaction is clunky.
Of course, we're going to improve on the PoC architecture, which is not worth describing. Just know that, at least, while working on it, I separated between `PlayerManageBhv
`, `InputManagerBhv
`, `PortalManagerBhv
` and `FireballManagerBhv
`, which helped me to maintain the project manageable.
Now that we're getting serious, I'm going to split carefully the parts describing the game logic state and its representation by nodes in the Unity scene.
While reviewing `PlayerManagerBhv
`, as first thing, I separated the "debug visualization" elements to a different `DebugPlayerManagerBhv
`. This allows to remove clutter from the "main" manager, and it will also make it more convenient to completely disable the debug features at some point.
Something that jumped to my eye while reviewing `PlayerManagerBhv
` and `PortalManagerBhv
` is that they still use the `Vector2Int
` coordinates and call into `EnvironmentManagerBhv
`.
So I think it's time to complete the transition from `Vector2Int
` to `Hex
` and to get rid of `EnvironmentManagerBhv
`, having everything calling directly `ZoneManagerBhv
`.
Done: no trace anymore of grid coordinates expressed as `Vector2Int
`, and `EnvironmentManagerBhv
` deleted.
And everything still works, which is always good after a refactoring session.
2023-10-27 - Binding player and portals
Now that I have made the player handling code a little more manageable, I want to try to complete the week by getting to a state where I can move between zones playing the actual game, and not only with the "mock" player moved by keyboard.
I changed the calls to `MockPlayerManagerBhv
` with calls to `PlayerManagerBhv
` and shuffled a bit of code around.
To avoid problems related to the order of script execution by Unity, I gave (for now) control of the zone switching to the player, which kinda makes sense anyway.
Remember `crossBrigde()
`? I removed it, and added instead a `swapZones()
` which only takes care of swapping current and adjacent zone (and adjusting their position so that the current zone origin is at the origin of the world).
Previously, `crossBridge()
` also updated the (mock) player coordinates. Now, the player coordinates get updated through the interaction of `PlayerManagerBhv
` and `PortalManagerBhv
`.
I did a couple of surgical changes to the PoC implementation, just to get things working.
Previously, to see if a portal could be opened, I only checked if there was a platform along the selected direction. Now, I also check, in case the current platform is a bridge platform, if the selected direction matches the bridge direction. In that case, I enable the portal, setting its exit to the target zone and platform. And of course, while with a single zone the target coordinates were enough to know where to teleport the player when they cross the portal, now I also had to keep track of the target zone. If the target zone is different than the current, I call `swapZones()
` before teleporting the player.
Let's see the changes in action with a little gameplay video.
It kinda works: the adjacent zone gets loaded, but at the end of the video, if you look carefully, there's a glitch, and at the moment of "crossing the bridge" stepping in the second zone, the target platform gets skipped. A bug! Damn it! And to make it worse, a bug that only showed up when I did the gameplay capture using the Quest, and not testing from the Unity editor using Link.
Well, that's what you get when you hack things around: the implementation is not robust.
I'll fix it ASAP, and I will also take care about the other big problem with the current state of the system: the target zone platforms appear when you step onto the bridge platform. But that's not what we want, because we want to give the player the feeling that the bridge platform portal brings them elsewhere. This means that the destination zone must only be shown through the portal.
Let's revisit our TODO list of a week ago and update it:
allow our "mock" player to move between zonesstop hardcoding the start platform coordinates to 0, 0, 0unload the previous zone when appropriatekeep refactoring player/portals and fix the "platform skip" bug
render zones to different layers (needed for portal visualization)
reuse platforms instead of instantiating/destroying them (use a pool)
improve architecture (even better logic/visualization separation)
The "render zones to different layers" entry was exactly about this (expected) characteristic of showing the destination zone only through the portal.
Next week I will be busy and so unable to work on the project, but considering I'm publishing entries with some weeks of delay, you will still get an entry!