An awful lot of work for a joke
This is a demake of Dota 2 which was meant meant to be a quick weekend thing but turned into a multi-month project. The premise is a bit of a joke at the expense of one of my gaming friends. He bought a Divine Rapier in the game and immediately lost it. I decided to recreate that moment in a game and let my teammates see if they could do better.
Here’s what the game looks like:
Building the Game
The core concept is that the player plays as a single hero (Kunkka) and tries to survive as long as possible while being chased by a team of 5 enemy heroes.
I wanted the player to be able to use all of Kunkka’s active spells, juke around trees, heal at the fountain, and generally feel like they are playing a little mini version of Dota. The engine I had been building up from previous game jams was getting more and more capable with every project, but I needed to do a lot of work to realize my vision for this game.
I started with a an image of the Dota minimap and added a little Kunkka icon that the player could move around by right-clicking.
Next, I added a graph of connected waypoints marking out all of the walkable parts of the map. Typing out the coordinates and links for all of the waypoints in code would have been crazy so I took some time to build some rudimentary editing tools into the engine. It’s not pretty but you can add, move, and delete waypoints to the graph with the mouse and keyboard and then click to define connections between them. The system can serialize the graph to a lua file for easy runtime loading.
The player can then right click a spot on the map and the A* library will generate a series of waypoints to follow to get there. Having the points be hand edited made it easy to tweak the paths around hard to define areas like juke spots in the trees.
Iterating the navigation system
The navigation graph works well enough once the entity is actually following a set of waypoints but the start end end positions will rarely ever fall directly on a graph node. The system needs a strategy for moving an entity onto and off of the graph.
The simplest strategy for getting an entity on the graph is to find the closest waypoint to the desired position and move the entity there in a straight line. This works great as long as the nearest waypoint is relatively close to the starting position and roughly in the direction of the goal.
However, this immediately gives unnatural results in cases where the nearest waypoint is in a different direction. When the player clicks, the hero goes the wrong way, despite there being a clear path towards the goal!
To correct this, the system takes the set of waypoints in sight of Kunkka. Line of sight is calculated by raycasting against wall polygons in a HardonCollider-based physics system. It then chooses the waypoint with the smallest dot product compared to the desired destination vector. This causes Kunkka to always move towards the goal where possible and generally matches what the player expects when they issue a move command.
AI, goal planning, memory
Once I had a map that could respond to navigation and line of sight queries, it was then possible
to implement an enemy bot on top of that. The core of the AI is a finite state machine where each
state is a class that implements the
update() behavior for an entity in that state.
The simplest AI implementation has two states. One state moves towards the player, the other attacks the player continually when in range. This behavior is, unsurprisingly, not very fun and doesn’t feel like Dota.
I needed to make the enemy behavior more interesting. As I learned from a previous game, showing the AI’s state clearly and allowing the player to trick the AI greatly increases the fun factor.
Each bot begins by following a waypoint-based patrol route. When a bot gains line of sight on the player it will immediately begin to chase them. After a short delay, the bot will broadcast the player’s position to the other bots and they will move off of their patrol routes towards the player. As long as the player is in sight, the bots will give chase. However, the player can break line of sight by juking around trees, causing the bots to become confused and return to their patrol route.
A big part of the game is being able to cast Kunkka’s spells and see them faithfully translated to 2D. I built a general purpose SpellManager system which handles input, UI, and logic for Kunkka’s three active abilities.
SpellManager class sits between the
Spell classes. It listens for
events generated by the Input class. The spell manager has a reference to each available Spell and knows which keys are bound to which.
You can see code to handle each individual spell here. It was really fun breaking down the various elements of the abilities in Dota and mapping them to concepts in the spell system while thinking about how other (non-Kunkka) abilities would be built. I gained a lot of respect for the task of implementing and balancing the real game.
Basic attacks are represented with a simple swipe animation and sound effect. A timer controls the duration between attacks and are tuned so that the player can generally win any 1v1 (they are carrying a Divine Rapier after all).
All of these elements combine to create a rather compelling gameplay loop. The player, unseen, moves around the map looking for an opportunity to attack a patrolling enemy alone. When spotted, the player will quickly be outnumbered so they can use spells, attacks, and movement to get kills and escape.
The UI is implemented as a separate spritesheet layer on top of the gameplay and effects. It is designed to look like a passable Dota HUD with as little code as possible.
UI elements watch for clicks within their boundaries and dispatch relevant events to a gamewide message system.
Building this demake ended up being far more involved than I expected. I have a new appreciation for the challenge in building and balancing a game like Dota 2. There are so many parts of the full game that aren’t represented here, like:
- Fog of war
- Allied teammates
- Passive skills
- Shop and Items
- Damage types / armor / immunity
- Flaming your teammates
- You get the idea…
- Lua / LÖVE
- Squad-based AI
- A* pathfinding