Episode 11 of a series about building an MMO from scratch, with the focus on the engineering around the game. We fight the monsters from episode ten. Each player holds Quaternius's sword, attached to the hand's bone in the client, with a slash and a death from Mixamo. The space bar swings it, and the server decides what it hits, for how much, and who gets the experience. The monsters spot players near their home, run at them and swing back, and the dead play their death animation and lie there before they respawn.
Topics: Gameplay
The code
Each step on screen is a tag in the game's repository. The code at the end of the episode is ep011/end.
Transcript
Intro
0:00 Hello and welcome back to episode eleven, where our players get a sword to fight the monsters from last time, and the monsters fight back. Let's begin with a quick look at where we're going to be by the end of the episode.
0:13 Our players carry a sword now, and three skeletons wander around the board. I walk towards one of them, and it spots me, and runs at me. I hold the space bar, and we fight, and each of its hits takes a bit of my health, in the top left. A few swings later, it falls apart, and its experience goes into my bar.
0:37 On the website of the series you can find more information about the project. The code for the project is open source on GitHub, tagged at each step of each episode so that you can follow along.
Last time
0:49 Now, before we get to today's work, let's quickly go over what we did last time. Our first monster came from Poly Pizza: Quaternius's skeleton, in the public domain, so it's in our repo as it was downloaded, and the client loads it as it is. The server owns the monsters, and each tick, it walks them around their homes. Players and monsters got health and levels, and each player's experience goes to their own client alone. And the client draws health bars over everyone, and a frame with your own health and experience.
Today
1:19 Today, it's time to fight the monsters from last time. Our players get a sword, from Quaternius too, in their right hand, which the client attaches to the hand's bone. The characters get two more animations from Mixamo, a sword slash and a death. The space bar swings the sword, and the server decides what it hits, for how much, and who gets the experience when a monster dies. And the monsters fight back, going for the players near their home, so our players can die too, with a death animation for both, before they come back. All right, let's get started with where the sword comes from.
A sword
1:54 Before our players can fight, they need a sword, which comes from Poly Pizza too, by Quaternius, so it's in the style of our skeleton. It's public domain, CC0, so it goes into our repo as it was downloaded, like the skeleton. Download gives us a g l b file again, with no bones and no animations, as a sword doesn't bend, and it moves with the hand that holds it.
2:17 In its file, the sword stands on its pommel, with the blade up, and it's about two and a half metres long. So first, we move it down, so that the middle of its grip is at zero, and scale it to a metre. Then it goes into the right hand, at the palm. Each frame, the sword goes where the hand is in that frame's pose, and from there to where the player stands, like the character does.
2:41 The sword goes into a new folder, assets slash weapons, as it was downloaded. And a README next to it says what it is, who made it, where it's from and its licence, like the skeleton's. In the client's CMakeLists, the assets target copies assets slash weapons next to the client too, into a weapons folder. In the characters header, we include optional, which we saw in episode three, for a value that may or may not be there. A Characters may hold no weapon, say if the file is missing, and an optional says that in its type, without a pointer, or a flag next to the model. Weapons folder is the folder next to the client with the weapons in it. Hold puts a weapon in each character's right hand: the model in a file, scaled to a length in metres, and drawn with a shader. It's a call of its own, and not part of the constructor, as the skeletons hold nothing for now. Each Character gets hand, where its right hand is in its list of bones, or minus one if it has none. And the class keeps the weapon, as an optional Model, and grip, the matrix that takes the weapon from its own space into the hand's. We work it out once, when the weapon is loaded, so that each frame has less to do. In the source file, k right hand is Mixamo's name for the right hand's bone, mixamorig colon RightHand, which finds the hand in X Bot and in Y Bot, as they share Mixamo's skeleton. K grip says where a hand holds a weapon, as a share of its height from the bottom, so that it holds whatever length we scale the sword to. Its comment says where the sixth comes from: the middle of the sword's grip is a sixth of the way up from the pommel. K palm is how far the palm is from the wrist, along the fingers, eight centimetres, as the hand's bone sits at the wrist. Weapons folder works like monsters folder, and in the constructor, once the shader is set, we look through the model's bones for the right hand, by name, and keep its index. The skeleton has no bone by that name, so its hand stays at minus one, and it holds nothing. The destructor unloads the weapon, if there is one, as it's ours to free, like the characters' models. Then hold, which takes the path, the length and the shader. If the file isn't there, it returns, and the hands stay empty, like a missing character leaves us with cubes. Otherwise, it loads the model, and then we fix its colours, going through the sword's materials one by one. Over the loop, the comment reads: glTF colours are linear, and our shader writes them out as they are, so we turn them into sRGB, or the sword comes out nearly black. Linear means in proportion to the light, so twice the number is twice the light. A screen takes sRGB, which gives more of its steps to the dark shades. So a linear colour shown as it is looks a lot darker, which is what I got the first time I loaded it. So in the loop, each material gets the shader, and we take a reference to its diffuse colour, which is the base colour of its surface, so that we can change it in place. Color normalize turns it into four floats from zero to one, which is what the maths needs, as a colour keeps each channel in a byte. We raise the red, the green and the blue to the power of one over two point two, which is close to the sRGB curve, and keep the alpha. And color from normalized turns them back into a colour. Then the sizing, where get model bounding box gives the box around the sword, and the scale is the length we want over its height. Sized moves the sword down by its bottom, plus a sixth of its height, which puts the middle of the grip at zero, and then scales it. Matrix multiply, in raylib, applies the first matrix, then the second, so we move it first, and scale the moved sword. In hand then moves the sword along the bone, by k palm, from the wrist to the palm. Grip is the sizing, then the hand, and then we keep the weapon in the class. In draw, once the bones are posed and the character is drawn, comes the weapon, and if there's none, or the character has no hand, we're done. Hand is the hand's bone in the current pose, the one we've just set, in the model's own space: a scale, a rotation, and a translation. The rotation is a quaternion, four numbers that describe a turn, which is how glTF stores them, as they blend well between frames. Bone builds a matrix out of them, which is what a model's transform takes, with matrix scale, quaternion to matrix and matrix translate. It multiplies the three in that order, so the scale and the turn happen around the bone's origin, and the translation comes last, without getting scaled or turned. Placed is what draw model E X does for the character: its scale, its yaw around y, then its position on the board. Then we take a copy of the weapon's model, to set its transform. A Model is a small struct of pointers, so the copy is cheap, and it shares the mesh. Its transform is the grip, then the bone, then placed, and draw model draws it there. And in the renderer's constructor, the characters hold the sword from the weapons folder, a metre long, with the cube's shader. The comment gives the reason: the sword has no bones, so it's drawn with the cube's shader, and not the characters' skinned one.
7:45 Let's build it and log in, to see how the sword sits. Our player has a sword in its right hand, but not the way you'd hold one. The blade sticks straight out of the fingertips, like a very long finger, and as the player walks, it swings back and forth with the arm. We moved the sword into the hand's space, to the palm, and didn't turn it, so it points wherever the hand's axes point.
8:07 Each bone has axes of its own, and in Mixamo's right hand, y points along the fingers. The sword stands along its own y, so it came out of the fingertips. I looked at where the fingers start, to find the other two. X runs along the knuckles, from the little finger to the index finger, and the palm faces along z. A fist holds a sword along the knuckles, with the blade out past the index finger, which is a quarter turn around z. The grip runs across the palm at a slant, so the blade leans twenty degrees towards the fingers. Mixamo's idle, walk and run weren't made with a sword in mind, and they turn the hands in. So with that turn alone, the blade would point across the legs, and we turn it forty degrees out, towards the back of the hand.
8:54 So, back in hold, in hand moves the sword to the palm, and so far nothing turns it. Along turns the sword around z, by a quarter turn, less twenty degrees. Over it goes a comment on Mixamo's hand: y along the fingers, x along the knuckles, palm facing z. The fist holds the blade along x, leaning twenty degrees towards the fingers. Then out turns it forty degrees around y, which runs along the fingers. Turning around that axis keeps the grip in the fist, and swings the blade towards the back of the hand. And it has a comment of its own: Mixamo's clips turn the hands in, so we turn the blade forty degrees out, clear of the legs. In hand puts the two turns together, and then moves the sword to the palm, as before. The turns come first, so the sword rotates around its grip, while it's still at zero, and the move to the palm comes last.
9:47 Let's log in again, and now our player holds the sword in its fist. As it walks, the sword swings with its arm, pointing forward, clear of the legs. And as it runs, the fists come up, and the blade with them.
Fighting
10:01 Fighting starts with the space bar, which a player holds down to attack. Like the movement keys, the client says when it's held down, in Input, and the server decides the rest. A player that attacks swings at the nearest living monster within one point six metres, and turns to face it. So you don't have to line up with a monster under a fixed camera. With no monster in reach, it swings at the air. A swing takes two seconds, which is how long the slash animation is, and the player stands still until it's over. Holding the space bar swings again and again, a bit like the auto attack in a lot of MMOs. The sword hits once, nought point nine seconds in, as it comes across the front in the animation. If the monster is still alive and in reach, it loses fifteen health, and five more for each level the player has after the first. A monster with no health left dies, and plays its death animation. Whoever killed it gets its experience, fifty for a skeleton, and going up a level fills your health. And ten seconds later, the monster is back where it spawned, at full health.
11:06 For the players' slash and death, we go back to Mixamo, and download two more animations, with the settings we used in episode seven. The README lists them, so that anyone who builds the characters downloads the ones we use. Stable Sword Outward Slash swings a sword out across the front, and Dying Backwards falls on its back. Build dot s h converts them to glTF, as attack and death, and merges them into both characters, after the other three clips. And the order doesn't matter, as the client finds the clips by their names. In merge dot m j s, in place is a set with the clips we keep in place, the walk and the run. Until now, the hips' travel came out of all the clips, as the walk and the run carry the body forward, and our server does the moving. Dying Backwards falls about a metre back, and taking that out would make the body slide forward as it falls. So the condition that finds the hips' translation also checks whether the clip is in the set, and the travel comes out of the walk and the run, and nowhere else. And the comment over in place gets a line: the other clips keep theirs, a player falls back as it dies.
12:14 Then we run the characters stage again, with the new files in assets slash mixamo. Merge prints the walk's and the run's speeds, as before, and X Bot and Y Bot each have five clips now: idle, walk, run, attack and death.
12:29 The protocol comes first, where Input gets attack, a bool, field four, which is whether the player holds the attack key down. Motion gets attack, three, which is a swing, and dead, four, which is dying, then lying dead, as the client plays a clip for each motion. In the world's header, Motion gets attack and dead too, in the protocol's order. A player gets target, the id of the monster it's swinging at, or zero for none, as monster ids start at one. And swing, how far into its swing it is, as a duration, and input attack, whether its client holds the attack key down. A monster gets experience, which goes to the player that kills it, and respawn, how long until it's back once it's dead. Then the world's numbers, as constants, so that the tests can use them by name. K reach is how far a sword reaches, one point six metres, from the player's middle to the monster's. That's an arm and a metre of sword, and a bit more, so that a swing isn't fussy about distance. K swing time is two seconds, which its comment says is as long as the slash animation. If a swing were shorter than its clip, the slash would get cut off and start again. K hit time is nought point nine seconds, and k respawn time is ten seconds, how long the dead lie there. Set input takes attack too, with a default of false, so the tests that walk players around stay as they are. Then four private functions, for a player's turn in a tick: act decides what it does, and move walks it. Swing starts a swing, and hit lands one, once the swing gets to its hit time. Step was one loop that moved the players, and it's going to grow, so splitting it up keeps each part short enough to follow. In levels dot h, damage is how much health a player's sword takes off with a hit, at a level. It's fifteen at level one, and five more for each level after it. So a skeleton's fifty health takes four hits at level one, and three at level two. In monsters dot h, revive moves a dead monster on by some time, like wander does for a living one. In monsters dot c c, the skeleton's initialiser goes one field to a line, as it no longer fits on one, and the new field is experience. A skeleton gives fifty, and level two takes a hundred, so two skeletons take a new player to level two. Then revive, which takes the monster, the time and the generator, like wander does. It takes the time off the respawn, and while there's some left, it returns, and the monster lies there. Once the respawn runs out, the monster goes home, with its max health, idle, and with a new random rest, so that it doesn't wander off the moment it's back. In world dot c c, distance, in the unnamed namespace, is how far apart a player and a monster are, with hypot. We need it when a swing picks a monster, and again when the hit lands, so it gets a name. Set input takes attack, and keeps it next to run. Step changes the most, as each player now gets its turn from act, and the movement code that was in its loop moves out, into a function of its own. The monsters' loop gets a check, so a dead monster revives, and a living one wanders, as before. Being dead is a motion, like walking, so the clients play its clip, and there's no flag of its own to keep in step with it. Act is a player's turn, and it's where the rules of a swing live. If it's swinging, the swing goes on by the tick's time. Before is how far into the swing it was at the start of the tick. If the swing crossed the hit time during this tick, the sword hits. We check it as a crossing, from before to after, as a tick doesn't have to end on nought point nine seconds, so the sword hits once, whatever the tick's length. Until the swing time is over, the player stands still, and that's its turn. Then it's idle again, and if the attack key is still down, it swings again, and otherwise, it moves. Move is the code that was in step, moved as it was: the tick in seconds, the speed, walking or running, and the new x and the new z, clamped to the board's edge. D x is the distance it moved along x, after the clamp. Its comment moves with it: motion comes from how far it actually went, so that a player pushing against the edge stands still, instead of walking on the spot. Then comes d z, and if the player didn't move along either, it's idle. Otherwise, it walks or runs, and its yaw is a tan two of how far it went, and the player takes its new position. Swing starts a swing: the motion is attack, and the swing starts from zero. Then it picks a target, and nearest starts at the reach, so that anything further away doesn't count. It goes through the monsters, and a monster that isn't dead, and is no further away than the nearest so far, becomes the target, and its distance is the new nearest. If there's a target, we look the monster up, and the player turns to face it, with a tan two of the way to it, like when it walks. Then hit, at the hit time, finds the target, and if it's gone, it misses. If it's dead, or it got out of reach since the swing started, it misses too. Otherwise, we work out the player's level, and its damage comes off the monster's health. Max keeps the health from going below zero, so that the bar doesn't show a negative number. If there's health left, that's the hit, and if not, the monster is dead, for k respawn time, and the player gets its experience. Now is the player's level with that experience added, worked out by level for. If it's higher than the level it had before the kill, the player went up a level, and its health is the new level's max health. It's a small reward for the level, and it saves your health bar from starting the new level partly empty. And in the server, to protocol turns attack and dead into the protocol's motions, and set input passes the input's attack on to the world.
18:20 The world gets two tests, starting with swings at the nearest monster in reach, which puts a player in the middle. It spawns a skeleton a metre to its right, which the test calls near, and another one, called far, a metre and a half away, along z. Both are in reach, so the test is about which one is nearer. The player attacks, and after a tick, its motion is attack, and it has turned a quarter turn, to face the nearer one, to its right. Then we run a second, which takes the swing past the hit time. The nearer skeleton has lost fifteen health, and the other one still has its fifty, so the swing picked the right monster, and the hit landed once. Kills monsters for their experience, and they come back, is a whole fight, from start to end. The player joins with sixty experience, forty short of level two, and a weak skeleton has a max health of ten, so that one hit kills it. The player attacks, and after a second, the skeleton is dead. That gives the player a hundred and ten experience, which is level two, and level two's hundred and twenty health. Then the player lets go of the key, so that it doesn't swing at the skeleton once it's back, and we run k respawn time, a second at a time. And the skeleton isn't dead any more, and it's back at its full ten health.
19:39 On to the client, where attacking says whether the player wants to attack, next to running. It's the space bar, held down, with is key down. We send whether it's held, and not each press, so that holding it swings again and again, and the server decides when each swing starts. In main, attack goes next to run, in the input we send to the server. The input goes to the server when it changes, so attack joins the check against what we sent last. And set attack puts it in the message, so holding the key down costs one message when you press it, and one when you let go, however long you hold it. In the characters header, each character gets an attack and a death, so clip for and speed have five places, one for each motion, in the protocol's order. In the source file, k clips has five words: idle, walk, run, attac, and death. That's attac, a t t a c, without the k. The comment over it says why: in the protocol's order of motions, with attac for the skeleton's attack, whose name raylib cuts at thirty-one characters. And attac still finds the attack in X Bot and Y Bot, as it's inside the word. Frame at gets a new argument, loops, which says whether the clip starts again at its end. Its comment changes too: raylib samples glTF clips sixty times a second, and the last frame is the first again, so a loop skips it, and the death stops just before it. So the death, which doesn't loop, stays on that frame with min, and without it, a dead character would get back up, and fall again, over and over. The structured binding names the attack and the death too, and a character without all five clips is left out, as before. Index takes motions up to the size of k clips now. S size gives a size as a signed number, so that the compiler doesn't warn about comparing a signed motion with an unsigned size. And in draw, frame at gets whether each clip loops, which is when it isn't the death, for the clip we blend from and the clip we blend to. In the animator, back is when we had a figure as dead, and the server says it isn't any more. Without it, a monster that respawns would slide from where it died to its home, as the figure catches up with the server. And it would blend out of its death on the way. So a figure that's back starts afresh, like a new one, standing where the server says, with the unit's yaw, and in its motion.
22:06 Once it's built, we run the world's tests, and all ten pass, with the two new ones among them.
Fighting back
22:13 Now the monsters fight back, and each tick, a monster that isn't swinging looks for the nearest living player within three metres of it. That player also has to be within four metres of the monster's home. This is called a leash, and a lot of MMOs have one, so that a monster doesn't follow you across the whole map. Get far enough from its home, or out of its sight, and it gives up, and wanders back. It runs at the player, at the players' running speed, three point one metres a second. Once it's within one point two metres, it stops and swings. The skeleton's swing takes nought point nine seconds, the length of its attack animation, and hits for five as it lunges, nought point six five seconds in. Like a player's swing, it misses if the player has got out of reach since. A player with no health left dies, plays its death animation, and lies there for ten seconds. Then it's back in the middle of the board, where new players start, at full health.
23:10 In the world's header, a player gets respawn, how long until it's back once it's dead, like a monster has. A monster gets damage, how much health its hits take, and swing time, how long its swing takes. And hit time is how far into the swing it hits. The players all swing one kind of sword, so their numbers are constants in the World, while each kind of monster swings its own way, so a monster carries its own. Then comes prey, which is the player the monster is going for. It's called prey, and not target, as target x and target z are already where it's walking to, and the two would be easy to mix up. And swing, how far into its swing it is, like a player's. Then two more private functions, for a monster: act, for its turn, and hit, for when its swing lands. They overload the player's, so step calls act for players and monsters alike, and the compiler picks one by the type it's given. In monsters dot h, we include cstdint, for the ids, and map, for the players, which a monster now looks through. K sight is how far a monster sees, three metres, and k leash is how far from the monster's home a player can be for it to give chase, four metres. The leash gets a comment: each skeleton's home is further than this from the middle of the board, so players are safe where they start and come back to life. In my first try, it was six, and a skeleton killed my player again as soon as it was back. K monster reach is how close a monster gets before it swings, one point two metres. Spot picks the player a monster goes for, if any, from the players by id. It's a function of its own, so that the tests can check who gets picked, without running the rest. Hunt is a living monster's turn, once any swing is over: it runs at the player it spots, swings once it's in reach, and with nobody about, it wanders. In monsters dot c c, we include map too, as spot and hunt go through the players. The skeleton gets its damage, five, its swing time, nought point nine seconds, and its hit time, nought point six five. Five a hit is twenty hits to kill a new player, so you have time to fight back, or run. Spot starts with nobody, and closest at the sight, like a player's swing starts at its reach, and then it goes through the players. Away is how far a player is from the monster, and from home is how far it is from the monster's home. A living player within sight, and within the leash of home, nearer than any so far, becomes the nearest. So the leash is about where the player is, not the monster, and a monster follows you to the edge of its patch. Hunt starts by spotting, and keeps whoever it spotted as its prey, or zero for nobody. With nobody spotted, a monster that was running after someone stops there, goes idle, and rests for a random time. So it doesn't turn round and walk off the moment you leave. Then it wanders, as before, which takes it back towards its home. With a player spotted, d x and d z are the way to it, and distance is how far. The monster turns to face it, and in reach, it starts a swing, from zero. Otherwise, it runs, as far as it gets this tick at the running speed. Min stops it at its reach, so that it doesn't run into the player and stand inside them. Then it moves that far, along d x and d z over the distance, like a wander does. And its motion is run, so that the clients play the run. In world dot c c, step hands each monster to act, like the players. A player's act starts with the dead, whose respawn goes down by the tick's time. Once it runs out, the player is back in the middle of the board, idle, with the max health for its level. Either way, a dead player does nothing else in the tick. Then a monster's act mirrors a player's, so a dead one revives, and that's its turn. If it's swinging, the swing goes on by the tick's time, and it hits once, as it crosses the monster's hit time. Until its swing time is over, it stands there, and then it's idle again, and it hunts. Hit, for a monster, is the player's hit the other way round. It finds its prey among the players, and misses if the player has left the game, died, or got out of reach since the swing started. Otherwise, the player loses the monster's damage, with max, so that health stays at zero or above. And with no health left, it's dead, for k respawn time. On its next turn, the skeleton finds nobody alive to spot, so it goes back to wandering. And in the client, the skeleton's run plays slower too, now that skeletons run after players. Its feet go at about four and a half metres a second in its run, and nought point seven of that is about three point one, our running speed. So under the walk's speed, the run's speed is nought point seven. And the comment above adds that the skeleton's feet keep up with our run at nought point seven.
28:05 In the monsters' tests, we include cstdint and map, for the players. Runs at players it sees, then swings, puts a skeleton in the middle, and a player two and a half metres away, which is within sight. One turn in, its prey is player one, and it's running. Then ten more ticks run, half a second, and at the running speed, that's further than the one point three metres it has to go. So it has stopped at its reach, at two and a half less one point two, and not on top of the player, and it's swinging. Spots living players in sight and near home is about which player a monster picks. The skeleton's home is three metres to the left of the middle. A player at nought point five, three and a half metres away, is out of sight. A dead one, two metres away, is in sight but doesn't count. With the skeleton moved to the middle, a player at one and a half is in sight, but four and a half metres from home, past the leash. And of two players in sight, it picks the nearer. The world's new test is a whole fight, which the player loses. A player stands at x two, and a skeleton at x three, a metre away, and the player does nothing. A hundred health at five a hit is twenty hits, of nought point nine seconds each, and the twentieth lands just before eighteen seconds. So after eighteen seconds, the player has no health, and it's dead. Then we run k respawn time, ten more seconds, to bring the player back. The loop counts up to k respawn time's count, one second at a time, like the test where the monsters come back. The player is back in the middle, at x zero, with its hundred health, as it's still level one. So dying costs you ten seconds and your place on the board, and you keep your experience.
29:53 Once it's built again, we run the world's tests and the monsters', and all fifteen pass, with the three new ones among them.
30:00 Let's put it all together now, and fight a few skeletons. The servers are at the bottom, as before, and the client logs in. I walk towards the skeleton at the bottom right, and once I'm within three metres, it spots me, and runs at me. I hold the space bar down, and my player turns to the skeleton and swings, again and again, two seconds a swing. The skeleton swings back, quicker than me, and each of its hits takes five off the health in my frame. Its own health bar goes down by fifteen with each of my hits. On the fourth, it falls apart, and its fifty experience goes into my bar. Now I walk up to the skeleton at the top right, and this time, I let it hit me. My health goes down, five at a time, with each of its swings. With none left, my player dies, and falls on its back. The skeleton leaves it alone, and wanders back towards its home. Ten seconds later, I'm back on my feet in the middle of the board, with all my health.
Docs
31:08 Last, we update the README for the sword and the fighting. Space swings the sword now, and the characters section lists the slash and the death among the clips to download from Mixamo. So if you made your characters before today, download those two, and run the characters build again. The sword gets its source and licence, and a new section on fighting has the reach, the damage and the timings, next to the monsters' numbers.
Wrap-up
31:33 So, today our players got a sword, from Quaternius too, which the client puts where the hand's bone is, each frame, with two more clips from Mixamo for the slash and the death. Space swings it, and the server decides what it hits, for how much, and who gets the experience. And the monsters fight back, going for the players near their home. The dead play their death animation, and come back ten seconds later.
Playing it
33:04 Let me know in the comments what you'd like to see next, and if you want to follow along, subscribe. I hope you find this useful, and thanks for watching.