2023-11-06 - Player refactoring
Last week I took a break to take care of some other tasks (which I only partially completed), but I'm excited to get back at work on the project.
At the end of the previous entry, we had a bug in the teleportation logic that showed up when crossing the portal on a bridge platform. And it only happened while testing on Quest.
I might spend some time looking for the bug, but I think it's better to just keep refactoring the related subsystems to a cleaner, proper version, and maybe that will also get rid of the problem.
So, I'm going to work for a while on the player management and portal management subsystems.
I started by working on the `PlayerManagementBhv
` code.
I basically extracted a `Player
` struct, which is going to only contain the state variables of the player, and a `PlayerBhv
` class which handles the scene nodes related to the player.
`PlayerManagerBhv
` is now much simpler: it holds a `Player
` struct and a reference to a `PlayerBhv
`, and has the two interacting together.
`PlayerBhv
` is used to both extract some input data from the scene nodes, and to update them on the basis of the `Player
` state.
In an ideal world, input data would be taken directly from the input devices, but we're using the pose detection offered by the Meta SDK for Unity, which needs some behaviours active in the scene (or at least, that's how I did it until now, following the documentation).
Input is then used to update the game state, and finally the scene gets updated to reflect such state and have it visualized to the player.
Summarizing, the new `PlayerManagerBhv
` update logic is:
fetch data from the current scene through `
PlayerBhv
`update the player state (`
Player
`) on the basis of the fetched dataupdate the scene through `
PlayerBhv
` so that it represents the updated player state
This kind of separation helps to keep logic and visualization isolated, and will also be useful when we're going to implement savegames.
A savegame will be a collection of all the pieces of state data of the running game, and only that: it will be a responsibility of the loading process to load and instantiate the scene nodes needed to recover execution from that saved state.
But that's a problem for another day.
While I was at it, I also improved my "debug" controls, that now use both mouse and keyboard.
I added a proper mouse look to the basic absolute W-A-S-D movement I already had implemented.
Additionally, I modified `MockPlayerManagerBhv
` so that it teleports the actual player when using the keypad, and not just a placeholder sphere.
This allowed me to easily test the player movement at the PC, using keyboard and mouse.
As I hoped, the bug also shows up at the PC now that I can properly simulate the change of look direction (which is important in target platform selection).
Tomorrow I'll complete the refactoring and, hopefully, get rid of the bug.
2023-11-07 - SDK update
Today unfortunately I don't have much time for the project, so it's going to be a "light" day.
But I have something on my "TODO" list that should fit today's limited time slot: updating the Meta Quest SDK.
Some days ago an article on some social media feed caught my eye: they finally released the "package manager friendly" version of the SDK, which is something I was looking forward to.
What does it mean in practice?
By updating to the new version of the SDK, I won't have the `Oculus
` directory inside the `Assets
` of the project. The non-project specific code, as it should be, will come from isolated, external packages with proper dependency management.
The package management system is probably the best thing that happened to Unity in the latest years.
Moreover, Meta went from a monolithic distribution of the SDK to one where different packages provide different features, so one can include in a project only what's needed. Well done!
So, I went and downloaded the new packages. I decided to use the `tgz
` version and add them locally from my file system. The default is just installing them from the package manager, but I like the idea of having in my own repo the dependencies needed to build the project.
I'm using a self-hosted Gitea, so I don't have strict space restrictions (you wouldn't want to upload hundreds of megabytes of Meta packages to GitHub or BitBucket).
I downloaded and configured the packages I needed, and removed the `Oculus
` directory from `Assets
`.
Then, I made a fresh build and tested it on my Quest 3, to be sure that nothing broke in the process.
I only had to restore a few settings in `OculusProjectConfig
`, which still needs to go under `Assets
`, and then everything worked fine.
While I was at it, I also tried installing the new version of the XR simulator (`com.meta.xr.simulator
`). You might remember that I tried it in the first prototyping phase, but it constantly crashed and I had no time (or patience) to understand the reason. The current version, instead, works, and that's good news, as it could help to speed up iteration.
This SDK upgrade should also allow me to play a bit with the latest features offered by the SDK, which if I remember correctly should include the possibility of using both hands and controller at the same time, which could be handy, and the new "spatial" feature of the Quest 3, which I also would like to use to provide a very easy setup of the play area.
I need to stop for today, no real progress today but I still managed to do something useful for the development, so it's not that bad.
2023-11-08 - Introducing locations
I went back to the player and portal refactoring and to the bug of the "double teleport" when going into a bride platform portal.
While testing with the new very practical PC controls, I also noticed that the check for availability to move towards a certain direction wasn't working well in all the cases.
It's a good moment to fix something else which was bugging me: the `getAdjacentPlatformCoords(Hex coords, EDir dir)
` method in `Zone
` should not really exist, the availability of a platform suitable to move to should be checked in the "data" layer (only accessing the `ZoneDesc
` entities, and not their components in the Unity scene).
So, I removed it and then, at least for now, moved some logic to `ZoneManagerBhv
`. In the architecture that is shaping up while progressing, we have "pure" data/logic classes, classes related to scene nodes, and managers that handle their interactions and can access both.
So it make sense, at least for now, to have in `ZoneManagerBhv
` (that holds the database of all the available `ZoneDesc
` entries) the logic to check if, given a location, there's an adjacent platform along a certain direction.
I wrote location, and in fact I think it makes sense to add a little new struct which aggregates a zone identifier and some coordinates.
using BinaryCharm.ParticularReality.HexGrids;
namespace BinaryCharm.ParticularReality.LevelManagement {
public readonly struct Location : System.IEquatable<Location> {
public readonly Hex m_coords;
public readonly string m_sZoneId;
public Location(Hex coords, string sZoneId) {
m_coords = coords;
m_sZoneId = sZoneId;
}
public override bool Equals(object? obj) =>
obj is Location other && this.Equals(other);
public bool Equals(Location p) =>
m_coords == p.m_coords && m_sZoneId == p.m_sZoneId;
public override int GetHashCode() =>
m_coords.GetHashCode() ^ m_sZoneId.GetHashCode();
public static bool operator ==(Location lhs, Location rhs) =>
lhs.Equals(rhs);
public static bool operator !=(Location lhs, Location rhs) =>
!(lhs == rhs);
}
}
I promise we're going to get rid of `string
` identifiers pretty soon - bear with me.
This way, we have a single type of data that can represent both a neighbouring platform in a same zone, and a neighbouring platform which is at the other side of a bridge platform, coming from another zone.
// extract from ZoneManagerBhv.cs
public Location? getAdjacentLocation(Location loc, Hex.EDir eDir) {
ZoneDesc zd = m_rZoneDescs[loc.m_sZoneId];
PlatformDesc pd = zd.getPlatformDescByCoords(loc.m_coords);
Hex destCoords = loc.m_coords.getNeighbour(eDir);
if (zd.containsPlatformWithCoords(destCoords)) {
return new Location(destCoords, loc.m_sZoneId);
}
if (pd.isBridge() && pd.m_eDestDir == eDir) {
ZoneDesc adjZoneDesc = m_rZoneDescs[pd.m_sDestZoneId];
Hex destPlatformCoords =
adjZoneDesc.getPlatformCoordsById(pd.m_sDestPlatformId);
return new Location(destPlatformCoords, pd.m_sDestZoneId);
}
return null;
}
So, I refactored the player and portal management so that it deals with locations and not zones and coordinates.
This cleaned up the API and, during the process, simplified the code in many places. And, as I had hoped, it got rid of the bugs I had experienced.
2023-11-09 - Zone layers
We have teleporting between zones, and zones loading/unloading, working properly.
What's in store now? Let's go back to our TODO list.
allow our "mock" player to move between zonesstop hardcoding the start platform coordinates to 0, 0, 0unload the previous zone when appropriaterender 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 last two points are a refinement/optimization which doesn't bring nothing new in terms of features, so let's postpone them. We're going to take care of them when we're "done" with this subsystem, before moving on to something entirely different.
The core point is than "rendering zones to different layers".
Why's that needed exactly?
When we are about to cross a bridge platform, we're going to see the target zone inside the portal, but only there. While a normal platform portal brings us "nearby", a bridge platform portal can bring us to a totally different place.
Remember that the content of the portal is what a secondary camera sees.
So, a simple way to only show the adjacent zone in the portal, is configuring the two cameras culling masks so that each camera only renders the elements in one of the (at most two) zones loaded.
The primary camera (at the eyes of the player) will render the current zone.
The portal camera (at the appropriate offset from the exit of the portal) will render the adjacent zone.
When we cross the bridge platform portal, we adjust the two cameras culling masks accordingly (swapping what is rendered and what is not rendered)
We just need to reserve two layers for this mechanism, which should be not a problem at all, and everything should work with very little code.
So, first we define two layers `ZoneA
` and `ZoneB
` and set some constants accordingly. I'm gonna pick layer 20 and 21. Picking two consecutive layers helps in keeping the operations simple, as you're going to see.
// extract from PR_Defs.cs
public const int iLAYER_IDX_ZONE_A = 20;
public const int iLAYER_IDX_ZONE_B = 21;
When we instantiate a zone adjacent to the current one, we're going to assign to it "the other" layer. If the current zone has layer `ZoneB
`, the adjacent will be on `ZoneA
`, and vice versa.
So, I modified `ZoneManagerBhv
` and `Zone
` so that there's a 0/1 flag, `m_iZoneLayerSwitch01
`, which tells if the current zone is a A or a B.
Every time we switch from a zone to another, we will flip the value.
To flip a 0/1 value, one can just do `val = 1 - val;
`, avoiding useless branching.
I also modified `Zone
` so that its `Instantiate
` method accept a 0/1 value, and on the basis of that, the platforms will be put on one of the two `ZoneA
`/`ZoneB
` layers. When we instantiate an adjacent zone, it will always be on "the other" layer.
// extract from ZoneManagerBhv.cs
private int m_iZoneLayerSwitch01;
public int getZoneLayerSwitch() {
return m_iZoneLayerSwitch01;
}
public void swapZones() {
//...
m_iZoneLayerSwitch01 = 1 - getZoneLayerSwitch();
}
public void updateZones() {
//...
// instantiate adjacent zone at the calculated offset
m_rAdjacentZone = Zone.Instantiate(
zd,
s_ZONE_PLATFORMS_SETTINGS,
currPlatformOffset,
destPlatformCoords,
1 - getZoneLayerSwitch()
);
}
After doing this "preparation" step, I went and took care of the camera culling setup.
`PortalRendererBhv
` is the manager that handles the secondary camera needed to properly render the portal.
We might do a separate camera management subsystem at some time, but for now I added to `PortalRendererBhv
` the additional features we need for this specific feature.
This is the gist of it:
private void adjustCullingMask() {
int iCurrLayerSwitch = ZoneManagerBhv.i().getZoneLayerSwitch();
int iSrcZoneLayerIdx = (1 - iCurrLayerSwitch);
int iDstZoneLayerIdx = PortalManagerBhv.i().isBridgePortal() ?
(1 - iSrcZoneLayerIdx) : iSrcZoneLayerIdx;
int iMainCamerasCullingMask =
-1 & ~(1 << (PR_Defs.iLAYER_IDX_ZONE_A + iSrcZoneLayerIdx));
int iPortalCamerasCullingMask =
-1 & ~(1 << (PR_Defs.iLAYER_IDX_ZONE_A + iDstZoneLayerIdx));
m_rMainCam.cullingMask = iMainCamerasCullingMask;
if (Camera.main.stereoEnabled) {
m_rPortalCamR.cullingMask = iPortalCamerasCullingMask;
m_rPortalCamL.cullingMask = iPortalCamerasCullingMask;
} else {
m_rPortalCamMono.cullingMask = iPortalCamerasCullingMask;
}
}
Basically, we calculate two culling masks which are "everything except a zone layer".
The excluded layer is always "the other zone" for the main camera.
For the portal cameras, it depends on if the portal brings to another zone or not.
If it's to the same zone, the excluded layer is the same also excluded by the main camera.
Otherwise, it's the other.
This worked well, but of course, the change only affects the platforms: the other elements (enemies, the platform selection indicators, and other debug elements like the small blue spheres representing the "bridge") are not yet assigned to one of the two "zone" layers, and consequently are always rendered. Let's finish the day with a short gameplay video, and tomorrow I'll take care of these small remaining issues.
2023-11-10 - Portal refactoring
In yesterday's video, some of the elements to be fixed where the "portal indicators", the elements that help to understand what is the currently selected target platform, and if there's a portal opened behind you. Currently, these elements are handled by `PortalManagerBhv
`, which is quite bad (something coming from the initial PoC, of course).
So, time to do more refactoring of the portal management subsystem, separating logic and visualization like I did with the player.
I simplified the monolithic, messy `PortalManagerBhv
`, adding a `Portal
` struct (for the state) and a `PortalBhv
` class to handle the scene nodes, in a similar fashion to what we did for the player management some days ago.
Then, I factored out from `Zone
` the simple logic to move a `GameObject
` to one of the two zone layers, so that I can use it elsewhere:
public static class PR_Defs {
//[...]
public const int iLAYER_IDX_ZONE_A = 20;
public const int iLAYER_IDX_ZONE_B = 21;
public static void setObjectZone(GameObject rGO, int iLayerSwitch) {
rGO.MoveToLayer(iLAYER_IDX_ZONE_A + iLayerSwitch);
}
}
For now, I put this super-simple static method in `PR_Defs
`. At some point, I'll probably do better than that: changing a property of an arbitrary `GameObject
`, the way I did now, goes against the conventions and best practices I discussed.
But it's Friday, and I want to see this feature done, so I was a bit more hacky than usual.
I assigned the "bridge indicators" (the small blue spheres on the edge of the bridge platforms) to the proper zone, and I adjusted the visibility of the debug elements so that they're only visible when the debug view is activated.
I also made sure to change appropriately the layer of the "portal indicators" (the sliding cyan elements that also work as target platform indicators, and that were erroneously visible yesterday when on the bridge platform).
There's a bit of cleanup to do next week, but I managed to have everything working properly, as shown by this quick video.
Moving around feels nice, and I can now design an arbitrarily large and complex game world made of interconnected zones.
Ok, let's keep the good habit and prepare a TODO list for next week:
allow our "mock" player to move between zonesstop hardcoding the start platform coordinates to 0, 0, 0unload the previous zone when appropriaterender 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)
So, the only two "not done" entries are about clean-up and optimization.
I think I'm going to do another bit of refactoring to clean up the architecture, but I might postpone pooling and prioritize adding features instead.
For example, something I'd like to do is handling different backgrounds for different zones. The passage through the bridge portal, right now, doesn't feel particularly special. Having different skyboxes, instead, could make it more obvious that we are teleporting elsewhere when changing zone.
And one of the possible backgrounds could be the video passthrough of the headset: that's what I had in mind for the "menu" of the game: a platform which is in your actual room, and that allows you to step into a... particular reality.
And about the actual room: I want the platform to be automatically placed at the centre of the configured play area.
We'll see if it works nicely, hopefully next week. So, updated TODO list:
allow different backgrounds for each zone (skyboxes, scenes, video passthrough)
improve architecture (even better logic/visualization separation)
reuse platforms instead of instantiating/destroying them (use a pool)
And now... weekend!