2024-01-15 - Zone management bugfixing
And after the longest break since the start of the "advanced prototyping" phase, I'm back at work on the project. Yay!
I never left you without the usual weekly dose of Particular Reality DevLog, but the break has eaten into the "ready to publish" material. This means that from now on I'm going to publish on Saturday (or on Sunday, if I'm late) the DevLog about that same week. You'll get "fresh" material!
The downside of this is that when I need to stop for a week, you won't get anything. Unless I manage to write an "extra" entry like the previous one.
So, let's go back to the TODO list from the end of Week 10:
TODO:
refactor the zone management so that it fits the new architecture (getting rid of string based logic in the process, mapping to `
int
` identifiers once, at load time)work on a fully fledged player avatar (now we just have three spheres for hands and head)
implement (more or less explicit) access to the save/load system in VR (now we just use keypresses for testing)
improve the pause/unpause visualization and interactions (now we access it with the "system" pinching gesture, or with the test "left hand thumbs down" gesture)
improve the visualization of the bridge portals (now we just have a small blue sphere on the platform edge)
add a pooling system to recycle platforms (and other stuff in the future)
Ok! I'm going to take care of the first bullet point, but I'm also going to throw into the mix the last two.
Basically, I need to improve the zone management logic so that it works properly with the save/load system, but in the process I'm also going to
make it more efficient (no string identifiers, object pooling)
improve the visualization of the bridge portals, which is correlated to the zone loading
The current implementation of zone management works well when dealing with the player moving between different zones, preloading adjacent zones when they step on a bridge portal, but (as mentioned in Week 10) often breaks when dealing with the loading of a game state (which can bring to player to any zone, as it is now using a test keypress for activation). Why? Because of this code snippet:
private void teleportPlayer(Location loc) {
Debug.Log("teleportPlayer to " + loc.ToString());
m_rPlayerStateManager.getFSMData().teleportTo(loc);
if (ZoneManagerBhv.i().getCurrentZoneId() != loc.m_sZoneId) {
Debug.Log("swapping zones");
ZoneManagerBhv.i().swapZones();
}
}
The `ZoneManagerBhv
` is not totally passive yet, it needs a call to `swapZones()
` to swap current and adjacent zone, when appropriate. This needs to go away: the `ZoneManagerBhv
` must understand "by itself" that it needs to swap zones.
More generically, it needs to load and set as current any zone needed by the player state: it's not just a matter of "swapping" a current and adjacent zone, because in the case of game loading, the "adjacent zone preloading" doesn't apply, and no explicit teleportation happens.
To get started, I'm going to fix the current `ZoneManagerBhv
` doing the minimal changes required.
Then, in the upcoming days, I'm going to improve the whole subsystem as discussed.
To make `ZoneManagerBhv
` able to detect if it needs to perform a `swapZones()
`, I added to it a little piece of state: the previous player location (`m_prevPlayerLoc
`).
That way, I could detect if a "zone swap" needed to be performed. Moreover, I also handled the possibility that - because of a game load - the current player location requires a "new" zone (meaning: a zone which was not preloaded as adjacent).
In code, after factoring out some code to a few auxiliary methods for readability, this is the updated logic of `updateZones()
`.
public void updateZones() {
Pose originPose = RealityManagerBhv.i().getPose();
m_rWorldOriginTr.position = originPose.position;
m_rWorldOriginTr.rotation = originPose.rotation;
Location currPlayerLocation =
PlayerManagerBhv.i().getState().getCurrLocation();
if (m_rCurrentZone != null) {
if (m_prevPlayerLoc.HasValue &&
m_prevPlayerLoc.Value.m_sZoneId == m_sCurrentZoneId &&
currPlayerLocation.m_sZoneId == m_sAdjacentZoneId) {
swapZones();
} else if (currPlayerLocation.m_sZoneId != m_sCurrentZoneId) {
destroyCurrZone();
}
}
if (m_rCurrentZone == null) {
instantiateAsCurrZone(currPlayerLocation.m_sZoneId);
}
m_prevPlayerLoc = currPlayerLocation;
Hex currPlatformCoords = currPlayerLocation.m_coords;
Platform rCurrPlatform =
getCurrZone().getPlatformByCoords(currPlatformCoords);
ZoneBridgeDest? zbd = rCurrPlatform.m_zoneBridgeDest;
bool bDebugging = DebugManagerBhv.i().isDebugging();
getCurrZone()?.setDebugVisualization(bDebugging);
getAdjZone()?.setDebugVisualization(bDebugging);
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) {
destroyAdjZone();
}
}
if (m_rAdjacentZone == null) {
instantiateAsAdjZone(currPlatformCoords, zbd.Value);
}
}
}
As anticipated, I also removed the `swapZones()
` call from `teleportPlayer()
`, and changed its visibility to `private
`. Another little code smell was removed, and the game loading now works properly even when it refers to non-adjacent, non-preloaded zones.
Now that we got rid of the bug, we can start thinking about further improvements and developments. Tomorrow, I mean.
2024-01-16 - Reimagining bridge portals
Today I have just a couple of hours available, and I feel like doing a bit of design.
If you've been following the DevLog, you know that the game world is made of multiple zones linked together by portals that the player can open on "bridge platforms".
To make things easier, I imposed the restriction that a platform can only have a single "bridge portal" to other zones. This helps, because it implies that when the player is standing on a platform X of zone A, there is at most one other zone B that needs to be loaded to allow for its potential movement.
Currently, in terms of visualization, if a bridge portal is available I put a small blue sphere on the edge indicating the portal direction.
Of course, that's only a temporary "debug" thing: I need to do better.
Additionally, there's the "loading" problem, which is correlated.
Stepping on a bridge platform preloads the adjacent zone, so that when the player opens the portal, they can cross it and access such zone.
Now that we only have some platforms to spawn, loading is instant and that's fine, but inevitably at some point loading a zone might need some time. I'd like to keep it very low, let's say under 2-3 seconds, but if you want to do things right in a VR game, there are just two categories of operations
those that can fit in the frame time budget
those that can't
As long as you have something in category 2, if you're aiming for quality (meaning: no stuttering), you need to handle it asynchronously.
So, I'm going to make the logic and visualization work properly with an arbitrarily long loading time.
As long as a zone is loaded, the information about its "bridge" platforms is of course known.
The destination zones of the bridges, instead, are not loaded.
When one steps onto a bridge platform, the adjacent zone loading must start, and that must be visualized in some way. As soon as the loading completes, instead, the player must be made aware that the portal is ready to be used. At any time (so, during the loading or when the loading has been completed) the player can step back and exit the "bridge" portal.
I also feel like adding another small thing into the mix: the possibility that a bridge portal might be locked. For example, the bridge portal to the next zone might unlock only when you clear it of all the enemies. Or it could require some kind of key found elsewhere. I don't have anything specific in mind, but it's something so common and basic that I feel it makes sense to implement it anyway.
So, Let's take another look at the portal state machine we designed on 2023-12-12:
This logic is still valid, but it applies after the destination zone has been loaded, and only if the portal is unlocked.
So, to handle asynchronous loading and portal unlocking, we must do some additional work.
At high level, without thinking about animation details and responsiveness, we must handle this flow:
Basically, before getting to the part where it's possible to open the portal, we make sure that
the portal is "unlocked", a prerequisite for the loading to start
the loading completes (the player stands on the platform for the required time)
Let's start designing and implementing the locking/unlocking part, and we'll hopefully take care of the rest next week.
We want to be able to initialize a bridge portal as `locked
` or `unlocked
`, and want to have a `locking
` and `unlocking
` animation taking some time.
As done previously, I want to keep things as responsive I can, so I'm going to allow the possibility that a locking or an unlocking can be interrupted and reversed anytime.
This should do it.
In terms of visualization, I'm going to use a placeholder that should be able to evolve to the loading/opening portal states that we're going to implement later.
2024-01-17 - Implementing bridge portal locking
No chit-chat today: I'm going to start implementing what I described yesterday.
I'm going to use a vertical hexagon (placed where we currently have the small blue sphere) as "bridge portal" indicator.
When the portal is locked, I'm going to show the hexagon as red, and with "pointy-top" orientation. When it's unlocked, I'm going to switch to "flat-top" and change the color to green.
I'm going to implement a smooth transition between the two states.
Ok, I did all the adjustments needed to have the placeholder instantiation and placement working.
I also started to implement the animation logic described by the FSM designed yesterday.
Tomorrow afternoon I'm going to be out, but hopefully I'll manage to complete this on Friday.
2023-01-19 - Persistent World Basics
By adding the possibility to have "locked" portals, I created the first piece of game state which relates to the game world.
Let's say that I manage to unlock a bridge portal in zone A, and then I go to zone B. When I come back to zone A, I must find it still unlocked.
Will this apply to the game? Maybe yes, maybe not, but I definitely will have some persistent data describing the state of the game world zones. Additionally, I want to be able to simulate some logic for the active zones (the current and adjacent zone, if any). For example, there might be some traps, or some moving walls, or some disappearing platforms.
Basically, I need to add another state manager, let's say `WorldStateManager
` and add it into the game execution (also handling it in the save/load process).
While we have some static description of the zones, loaded once at startup, we also need to take care of the dynamic information that depends on the player actions and progress in a specific gameplay session.
Let's go!
It took a little time to have all the pieces fit the puzzle properly, but I'm quite happy about the result.
Like for the main `GameStateManager
`, in the case of `WorldStateManager
` and `ZoneStateManager
` I had to nest other FSM executions.
Basically, I hold at runtime in `WordStateManager
` a `ZoneStateManager
` for each zone, and each `ZoneStateManager
` holds instead a `BridgePortalStateManager
` for each bridge portal of that zone.
Their update loop also updates their "child" state machines.
In the `World.Data
` and `Zone.Data
` definitions, that become part of the savegame, I put all the "child" state machine executions.
For example:
namespace BinaryCharm.ParticularReality.StateManagement {
public class World {
//...
public class Data {
[JsonProperty("rZonesFSMEx")]
public Dictionary<string, ZoneFSMExec> m_rZonesFSMEx;
[JsonConstructor]
public Data(Dictionary<string, ZoneFSMExec> rZonesFSMEx) {
m_rZonesFSMEx = rZonesFSMEx;
}
}
}
}
...and:
//...
namespace BinaryCharm.ParticularReality.StateManagement {
public class WorldStateManager : AStateManager<EState, EInput, Data> {
private Dictionary<string, ZoneStateManager> m_rZonesManagers;
public WorldStateManager(WorldFSMExec rExec)
: base(
new WorldStateFSM("WorldFSM", rStates),
rExec) {
m_rZonesManagers = new Dictionary<string, ZoneStateManager>();
foreach (var zoneKV in rExec.getData().m_rZonesFSMEx) {
string sZoneId = zoneKV.Key;
ZoneFSMExec rZoneFSMExec = zoneKV.Value;
ZoneStateManager rZM = new ZoneStateManager(rZoneFSMExec);
m_rZonesManagers.Add(sZoneId, rZM);
}
}
public override void update(float fDT) {
base.update(fDT);
foreach (var kv in m_rZonesManagers) {
kv.Value.update(fDT);
}
}
//...
}
}
Expanding the contents of only one zone (`corridorZone
`, containing a single bridge portal on the `exit
` platform), this is what the savegame now looks like):
{
"rPlayerFSMEx": { /* ... */ },
"rPlayerAvatarFSMEx": { /* ... */ },
"rPortalFSMEx": { /* ... */ },
"rWorldFSMEx": {
"rData": {
"rZonesFSMEx": {
"corridorZone": {
"rData": {
"rBridgePortalsFSMEx": {
"exit": {
"rData": {
"fLockingLevel01": 0.5211675,
"fOpenedLevel01": 0.0
},
"eCurrStateId": "unlocking",
"ePrevStateId": "locked",
"fCurrStateElapsedTime": 0.0957665
}
}
},
"eCurrStateId": "normal",
"ePrevStateId": "normal",
"fCurrStateElapsedTime": 248.092148
},
"leftTriZone": { /* ... */ },
"limboZone": { /* ... */ },
"menuZone": { /* ... */ },
"rightTriZone": { /* ... */ }
},
"eCurrStateId": "normal",
"ePrevStateId": "normal",
"fCurrStateElapsedTime": 248.092148
}
}
Let's conclude the post with a short video showing this week's results.
I'm just using three keypresses as inputs:
`
u
` to lock/unlock the portal`
Shift+1
` (shown as `!
`) to save the game to slot 1`
1
` to load the game from slot 1
The video shows how I tested the robustness of the implementation.
first, I flip from `
locked
` to `unlocked
` waiting for the transition to completethen, I flip very rapidly, interrupting and reversing `
locking
` and `unlocking
`, to make sure that nothing breaks and there are no jumps in the animationaround 00:18, I save with the portal unlocked, lock it pressing `
u
`, and then load: as expected, in this case the change from `locked
` to `unlocked
` is instantaneous (the execution from the savegame gets restored)I repeat the previous step, but saving in the `
locked
` stateas last test, I quickly hit `
u
` and then `shift+1
` to save during the `unlocking
` state. Then I load the state multiple times, and you can see that the execution correctly resumes from the middle of the unlocking animation
That's it! Nothing special for the eyes of the casual observer, but you, my dear reader, can appreciate the work behind the scenes.