Implementing the Battle System for The Marker Game
I wanted to take some time today to talk in a bit more detail about the steps involved in implementing some of the functionality for my week 3 game, The Marker Game.
The format for the battle system in The Marker Game is turn-based combat. On your turn, you have the option to move some or all of your units and/or attack your opponent's units -- and then once you're done, you end your turn, and your opponent gets their chance to maneuver their units and attack you. Today, I'd like to go into a bit more detail about the development process of bringing this turn-based battle system to life.
Setting Up The Characters
The first thing I had to do was create a game world where the battles would take place. I'd decided on making this a hexagonal grid. As I mentioned in my last article, I created a level editor that I could use to draw different terrain elements onto my grid. Here's a little preview of that editor in action:
Now that I had a game board created, the next thing I worked on was actually instantiating some characters onto the hexagonal board. I created a list of characters which would belong to the player's "team", and then I wrote some code to identify one of those players as the "active player", which would be highlighted by a square cursor. I also implemented the ability to use the tab key to switch between the characters on your team. Now, you had a roster of your own units, and you could easily toggle between them.
Now that there were some characters on the board, I needed to implement a way for the user to maneuver their units around the board. For this kind of system, I thought that the best way to control the characters would be to implement some kind of point-and-click system, where you could click on the hexagon you want your character to move to and then they'd make their best effort to reach that target destination.
The problem with that kind of movement on an open hexagonal grid, of course, is that the user will expect the units to move along the shortest path to that destination square. So the first thing I did was to implement an algorithm which would find the shortest path between the current unit and the target square, navigating around other units and around natural obstacles like mountains or bodies of water.
Now that I had the functionality to compute the shortest path to the destination hexagon, I spent a lot of time trying to get the movement along that path working correctly, both in terms of functionality and visual animations. I didn't want the players to just instantly appear at their destination hexagon, so I implemented some code which had a player run along the path to their square. In The Marker Game, a character is only allowed to move a certain number of spaces per turn, so if you try to move a unit and they've already exhausted their step count for that turn, it prevents them from moving.
Attacking Enemy Units
Okay! So far, so good. We've now got a roster of units that can move from their current location to some target destination. The next thing we need to give them is the ability to attack enemy units.
So far in The Marker Game, I've relied on basic animations -- but for attacking other units, Unity's basic animation suite that I discussed back in week 1 was not sufficient. I needed characters to swing swords, react to damage, faint to the ground when they died, and potentially more things. I found an awesome asset called the RPG Character Mechanim Animation Pack -- it coms with a pretty steep price tag, but after evaluating some alternatives and examining what came with this pack, it seemed like it would probably cover all of my humanoid animation needs for the rest of One Game a Week. At this point I'm a little bit more than halfway through my budget of $500 USD, but I think the assets that I've gotten so far have a pretty good amount of reusability. I hope you'll forgive me if you see these characters and animations use again in a few more of my games along the way.
So now that I had all the necessary animations, I started to implement the functionality I'd need for combat. I first modified the code that handled user mouse clicks to differentiate between a click on an open space (which would indicate the player wanted to move their unit to that space) and a click on a space which contained an enemy unit (which would indicate the player wanted to attack them).
I gave each character a custom CharacterStats component which would track all the relevant data they'd need for combat -- their remaining health points, how powerful their attacks were, how much defense they had, and so on. Whenever an attack was initiated, I'd play an attacking animation for the assailant and play an animation on the recipient to show them reacting to the damage dealt.
This is what it would look like if a knight attacks a goblin:
I'd also do a check to see if this blow reduced the victim's health points to 0 -- if so, I would set their state to dead, and play a death animation which would have them collapse to the ground.
So if our poor goblin friend is attacked and his health goes to 0, it looks like this:
All right! This is starting to really come together. I still need to work a little bit on some UI components to show information like how much health each person has remaining or which units belong on which team, but the fundamental mechanics are really starting to look good!
Bow and Arrow Line of Sight
One of the mechanics I wanted to implement as part of this game was a bow and arrow — back in the original Marker Game, we enabled certain characters to have range attacks. I thought it could be a good addition here to give an added layer of strategy to the game -- whereas the powerful melee units would need to get in close to attack which would expose themselves to damage, units with a ranged attack could stay farther away and protect themselves from harm.
I wanted to add a bit of realism to these ranged attacks -- I didn't just want this to be an attack that could hit an enemy unit anywhere on the board, because that would not really seem reasonable or fair. I wanted the enemy players to be able to use mountains or other obstacles as cover, so that a player would need to take that into consideration when positioning their range-attack troops.
The first obvious constraint I wanted to put on the archers was to restrict their range to a certain radius. For example, in the image below, the blue squares represent a 5-hexagon radius in which the bow-person could shoot.
Next, I wanted to introduce the idea of having things "block" our bowman's range of attack. However, it ended up being a bit more complicated than I expected to determine which squares could or could not be "seen" from the perspective of our bow-wielder. In some cases, it's obvious what behavior we'd like to see -- if there's a straight line, and something is in the way, then we can simply make anything behind that obstacle out-of-range.
Things get a bit more interesting, though, when you consider squares that might be visible by our bow-person -- hexagons not directly obstructed by obstacles. For example, in the image below, I show an example of which squares might be visible -- take particular note of the green squares. Should these be shootable by our bow-person? Maybe the leftmost green square should be obstructed, but the far right green square seems to be pretty clearly visible to our bow-person's line of sight. We'd need a way to figure out which of these hexagons would be visible to our archer.
To the human eye, some of these choices are easy to figure out for this particular example -- but in terms of figuring this out in the general case, it's a bit tricky. I tried to figure out if there was a nice easy pattern to follow in terms of what is or is not obstructed by an obstacle. The first thing to check would the the line-of-sight along direct lines:
We could also try to cast straight lines outward along some other axes -- they don't line up quite as nicely, but let's focus on the ones along straight lines for now. In the next image, the green squares represent the new outward lines, skipping any tiles that don't line up exactly.
So now if we put a mountain along one of our outward green paths, maybe we could say that the two adjacent squares behind it would be out of range (shown in white below).
But if our mountains start to be off-axis, it becomes harder to say with certainty if something should or should not count as "behind" a mountain. For example, in this case, we'll use a similar idea of blocking the adjacent squares behind the mountain -- but would the white squares be visible to our bow-person?
In this case, it seems reasonable that the white squares would be blocked by the mountain -- but if we move the mountain back a little bit, and we use the same methodology, it makes less sense. Again -- should these white squares be visible to our archer?
In this case, the right-most white hexagon looks like it should probably be visible to our bow-person. But we've basically used the same algorithm as before -- so it looks like this the idea doesn't really seem to work.
I tried to enumerate all of the edge cases I'd need to account for in this algorithm, but it seemed like the bigger the shooting radius, the harder it became to really accurately designate what should or should not be in range. I tried to brainstorm some alternative algorithms which were all a bit complicated, and I still wasn't really convinced they'd give the right results in all situations.
Ultimately, I figured that trying to do this in a grid-based algorithm wasn’t really going to be simple or accurate enough. Thankfully, after spending a lot of time spinning my wheels on this, I realized that there was a much simpler approach that I should have thought of earlier – but having solved all these other path-finding tasks based on the grid, my mind was focused on those grid-based algorithms and solutions.
Instead of using the grid layout, I realized that I could just put some kind of collider on each of the mountain tiles, and actually do a raycast from the center of the archer's hexagon and cast it to all of the hexagons which would be in their shooting radius. If the raycast hits an obstacle along the way to a particular hexagon, then that hexagon would be out of range. If, instead, the raycast hit an enemy unit -- and that unit was within the bowman's range -- then they were a valid target.
So if we look at our earlier example from a top-down view:
Let's take a look at some example raycasts, with each ray represented by a black line. If the black line hits a mountain before it reaches a hexagon, then that means it will block the arrow -- but if it extends unobstructed to a target location, that means that location is a valid target for our archer.
As we can see from this example, the upper-most green space will be available to our archer, whereas the other two will not. This is definitely much closer to behavior that makes sense, and it doesn't require an advanced degree in topology to figure out what should or should not be considered a valid target.
The nice thing about this approach is that it's pretty flexible in terms of how big you want the colliders to be. Maybe if you look at this image, you might think "That green space closest to the archer SHOULD be considered visible, even though right now it's not! The raycast barely touches the mountain! That doesn't seem realistic " -- well, you could simply make the mountain colliders a bit smaller, which would allow that particular ray to pass by unobstructed, and that closest green space would now be visible. It's a flexible solution!
All right! So that's the update on The Marker Game. I'm still working on a few more UI elements and polishing things up a bit, but the game should be ready to play by late tonight.
Thanks for reading! I hope some of this was informative or helpful. I'll be back tomorrow with my week 3 recap article and the kickoff for week 4!