2023-11-20 - Fixing portal and portal indicators
So, we have a bug gloaming over us: the "portal indicators" didn't rotate properly.
Let's start by fixing that, which should be easy, and then move on to clean up and complete this "play area management" system we sketched last week.
The problem was that the portal indicators are not parented to platforms, they are "loose" elements that get properly positioned and rotated depending on the current target platform selection and portal state.
They did take the global position of the platforms correctly, but not the rotation, which was left untouched (when there was no "world origin" adjustment based on the play area configuration, it was fine that way).
So, the fix was easy: in `ZoneManagerBhv
`, I went from a `Vector3 getLocationPos(Location loc)
` to a `Pose getLocationPose(Location loc)
` method, and changed the calling code accordingly.
When testing this, I realized the portal itself was affected by a similar problem.
The portal position and rotation were only adjusted at a state change (open, close).
To have them stay properly positioned, they should instead update their pose at every simulation step, taking into account that the world might have moved since the portal opening.
So, I went and did a little more refactoring on the `PortalManagerBhv`/`PortalBhv
` scripts.
Now, `PortalBhv
` does not store position/rotation of the portal, calculated at opening/closing time: it stores the logical locations of the two platform put in communication by the portal.
Those locations are then used at every update to fetch the actual, proper positions and rotation needed to correctly adjust the scene elements.
Sometimes, bugs are your friends, helping to point out weaknesses in the code and opportunities to improve the architecture. Which is still not great, but ok for today.
2023-11-21 - Wrapping up play area management
There's just one more thing to do before wrapping up the play area management.
Currently, `RealityManagerBhv
` calls `GetDimensions()
` and `GetGeometry()
` at every update cycle, and adjusts the world origin accordingly.
That's nice because it's simple and robust, but it's also wasteful, because a change of boundary is quite rare: usually the player will not change their headset configuration while playing.
It's not really that the calculation of the center pose is expensive computationally, but I usually try to avoid calling any API that might allocate memory from any update loop.
That's usually a source of performance problems.
Let's see the signature of `GetGeometry
`:
public Vector3[] GetGeometry(OVRBoundary.BoundaryType boundaryType)
It returns an array of `Vector3
`. And if you go and check `OVRBoundary.cs
` you will see that, in fact, there's this line:
Vector3[] points = new Vector3[pointsCount];
This API design itself is an indication that they don't expect you to call the API repeatedly, but only when needed.
A reasonable alternative API for that other approach would have been:
public void GetGeometry(OVRBoundary.BoundaryType boundaryType, ref Vector3[] points)
This would require you to allocate an array of `Vector3` once, at startup, and then ask the API to update its contents (writing always in the same memory space, with no further allocations).
[...]
private Vector3[] m_rGeometry;
private void Awake() {
[...]
m_rGeometry = new Vector3[4];
}
private void Update(){
[...]
OVRManager.boundary.GetGeometryNoAlloc(
OVRBoundary.BoundaryType.PlayArea,
ref m_rGeometry
);
}
This would be a bit more cumbersome for the user, and it would be restricted to returning a specific, known in advance, number of points. Maybe they want to be able to return a different number of points in the future? I think that would break a lot of code expecting it to return exactly four points, so I doubt it. They probably just chose convenience for the user.
Anyway, let's stop speculating and do the best we can with what we have.
The other piece of the puzzle to fit in to be able to handle a boundary change appropriately is the event fired by the SDK when a change happens:
OVRManager.display.RecenteredPose
So, I must revise my simple logic so that it only fetches the new data and recalculates the world origin:
at startup
when this event is fired
A couple of flags and conditionals would work, but a very simple state machine looks like a better way of modelling the revised logic.
We have three states:
`
not_initialized
` : the startup state - we stay into this state until `OVRManager.OVRManagerinitialized
` returns true, then we move to `not_configured
``
not_configured
` : as soon as boundary data is available, we fetch it and configure our game world accordingly, then move to `configured
``
configured
`: we sit idle until a `RecenteredPose
` event is fired, which sends the execution back to `not_configured
`.
In diagram form, this looks like this:
Soon enough I'll add a proper, generic FSM implementation to use in many places of the project.
For now, let's keep it simple and implement it in the most crude and basic way possible.
private enum EState {
not_initialized,
not_configured,
configured,
}
private EState m_state = EState.not_initialized;
private void updatePlayAreaFSM() {
switch (m_state) {
case EState.not_initialized:
if (OVRManager.OVRManagerinitialized) {
OVRManager.display.RecenteredPose += () => {
DebugManagerBhv.i().appendText(
"RecenteredPose() handler\n"
);
m_state = EState.not_configured;
};
m_state = EState.not_configured;
goto case EState.not_configured;
}
break;
case EState.not_configured:
if (OVRManager.boundary.GetConfigured()) {
updateWorldOrigin();
m_state = EState.configured;
}
break;
case EState.configured:
// do nothing
// can only exit through state change in event handler
break;
}
}
That's basically it: a switch and an enumeration. It's pretty standard stuff, I'd only like to point out two details:
the event handler for the `
RecenteredPose
` event is defined on the fly with a lambda expression, it's super simple and this way we keep things tidy and compactin case this approach ends up being not reliable (e.g. the event is not always fired when a boundary change happens) we can easily fall back to the previous behaviour by not going into the `
configured
` state, so that the configuration is done at every frame, or we could introduce a time check in `configured
`, so that the execution goes back to `not_configured
` periodically (e.g. every two seconds)there's a `
goto case
` to have the switch fall through, so that we can execute the `not_configured
` case in the same frame when we exit `not_initialized
`, and not at the next update.
This is a C# thing, and it's the same behaviour you would get in C/C++ skipping the `
break
` statement at the end of a `case
`. As it's the less common option, they decided that it must be requested explicitly.
The `updateWorldOrigin()
` method contains the actual data fetch and world center pose calculation. I could embed the code into `updatePlayAreaFSM()
`, as it's not called from anywhere else, but it would make the `switch
` a bit harder on the eyes, while now you can basically understand the FSM logic at a glance.
I think I already showed most of this code in a previous entry, but still, here's the revised snippet.
private void updateWorldOrigin() {
m_rGeometry = OVRManager.boundary.GetGeometry(
OVRBoundary.BoundaryType.PlayArea
);
m_vDimensions = OVRManager.boundary.GetDimensions(
OVRBoundary.BoundaryType.PlayArea
);
if (m_rGeometry.Length != 4) {
DebugManagerBhv.i().appendText(string.Format(
"Unexpected number of geometry points: {}\n",
m_rGeometry.Length
));
return;
}
m_worldCenterPose = calcCenterPose(m_rGeometry);
DebugManagerBhv.i().appendText(string.Format(
"Play Area size: {0:0.00}x{1:0.00}\n",
m_vDimensions.x, m_vDimensions.z
));
}
I tested the change on my Quest and it works well, but I forgot to capture a video, so take my word for it, because it's getting late!
2023-11-22 - Main loop design
This afternoon (my usual working time slot for the project) was stolen by a headache.
Now I'm ok, but it's kinda late so I'll take it easy and just do a bit of design related to the things I'd like to develop in the upcoming days (and weeks).
Everybody knows what to expect when starting a game.
You get some kind of loading screen with company logo etc, then you are presented with a menu screen.
In the menu, the minimal set of available options are
new game
options
exit
Usually, if the game has been previously been played, there are, depending on how the save system is organized, one or both of the following options:
continue game (loads the latest savegame)
load game (lets you pick the savegame to load)
The save system is another critical thing to discuss. Not so many years ago, players needed to explicitly save the game state, usually going to a save game screen, selecting a save slot and sometimes writing a short description for the savegame.
A quicksave button, instead, was used to rapidly save the game state to a predefined slot, without interrupting the flow of the game.
Nowadays, an autosave system is basically taken for granted. Players expect to quit the game, and then automagicallly come back and restart playing from where they stopped.
Depending on the game type, there might be more or less explicit checkpoints that trigger the autosave.
Autosave systems are convenient, but being able to explicitly save at a specific point is also valuable. For example, in a branching narrative game, you might want to save at a specific scene, keep playing taking a branch, and after a while go back to that savegame to try another branch, without being forced to do a completely new playthrough from the beginning of the game.
I've seen many games preventing me to do that, and I still feel it very annoying.
That said, is an approach which makes sense if a game is linear, or if the player actions done completing a gameplay sequence have no long-term repercussions.
What's going to be the save strategy of Particular Reality? I'm not sure yet, even if I have an idea that it's not the right time to discuss.
What I definitely know is what I'm going to need during development, which is the possibility of saving the state at any moment to a slot, with no limit on the number of slots, and then being able to load that specific slot. That's critical for testing, even if it might end up not being a feature exposed to players.
Anyway, I want to make going back into the game as seamless as possible.
Talking about interrupting the gameplay and going back to it, another key component to take into account is the pause system.
During gameplay, one should always be able to pause, at any time and without problems.
In some games (in my opinion, not very well engineered) the pause does not work that well.
For example, it might be not possible to pause during a cutscene or a dialogue. Or, the pause might not be instantaneous. Even worse, it might break the game state in some subtle (or less subtle) way. Like, skipping the audio in an unpleasant way. Or breaking some animation.
From the paused state, the player should be able to go back to the game, or exit to the main menu. Actually, this can often be simplified, and the pause state can be the main menu itself. The same "continue" option that, right after having launched the game, allows to load the latest savegame, can in this case act simply as "unpause". It's still about "continuing" to play, after all.
I want that jumping into the game feels cool. So, something I've been thinking for a while is having at startup a `limbo
` zone that makes use of the passthrough video feed.
The limbo will feature a single platform with a bridge portal that brings into the menu zone. From the menu zone, one would then be able to jump back into the game.
I'm not sure yet if it's viable, but what I want to try is having the same interaction and movement system that you use in-game to be also used as a diegetic menu system.
The menu zone might end up being unnecessary, and be collapsed together with the limbo. It still makes sense to me to start by having it, at this stage. It could for example be the zone from where you can jump into different saved games, using different platforms. The limbo zone, in mixed reality, is intrinsically limited to a single platform and we couldn't handle that.
So, let's make a list of things that we need to do to build all this.
define two new zones, `
limboZone
` and `menuZone
`* allow `
limboZone
` to have the passthrough video feed as background, instead of a skyboximplement the activation of the paused state, which will bring the player back to the `
menuZone
`implement the deactivation of the paused state, which will bring back the player to the zone where they were playing
implement the paused state itself, that should make visually clear to the player that the game simulation is interrupted, and how to go back to playing (or to quit the game)
implement the save functionality, allowing a game state to be stored to the file system
implement the load functionality, allowing to load a saved state from the file system, and resume its execution
Every entry of this list contains many details and subproblems that it doesn't make sense to try to address now. We will deal with them "on the go".
I have a couple of ideas to try out about the activation and deactivation of the pause, and I look forward to see if it feels cool as I imagine it.
Anyway, all this is going to be a significant amount of work, but I think it's the right moment to do it. Why?
the save/load system, done properly as I'm imagining it, will force us to have a very good separation between game logic and visualization, and a precise control about the execution of the game simulation. We are going to be able to interrupt the execution at a specific frame, save all the logic state, and then be able to load such state and have the visualization layer properly display it, while the simulation will restart its execution from there as if it never got paused;
the menu system is going to be useful during development and testing;
the mixed reality startup sequence could help with gaining a bit of attention on social media, showcasing one of the recent Quest 3 features;
When this set of correlated features (startup, menu, pause, save/load) will be be complete, we are going to have a pretty good high level architecture of the whole game, to expand and build upon. And after that? Probably, the combat system and the enemy AI.
Meanwhile, it's time to go to bed!
2023-11-23 - Game startup
Ok then, let's start implementing yesterday's ramblings.
As first step, I'm going to add two new zone definitions, `limboZone
` and `menuZone
`.
`
limboZone
` is going to be a single platform, with a bridge to enter `menuZone
`, and it's going to be in mixed reality`
menuZone
`, for now, will involve four platforms, allowing to go back to `limboZone
`, to start a new game, or to continue a previous game
I did a sketch (after finding a very useful website that lets me print an hexagonal tiled pattern, even if that doesn't really show up in the photo) of the desired layout.
Can't wait to have a proper level editor that I can also use to do this kind of diagrams.
Note that I could add other platform on the right of "continue game", for different save slots. The `menuZone
` could be procedurally generated to have as many platform as needed on the right branch, allowing to access different save slots. An "options" platform could also be added somewhere. The central platform of the `menuZone
` is not really needed, I could have skipped it, but I feel like it could be better to have an intermediate platform there. I might cut it later.
Actually, in terms of minimalism, you might wonder why we don't use a single platform for the `menuZone
`, with three bridge portals to `limboZone.init
`, `corridorZone.start
` and to wherever continue game would bring us.
Well, when defining the bridge platforms I limited them to a single destination. I did that so that there's not more than two zones that have to be loaded. Otherwise, a platform could potentially bring to five different zones along its directions (each side of the hexagon, minus one that must have been used by the player to enter the platform). I might revisit this at some point, but it doesn't sound that bad right now, and ideally the limbo and menu zones will also teach new players how to move in the game, but in a quiet, safe environment with no enemies or hazards. So having to do a couple more platform jumps should be ok.
As they're not working properly after the changes done in the last weeks, I'm also going to temporarily disable enemies and pick-ups.
We're going to put them back-in (and have them working properly) soon, but right now, they're just a nuisance during testing.
I went and added the two new JSON files for `limboZone
` and `menuZone
`, and added two new skybox materials, with two other color selections, to be used as their background.
Of course, I don't have the "continue game" feature yet, so I configured both "new game" and "continue game" bridge plaforms to just bring the player on the start platform of `corridorZone
`, the zone where we previously started before today.
Compact view of the bridge definitions:
`
limboZone.init
` -> `N
` -> `menuZone.start
``
menuZone.start
` -> `S
` -> `limboZone.init
``
menuZone.new_game
` -> `N
` -> `corridorZone.start
``
menuZone.continue_game
` -> `N
` -> `corridorZone.start
`
Of course, I wasted half an hour because of an error in the JSON data nesting. Did I mention I should work on a level editor?
Anyway, fixed that, it worked. I also had to disable enemies and pick-ups, as I anticipated, and it wasn't just a convenience but a necessity: my placement code never placed more than an item on an already occupied platform, and of course, being the `limboZone
` made of a single platform, there were no sufficient platforms to place the enemies of the first wave. As I hadn't capped the number of the enemies to the number of platforms, the placement code would get stuck trying to find random empty platforms, but always trying the only platform in the zone, in an endless loop. That, thanks to the awesome architecture of Unity, where editor and game run in the same process, forced me to kill it from the task manager and reopen the project (which takes a ridiculous amount of time). The joys of game dev, offered to you by the Particular Reality DevLog.
Tomorrow, I'm going to try putting the passthrough video feed in place of the skybox in the limbo zone.
2023-11-24 - A limbo between realities
As I anticipated, I'd like to have the limbo zone in mixed reality.
Basically, I expect to be able to have the passthrough video feed as background instead of the skybox. Time to check the Meta documentation/samples and see if things are actually that simple.
To get the basics working, I had to add a `OVRPassthroughLayer
` script on the `OVRCameraRig
` and change a couple of settings, as explained in the documentation here.
The passthrough layer is shown if the camera clear flags are set to `Color
`, and the color is transparent black. But of course, I want the passthrough shown only in `limboZone
`.
This means that I had to adjust the main and portal camera settings depending on the zones involved.
Do you remember `adjustSkyboxes()
` in `PortalRendererBhv
`?
It's where we adjust the camera skybox materials, and it's the natural place to also set the clear flags.
private void adjustSkyboxes() {
Zone rMainCamZone = ZoneManagerBhv.i().getCurrZone();
Zone rPortalCamZone = PortalManagerBhv.i().isBridgePortal() ?
ZoneManagerBhv.i().getAdjZone() : rMainCamZone;
Material rMainCamSkyboxMat = rMainCamZone.getSkyboxMat();
Material rPortalCamSkyboxMat = rPortalCamZone.getSkyboxMat();
bool bShouldUsePassthrough = OVRManager.OVRManagerinitialized
&& OVRManager.IsPassthroughRecommended()
&& OVRManager.IsInsightPassthroughInitialized();
CameraClearFlags mainCamClearFlags =
bShouldUsePassthrough && rMainCamZone.isLimbo() ?
CameraClearFlags.Color : CameraClearFlags.Skybox;
CameraClearFlags portalCamClearFlags =
bShouldUsePassthrough && rPortalCamZone.isLimbo() ?
CameraClearFlags.Color : CameraClearFlags.Skybox;
m_rMainCam.clearFlags = mainCamClearFlags;
m_rMainCamSkybox.material = rMainCamSkyboxMat;
if (Camera.main.stereoEnabled) {
m_rPortalCamRSkybox.material = rPortalCamSkyboxMat;
m_rPortalCamLSkybox.material = rPortalCamSkyboxMat;
m_rPortalCamL.clearFlags = portalCamClearFlags;
m_rPortalCamR.clearFlags = portalCamClearFlags;
} else {
m_rPortalCamMonoSkybox.material = rPortalCamSkyboxMat;
m_rPortalCamMono.clearFlags = portalCamClearFlags;
}
}
Basically, we find out, for both sides of the portal, if the zone is the limbo, and in that case set the `clearFlags
` to `CameraClearFlags.Color
`. Otherwise, we use the skybox, of course.
I added an extra predicate that needs to be `true
`: the passthrough must be supported by the device, and the user must be in passthrough mode when launching the game.
Not sure about this last detail. I will probably force passthrough if available, but for now it sounded like an easy way to test the game startup with or without passthrough.
With this check, if you are in immersive mode and run the game, the limbo uses the skybox. If instead you're using the Quest shell in passthrough mode, the limbo keeps using passthrough.
Ideally, I'd want some kind of almost seamless transition between passthrough in the shell and passthrough in the game. Not sure it's going to be technically possible (maybe if I ditch Unity?), but still something to strive for.
If you are wondering how the `isLimbo()
` method works, I hacked in a very lame check on the zone identifier. If it matches the startup zone id, then it's assumed to be the limbo.
Do I like it? No. Does it work and took 20 seconds to add? Yes.
Here's a screenshot of entering the menu zone from the limbo platform:
...and here's where I turn back, in the menu zone, and open the portal back to the limbo:
Far from perfect, but still pretty cool.
I have some ideas to improve the effect, but it's going to be after I have completed the "particular" avatar visualization.
Yeah, I know, you wanted a video. But the place is a mess, and a couple of carefully selected screenshots is all you get this time!