In Part 6: Skybox and Tile Actors I discussed the process of spawning the initial map.
This time I will briefly cover updating the map state and spawning and updating the worms. For this post, I won't go into much detail as there is a lot of ground to cover.
The first step to updating the map state each round was to do a bit of a refactor. Previously I built different Actors for each tile type and each dirt block type. This was causing some issues with updating the actors down the line.
The refactor consisted of removing all the separate Actors and building a single actor with an associated static mesh. After spawning the actor, I updated the mesh based on the tile type. So now the entire map only contains a single type of Actor.
When spawning the map initially, as detailed in a previous post, I checked a collection of spawned tiles, and only spawned the new tile if it is not already contained in the collection.
Now, when the tile is contained in the collection, it has already been spawned and I can check if there is any transition that needs to happen.
At the moment (version 2 of the rules) there are only two transitions to handle:
- Dirt tiles can change into Air tiles (when a worm digs)
- Powerup tiles can change into Air tiles (when a worm picks up the powerup)
The new part of the event graph looks like this:
For each tile, I checked if the tile exists in the collection. If it did, I fetched the tile type as it exists in the collection (i.e. the previous round) and I compared to the tile type in the current round to determine the transition between types. If either of the two valid transitions were found, I updated the mesh on the actor, and set the material. I finished by updating the collection to ensure the subsequent rounds handle the state transitions correctly.
In the case of a transition from a Dirt tile to an Air tile, I spawn a simple particle effect at the place of the transition to show the 'digging'. I won't go into much detail here, but I might do a separate post on particle effects in Niagara (Unreal Engines new effects system).
Using a single actor type made this part pretty straightforward. Next up, spawning some worms...
For creating the worm model, I used Blender 2.8 sculpting tools. While I won't cover the entire process here, I can post some more details if anyone is interested. Just drop me a note on the Contact Page.
The base mesh for the worm, with the skeleton ended up looking like this:
I used the UV Unwrapping tools in Blender to unwrap the mesh, then exported the unwrapped mesh and did a very rough paint-over in GIMP. The result was then imported into Blender as a texture to apply to the worm model.
This was a very rough paint-over since this was an experiment, and it ended up sticking around. I may revisit this at some point, but it's unlikely since the models onscreen are too small to see the artefacts.
The final model:
Rough but workable.
Back in the initial post I mentioned the presence of two CSV files that summarise the players' moves. Those CSV files is what I used to drive the spawning and movement of the worms.
The spawning and the worm movements ended up looking like this:
Some redundancy in the worm location update can be refactored in the future, but in the meantime lets break it down.
First up, I created a boolean variable ShouldSpawnWorms to check if the worms should be spawned in the first round. After, I do a sanity check on both PlayerA and PlayerB move counts, to ensure there are moves left to be checked. This function is also a timed function that runs every second, alongside the function that updates the map.
The highlighted section contains calls to a custom function, SpawnPlayerWorms, to spawn the worms for each player. That function ended up looking like this:
Again, this could be improved in the future but for initially for each player I retrieved the location for each of the players' three worms and called another custom function, SpawnWorm, to place the worm and set the mesh.
The first part of SpawnWorm function looks like this:
Pretty straightforward. I took the x and y coordinates of the worm in the initial null move (The first line in the CSV), multiplied that by tile sizes, similar to how the tiles have been placed. These x, y coordinates are then fed to a Make Transform built-in node. Here I picked up an issue with the worm model I built in Blender, in that I didn't take into account the base scales. To fix this, as part of the Make Transform, I set the scale on all axes to some arbitrary value until it looked ok.
After I spawned the worm, I took the reference to the skeletal mesh of the newly spawned worm actor, in order to get the location of the worm in world coordinates. I then use the built in node to find the rotation of the vector from the current position, towards the map center. I then rotate the worm actor using this value, so that when spawned, all worms face the map center. (The additional 90 degree subtraction is needed to fix a mis-mapping of the forward vector when I exported from Blender).
The last part of the SpawnWorm function looks like this:
After spawning each worm actor, I added them to a Map data structure using the worm ID string as key (in the format P[A|B]W[1|2|3] where A|B indicates the player and 1|2|3 indicates which worm. Using this ID, I then switched on the player indicator (A|B) and then spawned a head-band actor, with the correct colour depending on which players' worm is being spawned.
To 'attach' the headband model, I had to add a Socket Component to the worm actor, and position that where I need to attach other actors. After spawning each headband for each worm, I used the AttachToActor node and select the socket I created.
Now we have both players' worms spawning all facing the map center:
This section is currently in a bit of a messy state. I repeat the same logic for each worm separately and the whole process is repeated twice for each player. This will have to be refactored into reusable methods at some point.
Clearly not ideal. I'll run through the logic for one of the worms to explain.
First I start by getting all the details from the player move array for the current round. This includes positions of all three worms, as well as the active worm ID. Event though all three worms positions are used to create the movement vectors, only the path for the active worm will be triggered each round.
Using the move command type to determine which command to handle was the first step. In case of a move command, I then checked the active worm ID. Depending on which worm is active for this round, I pulled a reference to the correct worm from the set of spawned worms.
After it's been determined that the command issued is a move command, and the active worm reference has been found, I ran the following logic:
I first got the reference to the active worm from the set of spawned worms, then extracted the worm actors location and rotation. The first step was to take the X,Y coordinates of the destination and turn that into a location vector. Taking the actors current location as well as the new location and feeding them into a Lerp (Vector) node gives a smooth transition between locations. The Lerp (Vector) node timing is driven by the alpha input from the MoveActorPAW1 timeline node.
The timeline node sets up a simple one second curve, with a few values to determine how the transition between the old and new location will run. The curve looks like this:
The timeline Update output pin will use the value on the curve to indicate how far between the current and new location the worm is and send that value to the Set Actor Location node. This update is called on each game tick and the timeline is set to completely interpolate between old and new location in over the course of one second.
The rotation is similar, but with one quirk. To start, I used the current location and the new location to determine the worms relative rotation if it were to 'look at' the new location from the current location. This is done by using a built in Find Look at Rotation node. Very convenient.
This result in a Rotator output, which is just a vector containing the relative rotations on each axis.
Due to the problem with axis mappings when I exported from Blender, as mentioned earlier, I needed to split this rotator into the separate pitch, yaw and roll values using the Break rotator node. I could now proceed to subtract the corrective 90 degrees from the yaw value (as I did when spawning the worm).
These values are then recombined into a rotator using the Make rotator node.
Same as with the location, the timeline is used to blend from the current actor rotation to the newly calculated rotation.
The only quirk is that the worms would rotate in the direction of movement, and rotate back to 0 degree yaw upon arriving at the new location. To be honest, I could figure out why, but setting the actor rotation before triggering the timeline resolved this issue.
With all of that done, I now have worms spawning on the map, moving around and facing the direction that they are moving in. The map also updates each round to reflect the effects of worms digging out dirt.
Next steps will be to add animations to the skeletal mesh of the worm actor to make actions more apparent. After that I will handle the digging action, to let the worm rotate in the direction it's digging.
Then onto the shooting action.
Once those are done, I will pull in the new rules, with the spreading lava and the new worm type.
In the meantime, here's what we've got:
As always, any comment or critiques are welcomed. If you want more detail on something I've glossed over, or would like a full post on some of the things I didn't cover, please let me know over HERE