2023-12-11 - Passive zone management
Continuing the process I started last week, I'm going to do a little more refactoring.
There's still plenty of code from the "16 days" prototype, and it's time to shuffle it into a proper structure, or to replace it.
In particular, the code related to the player and portal subsystems (which, together with the zone management, is the part we've been focusing in the last weeks), still needs some work.
I did some more cleanup and refactoring, and an important change about the zone management and initialization sequence.
Until now, the zone manager did the startup zone/player initialization. Now, I changed it so that it just loads as current zone what's needed by the current `GameState
` (which contains the player current location).
Basically, now the `ZoneManagerBhv
` is "at service" of the gameplay state visualization, passive, and the loading of the initial zone is just a consequence of a certain `GameState
` being active.
The zone management subsystem won't care at all if the state to show comes from an hardcoded "new game" default state, or a loaded savegame.
I proceeded doing a little more cleaning and refactoring. I'm afraid that most of this week is going to be about "behind the scenes" changes, but hopefully I'll be able to write about some interesting details here and there.
2023-12-12 - Portal refactoring
Today I spent the whole day refactoring the portal management code, so that the portal state is part of the game state and that the visualization is cleanly separated.
The magic question is: what is the minimal information needed to reconstruct the state of an in-game object?
It's usually the same info that gets sent through the network in a multiplayer game, or that is stored into a savegame.
In the case of our portals (considering the way they work right now in the game), we only need as state:
a `
float
` indicating "how much" the portal is open: `0
` for totally closed, `1
` for totally opened, anything in between for a portal that is opening or closing (like: `0.7f
` for a70%
opened portal)a `
Hex.EDir?
` indicating an hexagonal direction for the current portal (if any)
All the other information needed to represent the portal can be deduced:
the "going in" location is nothing else that the player location
the "getting out" location can be deduced from the zone descriptions (finding the adjacent platform along the selected direction)
Then, one must think about what can cause a state change:
the gesture to open and keep open a portal
player movement (currently, only considering the head position), which can "cross" an opened portal (teleporting the player to the "getting out" location)
Let's go deeper into these two inputs and subsequent state changes.
The portal activation and deactivation is currently tied to a simple binary input: keeping both hands vertical, palm forwards, means "I'd like to open and keep a portal opened here, if possible". The successful portal opening depends on the availability of a target platform considering the player state when they perform the gesture.
For testing purposes, that can easily be simulated with a key press, which is what I did. Actually, I did better: if the `R
` key is down, it is like I'm doing the gesture, so I can press it and keep it pressed to keep a portal open, and then release it and have it close. If instead I don't want to keep the key down, I can press `Shift + R
` to switch a boolean value on and off (with the same meaning: activation gesture being performed or not).
So, we want to send to the portal update logic the current binary input telling if we'd like to have a portal opened or not. Then, what's going to happen will depend on the game state.
The portal crossing, instead, is related to the player position and can only happen when a portal has already been opened. If the player steps on the "other side", the portal crossing happen.
What does it mean to cross a portal, in terms of portal state change? The player will be on the other side of the platform, so the location will be automatically updated, but we must flip the portal direction. If the player has entered a portal going north, when they exit it they have an opened portal behind their back, going south.
It's time for another state machine to model all this.
Finally a non-trivial FSM diagram! So satisfying. It expresses all the logic we described.
Notice some important details:
the portal can only be crossed in the `
opened
` state;the opening direction is only considered when going into the `
opening
` state: one shouldn't be able to change the portal direction mid-animation, or when it's already been opened;the `
opening
` and `closing
` states are not only a function of the player input, but of the passage of time (expressed as `$dt`
in the diagram), because the `m_fOpenedLevel01
` value gets incremented/decremented by a certain amount at every frame. The amount depends on the portal opening/closing speed that we define;the `opening` and `closing` states can switch between each other at any time, depending on player input. This makes the portal very responsive and makes the game feel good: you don't have to wait for the opening/closing animation to complete to see a feedback for your actions. If you are opening the portal but change your mind when it's at 30% opening, and you stop doing the opening gesture, the portal starts closing, going down from 30% to 0%. The time needed to switch from `opening` to `opened` and from `closing` to `closed` depends on "how much portal" there is to open or close.
Enough for today!
2023-12-13 - Guess what, more refactoring
I started my day by implementing the FSM described yesterday, using an instance of the generic FSM implementation of last week.
It's been just blabbering this week, so let's show some code: the FSM implementation.
The nice thing with this approach is that you can blindly translate the diagram to code and get a robust implementation. At least, when things go right.
private static PortalStateFSM getFSM(PortalState rData) {
Dictionary<EState, Func<IPortalStateFSMView, EState?>> rStates =
new Dictionary<EState, Func<IPortalStateFSMView, EState?>>() {
{
EState.closed,
(IPortalStateFSMView rExec) => {
rExec.getData().m_openedDir = null;
rExec.getData().m_fOpenedLevel01 = 0f;
if (rExec.isInputAvailable()) {
Input<ESignal> input = rExec.fetchInput();
if (input.m_id == ESignal.open) {
Hex.EDir openDir = (Hex.EDir)input.m_rData;
rExec.getData().m_openedDir = openDir;
return EState.opening;
}
}
return null;
}
},
{
EState.opening,
(IPortalStateFSMView rExec) => {
float fDT = rExec.getDeltaTime();
float fAmount = fDT * fOPENING_SPEED;
float fNextOpenedLevel = Mathf.Clamp01(
rExec.getData().m_fOpenedLevel01 + fAmount
);
rExec.getData().m_fOpenedLevel01 = fNextOpenedLevel;
if (fNextOpenedLevel == 1f) {
return EState.opened;
}
if (rExec.isInputAvailable()) {
Input<ESignal> input = rExec.fetchInput();
if (input.m_id == ESignal.close) {
return EState.closing;
}
}
return null;
}
},
{
EState.closing,
(IPortalStateFSMView rExec) => {
float fDT = rExec.getDeltaTime();
float fAmount = fDT * fCLOSING_SPEED;
float fNextOpenedLevel = Mathf.Clamp01(
rExec.getData().m_fOpenedLevel01 - fAmount
);
rExec.getData().m_fOpenedLevel01 = fNextOpenedLevel;
if (fNextOpenedLevel == 0f) {
return EState.closed;
}
if (rExec.isInputAvailable()) {
Input<ESignal> input = rExec.fetchInput();
if (input.m_id == ESignal.open) {
return EState.opening;
}
}
return null;
}
},
{
EState.opened,
(IPortalStateFSMView rExec) => {
rExec.getData().m_fOpenedLevel01 = 1f;
if (rExec.isInputAvailable()) {
Input<ESignal> input = rExec.fetchInput();
if (input.m_id == ESignal.close) {
return EState.closing;
} else if (input.m_id == ESignal.cross) {
Hex.EDir dir = rData.m_openedDir.Value;
rExec.getData().m_openedDir =
dir.OppositeCenter();
}
}
return null;
}
}
};
return new PortalStateFSM(
"PortalStateFsm",
rStates,
rData,
EState.closed
);
}
I'm in that weird state where things are starting to be structured the right way, but to keep the game running and the code compiling I need to add a bunch of ugly hacks here and there. Basically, I need to wire the new state management logic to the code that handles the portal scene nodes and components (audio source, VFX, mesh renderers).
Can't wait to get to the clean-up stage where I get rid of all the old stuff.
Guess what? It's late and I couldn't complete the process.
This is one of those sad days where you don't commit the changes to the repository.
And tomorrow I'm busy. Bummer.
2023-12-14 - Day off
This afternoon I had to be somewhere, so no progress and no devlog.
On the upside, the being somewhere involved some waiting, and it was a waiting with coffee, cake, and "The Art of Game Design" by Jesse Schell.
2023-12-15 - Unplanned day off
I had to do some extra hours on my current "contract work that pays the bills" to meet a deadline, so no "Particular Reality" today, which is a bit frustrating considering the state I left it on Wednesday.
2023-12-16 - An extra Saturday
I'm going to try and make it up for Thursday and Friday by working today.
It's never good to leave an in-progress refactoring session for too long, as one risks forgetting details about the "unfinished business", making things more time consuming.
I managed to put things into a decent state (commit and push done, yay!).
As you can see from the following video, there's still some glitches related to the portal rendering (which I haven't changed at all in these last few days)..
But on the logic side, the portal looks quite robust and and handles some cases that weren't working properly in the previous implementation. For example, I can put my head in and out of the portal, or look at both sides of it by keeping it open and turning. Additionally, I can now pause the simulation at any time and the portal stops opening/closing midway.
I'm still not done: the shader based visual effect into the portal doesn't stop, and neither does the audio.
Next week I'll continue working on this and I'll keep improving the code architecture.