2024-05-27 - Creating and destroying enemies
This week I'm super-busy, but I don't want to stop working on the project. I'll try to put in at least a couple hours every day, and maybe I'll do some extras on Saturday to balance things out.
That said: time to build upon the groundwork laid last week and spawn some enemies!
As previously discussed, in the "state management" layer, at least for now, it is the zone state manager which handles the enemy waves state update. I established that an enemy can't cross a zone boundary, so it felt the natural place in the data hierarchy of the game.
Consequently, in terms of instantiation and destruction of the enemies prefabs, it makes sense to similarly handle it together with the instantiation and destruction of the zone itself.
Will it be flexible enough to cover all cases the game will need? Probably not, but it should be quick to do, and it fits the current architecture.
I temporarily changed the wave definition for the `corridorZone
` from two purple sphere to one purple and one green: this way, I don't need to reach the `rightTriZone
` to test the spawn of different kinds of enemies.
Then, I successfully activated the instantiation and destruction of enemy instances, together with the instantiation and destruction of the platforms: I can go around the game world and the purple/green prefabs properly enter and exit the scene hierarchy.
There's a "small" problem: the enemies get spawned at the zone origin. Why?
I quickly figured it out: I was querying for the platform position from the zone definition in the presentation layer, which - obviously - is not ready yet, as I'm building the wave of enemies that will be part of exactly that zone definition.
So, I was bit by a default value returned if the actual value is not available, with a nice `// TOFIX
` comment on the side. It turns out the `// TOFIX
` comments don't help in avoiding errors if you never go and actually fix the problems you know are there!
I guess I know where to start tomorrow...
2024-05-28 - Fixing the spawn positions
Ok then, let's try to solve properly the enemy spawn positioning issue from yesterday.
Until now, I didn't really need the platform positions in the state layer.
They don't move, so I just calculated the proper offset of a platform on the basis of its coordinates, and set that offset when instantiating its game object.
This is different, because enemies will need to move around.
I could use the spawn definition to calculate the enemy spawn position, like I do for the platforms, but that fix would stop being useful as soon as I want to move them, which is definitely going to happen very soon.
So, let's do the right thing and start handling the enemy positioning from the state layer.
I added a `PosRot
` in the `Enemy.State` structure, to store the zone-relative position and rotation of the enemy entity.
Conveniently, in the scene, I'm parenting the enemy `GameObjects
` to the "zone parent" node. This way, I can set local position and rotation on the basis of the `PosRot
` in the state, and everything should work fine even when I move the zones around (read the entries for weeks between 4 and 7 to know more about that).
I calculated the initial pose so that I can have a correct startup state, and the rest followed naturally from there.
Here's a screenshot showing a wave of enemies spawned on the correct platforms, as specified in the JSON definition on the right:
2024-05-29 - Getting rid of `ZonePlatformSettings
`
There's something I "remembered" about the project while working on yesterday's fix which is bothering me a little.
I left as parameters defined in the code the radius of an hexagonal platform and the distance between two platforms.
Potentially, this would allow me to change those values from zone to zone, but I'm not currently defining these values in the zone description files: I have specific values hardcoded in `PR_Defs.cs
`.
But I still pass the values around, which is a bit annoying when they could just be global constants.
I feel it's time for a design decision. Do I really want to have zones where these parameters change?
Honestly, I think I don't! The single platform radius must fit into the play area, or the whole concept of the game locomotion breaks. I could make them smaller, but that would make movement more cumbersome: you need to be able to easily step into a portal to go to an adjacent platform.
So, definitely not the radius change. What about the distance between platforms? That might be less problematic, but I don't see it adding any gameplay value. It could maybe change the aesthetics of zones a bit, but it could also be a bit confusing, and it would limit the kind of visual effects I want to add.
All things considered, I think I can lock these two values and simplify the code.
It's fine trying to be generic while things shape up, but I think there's value in periodically revising these decisions and trying to keep things simple.
Done: I got rid of the `ZonePlatformsSettings
` structure and all its usages, simplifying the code in many places.
I guess today was one of those days where you just remove unnecessary code, which is always good (well, at least when nothing breaks during the process).
2024-05-30 - Enemy and EnemyWave state machines
While we want to have different kind of enemies with different behaviours, there's a core life cycle which can be shared by all of them: they appear in the scene, execute their behaviour (which could include roaming around, attacking the player, defending a platform etc) and, ultimately, get destroyed and disappear.
It's time to start working on this, because to be able to test the "wave change", I need to be able to destroy enemies.
Let's start with an important detail: a `GameObject` for an enemy entity that gets instantiated when we enter a zone, as we did on Monday, can be "active but doing nothing", and so it must be able to represent a `hidden` state: we can't assume that, when we spawn it, it must appear in the scene. Consequently, we must explicitly define and handle a `hidden` state as parte of the enemy life cycle. I'm keeping instantiation and destruction of instances confined to few, specific moments. This will be valuable in the future.
Well, do you miss my state machine diagrams? It's time to prepare one...
an enemy instantiated but not yet "alive" is going to be in the `
hidden
` statewhen its wave gets activated, the enemy will transition to the `
materializing
` state, which will involve an animationwhen the animation is completed, the enemy will enter its `
core
` state, which for now could be an idle loop. At some point, in this state we're going to execute a nested FSM taking care of all the cool stuff an enemy will be able to do when activewhen hit, an enemy will get destroyed, transitioning to the `
destroying
` state, involving another animationwhen the destruction animation is completed, the enemy will reach is final state, `
destroyed
`
In my first draft, I sent the enemy back to `hidden
` after `destroying
`, but then I decided that it's better to keep them separated. They might visually be the same (enemy not visible), but logically speaking they're different: one is ready to enter in action, the other has been destroyed.
Additionally, they might not be visually similar: we might decide to show some remains for a destroyed enemy, especially while developing, for debugging purposes.
What about the FSM for a wave?
The structure should be pretty similar, but the logic a bit different:
I avoided using the spawn terminology and used materialization to emphasize that we are not creating the objects at that point, but activating them: creation and destruction is handled separately as we already discussed.
To complete the day, I implemented the two state machines into `EnemyStateManager
` and `EnemyWaveStateManager
`.
A "blind", untested implementation following the diagrams: tomorrow, I'll try to see it in action.
2024-05-31 - Enemy presentation layer
We instantiate and destroy enemies game in the scene according to the active zones, and we have defined a basic FSM handling their state updates.
It's time to do another important step: implement the presentation layer of an enemy, which basically means "setting" its `GameObject
` so that it correctly represents what is described by its state.
To get started, I went back to the basic enemy implementation I did for the Proof of Concept prototype.
I used two simple procedural animations for the spawn and destruction of the "purple spheres", consisting of a scale change driven by an easing function.
Then, I have the particles VFX, and four sound effects:
`
enemy_spawn
` for the materialization`
enemy_alive
` with a looping sound for the `core` state`
enemy_pop
` for the destruction`
enemy_leech
` used when the enemy was damaging the player
We don't need `enemy_leech
` yet, but I definitely must use the other sounds.
I adapted the two simple animations and the sound playback so that they are driven by an `EnemyStateManager
`.
The system works like this
a presentation behaviour (`
EnemyBhv
`) on the enemy prefab is "bound" to an `EnemyStateManager
` by setting the data needed to fetch from the simulation state a specific `EnemyStateManager
` (what zone? what wave? what enemy in the wave?)during gameplay, that binding data is used to fetch the right `
EnemyStateManager
` from the whole game simulationthe `
EnemyStateManager
` data is read and used to "configure" the prefab with a `set(EnemyStateManager rESM)
` method
I don't remember how much I went into the details, but it's exactly how I already implemented the portals and the bridge portals "covers".
I remember that testing them was a bit annoying, so this time I'm going to do a bit of extra work to make debugging and tuning easy.
Ok, I'm pretty happy about the result: I modified `EnemyBhv
` a bit and did a `TestEnemyBhv
` that can drive `EnemyBhv` with manual input via the inspector.
When I add a `TestEnemyBhv
` to an enemy `GameObject
`, it disables the `EnemyBhv
`, meaning that it doesn't run its `Update()
` method (which fetches the `EnemyStateManager
` of the enemy and calls `set(EnemyStateManager rESM)
` to configure the `GameObject
`).
Instead `TestEnemyBhv
` hijacks the processing and calls the `set
` method with its own `EnemyStateManager
`, operating on test data accessible through the inspector.
So, via inspector UI, I can
set any init state and data (as if they're coming from a save game), and initialize the execution with those settings, pressing "Reinit"
inspect the current state data, and see how it changes following the FSM execution
use some buttons to send the events that the FSM can receive (in this case, `
Materialize
` and `Destroy
`), and check that the processing works as expectedstop the normal, time based update, and execute the FSM one timestep at a time, pressing the "Step" button, or directly manipulate the state data (in this case, the animation progress)
Let's see all this in action with a short video:
This should cover all my needs, and I might extend it to save and load state configurations to test more complex situations.
With a little more work, I should also be able to generalize most of the test behaviour so that it works with anything built using my FSM-based gameplay system, but I'll think about that another time.
Going back to the enemy presentation, this is a good start but it's not perfect: the VFX update doesn't use my simulation time, which sucks.
But I'm out of time, so, let's write a TODO list for next week, and call it a day.
go back to the VFX graph stuff and see how to control the VFX simulation time, then bind it to my gameplay system
handle basic interaction with the enemies, so that the player can destroy them and clear waves
As soon as these two points are done, I will be ready to port to the new implementation the enemy AI from the PoC (basic movement towards the player, and energy leeching attack).
Until next time!