Episode 15 of a series about building an MMO from scratch, with the focus on the engineering around the game. Our board of tiles becomes an island, 512 metres across, from value noise and a seed, in a library that the server and the client both build, so only the seed goes over the network. Both have to compute the island to the last bit, and by default they don't, as compilers fuse multiplies and adds on some machines and not others, so we turn that off and pin a fingerprint of the island in a test. The client draws it as flat-shaded low-poly triangles, with a sea, fog and a camera that follows you, and the server lets you wade into the sea and slide along the coast, with camps of skeletons the seed puts around the island.
Topics: Graphics
The code
Each step on screen is a tag in the game's repository. The code at the end of the episode is ep015/end.
Transcript
Intro
0:00 Hello and welcome back to episode fifteen, where we're going to give our players a world to walk around in, instead of a board of tiles. Let's begin with a quick look at where we're going to be by the end of the episode.
0:14 We're running across an island, which the server and the client both make from a seed number, with hills, beaches and a sea around it. The camera follows me, and turns around me when I ask it to, while the far hills fade into the fog.
0:30 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:42 Now, before we get to today's work, let's quickly go over what we did last time. We watched our login go over the network with tcpdump, and the password was there, as I typed it. So we made a certificate authority of our own, which signs the certificate our servers present. We brought in OpenSSL, through Asio's SSL stream, and put TLS 1.3 on each connection we make over TCP, from the client and the bots to both servers. A certificate from another authority, or with another name on it, gets refused, which a test and openssl's own client showed. And the world over UDP stays plain, as it holds what each player sees in the game anyway.
Today
1:23 Today, we're going to make an island out of noise, from a seed, in a library that the server and the client both build. For that to work, both have to compute it to the last bit, and we're going to find out why that isn't the case by default, and fix it. Then the client draws it, as low-poly triangles, which is the style I talked about in episode one. The island is a lot bigger than the screen, so we need a camera that follows the player. And the server has to know the island too, so that nobody walks into the sea, and so that it can put camps of skeletons around it. All right, let's get started with where the island comes from.
From a seed
1:59 Since episode two, our players have walked a board of twelve by twelve tiles, and a world needs more room than that. One way to get a world is to make it by hand, in an editor, and ship it as a file that the client draws and the server loads. That's what a lot of games do, with artists and an editor, and the file grows with the world, which each client has to download. The other way is to compute it, from a number we call a seed, which is the procedural generation I mentioned in episode one. The code that turns the seed into the island goes in a library of its own. The server builds it, to know where the ground is and what's in the way, and the client builds it, to draw it. So all that has to go over the network is the seed, which is four bytes, and a different seed is a different island.
Noise
2:48 To turn a seed into hills, we're going to use value noise, which is simpler than the better-known Perlin noise, and I'll show it along a line first. At each whole number i, we pick a random value between minus one and one. It doesn't come from a random generator, but from a hash of the seed and i, which mixes their bits into a number that looks random. So whoever computes it gets that value again, on any machine, in any order. Between two whole numbers, we blend from one value to the next. A straight line between them would leave a sharp corner at each whole number. So the share of the next value goes through this curve, called smoothstep, which starts and ends flat, so the slopes meet without a corner. Put together, that's one layer of noise, and on its own, it gives gentle hills, all about one size. So we add layers of it, which are called octaves. Each one is twice as fine, and half as strong, as the one before. So the big shapes come from the first layer, and the bumps on them from the later ones. In two dimensions, it works alike, with four values around each point, blended across and then down.
3:58 The new library goes in a folder of its own, called terrain, next to protocol and server. It starts with noise dot h, with its include guard, and cstdint, for the fixed-size integers. Hash goes in the header, and not in the source file, as the ground's colours are going to roll their dice with it too. The comment over it reads: a number for each point of a grid, which looks random, but is the one you get for that seed and point, on any machine. Noise is the function from the formula, in two dimensions: value noise, from minus one to one, random at whole numbers, and smooth between them. And fractal adds the octaves, as many as we ask for, as the hills and the mountains each want a different number of them. Above it, we write what each one is: twice as fine and half as strong as the one before, still from minus one to one. None of the three keeps any state, so it doesn't matter who calls them first. In noise dot c c, after our header, cmath, for floor, and cstdint, and then three small helpers in an unnamed namespace. Value turns a point's hash into a float between minus one and one. We shift the hash right by eight, and the comment says why: the top twenty-four bits, which is as many as a float holds without rounding. Smooth is the smoothstep curve from the formula, and mix blends from a to b by t, which goes from zero to one. The standard library has std lerp for this, but the standard says what std lerp guarantees, and not how each library computes it. So we write our own, and know which operations it does, in which order, as the server and the client have to agree on the result. Then hash multiplies x and z by two large odd constants, so that neighbouring points end up far apart, and XORs them with the seed. On their own, those bits aren't mixed enough, as nearby points would still share a lot of them. So we finish with the last step of MurmurHash3, a well-known hash. We don't use std hash, as each standard library has its own, and some hand integers back as they are. Over it goes its comment: it mixes each bit into all the others. The hash is all unsigned integer arithmetic, which C++ defines to wrap around, so each compiler gives us these bits. Noise takes the floor of x and z for the corner of the grid square the point is in. We use floor, and not a cast to an integer, as a cast rounds towards zero. Wherever x or z is negative on the island, that would pick the wrong corner. And across and down are how far into the square the point is, through smooth. Then, like in the formula, we blend across the top corners and the bottom ones, in upper and lower. Blending down between those two gives the noise at our point. Fractal adds up the octaves in a loop, into a sum, and keeps a total of their strengths. It starts from a strength and a scale of one, for the first octave. Each octave takes the noise with a seed of its own, seed plus octave, so that each layer has hills of its own, instead of the first layer's, shrunk. Then the strength halves and the scale doubles for the next one. Dividing the sum by the total of the strengths keeps the result between minus one and one, however many octaves we ask for.
7:13 The island's height at a point comes from three layers of that. The hills are five octaves of noise, scaled to plus or minus seven metres, on top of three. So most of the land is above the sea, which is at zero. The mountains are four more octaves, with a seed of their own, and larger, where we keep the part above zero and square it. Squaring leaves the small values close to zero and makes the large ones taller, so the mountains are few and steep, up to around twenty metres. To make it an island, c goes from zero to one as we move out from the middle, starting at a little over half the way to the edge. So towards the edge, the height fades into minus twelve metres, which is the seabed. And m is like c, but around the middle, where the players start. Within twelve metres of it, the slopes are cut to fifteen percent of what they'd be, around two metres above the sea. So the players start on a gentle meadow, and not in a lake.
8:08 Next to it goes the terrain, which is what the rest of the game is going to use. In terrain dot h, after its include guard, cstdint, for the seed's type, and vector, for the corners. It has one class, terrain, and its comment says why it's a library: the ground everyone walks on. The server and the clients each make it from one seed, so the seed is all that goes over the network. K cell is the size of a cell of the grid, two metres, and each cell is two triangles. Two metres is big enough that you can see each triangle, which is the low-poly look, and small enough for the slopes to look right. K sea level is zero, with land above it and the sea below. The constructor with no arguments makes the board we had, flat and twelve metres across, so that our server's tests keep walking players around on it. The other, explicit like before, makes an island from a seed, five hundred and twelve metres across, with the sea all around it. Height gives the height at any point, in metres, on the triangles between the grid's corners, as the client draws them. The noise between the corners has bumps that the triangles don't, and a player should stand on what we draw. Height is const, like the getters further down, as none of them changes the terrain. Corner is the height at a corner of the grid, from zero to cells on both axes. It's defined in the header, so the compiler can inline it, as the client calls it four times for each cell, about a quarter of a million times, when it builds the ground. Then getters for the seed, for the cells, which is how many there are along each side, and for half the size. And half the size is the distance from the middle to each side of the grid. The terrain keeps the seed, the cells, and the corners' heights in a vector, row after row. And the client builds its triangles from that vector, and the server reads the heights from it. In terrain dot c c, algorithm, for clamp and max, cmath and cstdint, and our noise dot h. In an unnamed namespace, the board is six cells along each side, and the island two hundred and fifty-six. And smooth is smoothstep again, clamped to zero and one, for the coast and the meadow. Island height is the formula we just saw, and from middle is the distance from the middle, with a square root. Hills is five octaves of noise, with x and z divided by ninety, so the first octave changes over about ninety metres. Peaks is four more, over a hundred and forty metres, with seed plus one hundred and one, which keeps clear of the seeds the hills' octaves use. And max keeps the part above zero, and the height adds them up like the formula. Coast is c, going from zero at fifty-five percent of the way to the edge, to one at ninety-five. And its comment reads: the land sinks into the sea towards the edge. It reaches one before the edge, so the last stretch of the grid is seabed all round, and the island doesn't end in a cliff where the grid does. And meadow is m, from twelve metres out to thirty-six, for gentle slopes in the middle, where the players start, and above the sea. Cutting the slopes to fifteen percent, and not to zero, leaves the meadow a few gentle bumps, instead of a flat floor. The board's constructor sets six cells, and seven by seven corners, all at zero, as a vector of floats starts out at zero. The island's keeps the seed, and makes two hundred and fifty-seven by two hundred and fifty-seven corners, as there's one more corner than there are cells along each side. That's sixty-six thousand of them, which takes a few milliseconds, and a quarter of a megabyte of floats. For each one, x and z are its place in metres, from minus half the size to plus half, and its height comes from island height. So the noise runs once, when the terrain is made, and everything after that reads the corners. Height starts by turning x and z from metres into cells, across and down, counted from the left and the top of the grid. The cell's i and j are the floor of those, clamped to the grid. The camera is going to ask for heights out past the edge of the grid, where i and j would point past the end of the vector. And u and v are how far into the cell the point is, from zero to one. They're clamped too, so a point outside the grid gets the height at its edge. Then the heights of the cell's four corners, from corner: top left, top right, bottom left and bottom right. With those four, we can work out the height anywhere in the cell, once we know which of its two triangles the point is in. The comment says which two: they meet on the diagonal from top left to bottom right. Either diagonal works, as long as the client cuts the cell the way the server does. If u is at least v, the point is in the top right triangle, and its height comes from the top side and the right side. Otherwise, it's in the bottom left triangle, with the bottom side and the left side. Blending all four corners, like the noise does, would be smoother, but that's a curved surface. The client draws two flat triangles, so a player's feet would sink into them or float above.
The bits agree
13:11 Before we test it, there's a problem with computing the island on two machines. Floats round each result to the nearest number they can hold. So take a times b plus c, which our noise does all the time, in mix and in smooth. Written out, C++ rounds the product, and then rounds the sum. But most processors have an instruction that multiplies and adds in one go, and rounds once, called fused multiply-add, or FMA. Rounding once is a little more accurate, so compilers use it when they can. Clang on Arm, which is my Mac, and GCC on Arm, which is Linux in Docker on my Mac, both fuse them by default. GCC on a plain x86 server doesn't, as the instruction isn't in x86's baseline, and with an option like march haswell, it does. So the client on my Mac and a server in a data centre would make slightly different islands. I tried it, and without the fix, an x86 build with FMA moves twenty-one thousand of the sixty-six thousand corners, by up to ten micrometres. You wouldn't see that by looking, but the server is going to use these heights to decide whether you're standing in the sea. So a corner that comes out a hair different can put the water's edge on one side of you on the server, and on the other in the client.
14:28 The terrain folder gets a CMakeLists of its own, where nanommo terrain is a static library of noise dot c c and terrain dot c c, like protocol. Then the fix, target compile options, with dash f f p contract equals off, as contracting is the C standard's word for fusing operations like these. It's private, so it applies to the library's code, and not to the rest of the game, which doesn't have to agree to the bit. The comment over it says why: compilers for Arm, and for x86 with dash march, fuse a multiply and an add into one instruction, which rounds once instead of twice. The heights would come out a little different on each machine. The other half of the fix is that the noise uses no sine, cosine or pow, as those come from each system's maths library, and different libraries round them differently. Square root is the exception, as IEEE floats define it to be correctly rounded. Then the tests, which link with the library and gtest main, like the others. In the top-level CMakeLists, add subdirectory terrain goes in after protocol, before the server and the client that link with it, and Doxygen's API docs get the terrain folder too. In the Dockerfile, the lint stage checks the terrain folder too, with clang-format and cpplint. Then the test, terrain test dot c c, which starts with our header, then bit, for bit cast, and cstdint. Fingerprint is a helper, and above it, we write what it is: FNV dash one A, over the bits of each corner's height. FNV is a small, well-known hash, and with it, the test checks all sixty-six thousand heights against one number, instead of a file of them in the repo. For each corner, bit cast gives us the float's thirty-two bits as an integer, which we XOR into the hash, before multiplying it by FNV's prime. So a change in the last bit of any corner changes the fingerprint. The first test checks that the bits agree on any machine. The island from seed one has the fingerprint I got on Linux, in Docker, and my Mac gives that number too, which it wouldn't without the flag. Its second line checks that seed two makes another island. The second test checks the shape, with gtest's expect greater than and expect less than. It wants land in the middle, above the sea, and sea at the edge, both along x and in a corner of the grid. The fingerprint changes whenever we tune the formula, and we'd paste in the new number. So this test holds what has to stay true through that: players start on land, with the sea all around. And the third checks height against the triangles, at points where we can work out the answer by hand. At the grid's corner three across and five down, the height is the corner's own height. Expect float equal, like in episode three, lets them differ in the last few bits, as height adds and multiplies on its way to the answer. And in the middle of the cell, the point is on the diagonal, so it's halfway between the top left corner and the bottom right one.
17:29 Let's build the library and its tests, with dash dash target, so it builds those and no more. All three pass on my Mac, so the island it computes is the one that Linux computed, to the last bit. CI runs these on x86, where GCC doesn't fuse unless we ask for it with dash march, so CI would pass even without the flag. On Arm, the test would fail instead, on my Mac and in Docker on it.
Drawing the island
17:54 First, the client needs the seed, so in our protocol, the world message gets a ninth field, seed. The server sends it in each client's own part, after the world, next to its id and its experience. So it costs a few bytes for each player a tick, and needs no new message for when they join. The parts class from episode nine, which puts the world back together from its UDP parts, keeps the seed too, in seed underscore. In parts dot c c, k room is the space we leave in each datagram for the fields that aren't players or monsters. With up to six bytes for the seed and its field number, it goes from forty-two to forty-eight, and its comment names the seed along with the others. When a part arrives, we keep its seed, and the world we put together gets it back. In the parts test, the client's own part gets a seed of four billion, near the largest there is. So the check that each datagram fits in twelve hundred bytes covers it. In server dot h, create's comment gets a sentence: the seed makes the terrain. And create takes the seed after the tick's interval, as both are about how the world runs. With one more parameter, the line doesn't fit in eighty columns any more, so clang-format moves the tokens down a line, next to the players. The private constructor takes the seed in that place too, and the server keeps it in a member after the interval, next to the world's random seed. Unlike that one, the island's seed has to stay put when the server restarts, as the players' saved positions are on that island. In server dot c c, clang-format wraps the definitions like that too, and create passes the seed on to the constructor, through wrap unique. And the constructor keeps it after the interval, in the order the class declares its members, like before. Then, in broadcast, each player's own part gets the seed, next to their id, their experience and their next level. In main, the seed comes from a setting, nanommo world seed, which is one by default, like the other settings. Simple a to i returns false for anything that isn't a whole number from zero to four billion and a bit, as seed is an unsigned thirty-two-bit integer. Then we print what the setting should be, and return one, like for a bad port. Server create gets the seed, and the line the server prints when it starts says which seed it's on.
20:14 Now, the client has to turn the corners into triangles for the GPU. Raylib has a function that makes a mesh from a height map, but it reads the heights from the grey of an image, so they come in two hundred and fifty-six steps. It also cuts each cell along the other diagonal from the one our terrain's height uses, and gives the faces no colours. So we're going to build the triangles on our own, in a new header, triangles dot h, with raylib and vector. Face normal is the normal of a triangle, and its comment says which way it points: out of the side where the corners go anticlockwise, which is the side OpenGL draws. Triangles collects them, and the comment over it gives the reason: they share no corners, so that each has its face's normal. And the light shades each face flat, like a low-poly model. The cost is memory, as each corner of the grid is stored six times, once for each triangle that meets there. Add takes the three corners of a triangle, and a colour, and size counts the triangles from the colours, at twelve bytes a triangle. And upload hands them to the GPU, as a mesh for load model from mesh. Inside, three vectors, the positions, the normals and the colours, as raylib's mesh wants an array for each kind of data, which the GPU reads side by side. In triangles dot c c, after our header, raymath, then algorithm, for copy, and vector, and in an unnamed namespace, a helper for upload. Copy is a small template, so that it works for the floats and for the bytes, and the compiler works out T from the vector we pass in. It asks mem alloc, raylib's allocator, for enough memory for all the values. We don't use new here, as memory from new has to go back through delete. The comment says who frees it: unload mesh frees what's here, so raylib allocates it. Mem alloc is the C library's allocator, unless raylib is built with an allocator of its own, so going through it keeps us matched with unload mesh either way. Then the values go over with std ranges copy, and the helper returns a pointer for the mesh to hold on to. Face normal is the cross product of two edges, b minus a and c minus a, which points out of the triangle, and normalize gives it a length of one. Add works out the normal once, and then for each of the three corners, it appends the position, the normal, and the colour. Each insert takes a list in braces, so the three floats of a position go on the end in one call. Upload starts from an empty mesh, which is raylib's struct of pointers to the arrays and their counts. And it fills in the triangle count, with three corners for each triangle, as no corner is shared. Then it copies the three arrays with copy, and upload mesh, with false, as the mesh won't change, puts them in buffers on the GPU. And we return the mesh, for load model from mesh to make a model of it.
23:12 The ground goes in a new header, ground dot h, which starts with raylib, and our terrain dot h. Ground mesh makes the terrain's two triangles in each cell, coloured by height and slope. Sea mesh is a square at sea level, four times as wide as the terrain, so the sea goes on past it, all the way to the fog. K sea is its colour, a blue with an alpha of two hundred and ten, so you can see the sand under the water near the shore. In ground dot c c, algorithm and cstdint, then triangles dot h, and the noise's header, for its hash. Then the four colours we paint with: sand, a pale yellow, grass, rock, a warm grey, and snow, nearly white. And the heights between them: the beach below point six of a metre, the tree line at twelve, and the snow line at nineteen. K steep is point eight, and when the y of a face's normal is below this, it's too steep for grass. A flat face has a y of one, and point eight is about thirty-seven degrees. Ground color picks the colour from the height and the slope, and the order of the checks decides what wins. Below the beach is sand, which goes for a steep seabed too, and above the snow line is snow, even on a steep peak. Above the tree line, or too steep, is rock, and the rest is grass. Vary keeps a field of grass from looking like one flat sheet, by making a colour a little lighter or darker from a hash. The change is up to four percent lighter or darker, so that faces of one colour still stand apart. Shade comes from the hash's last byte, and scaled is a lambda that multiplies a channel by it, clamped at two fifty-five, while the alpha stays as it was. Add face is where a triangle gets its colour, from the helpers above. Height is the average height of its three corners, and ground color takes it with the y of the face's normal, which is how steep the face is. Then it adds the triangle in that colour, varied by the hash. Ground mesh walks the grid, row by row, and cell by cell. Corner is a small lambda that captures the terrain and half the size by reference. And it turns a corner of the grid into a point in metres, with its height, as x and z go from minus half the size to plus half. Then come the triangles we fill, and the loops over the rows and the cells. Each cell takes its four corners, top left, top right, bottom left, and bottom right, and makes two triangles, which is a hundred and thirty-one thousand for the island. With no shared corners, that's about eleven megabytes of positions, normals and colours. The two triangles have to match the ones height uses on the server: top left, bottom right and top right, and then top left, bottom left and bottom right, which is anticlockwise from above. If the client cut the other diagonal, players would float over some cells and sink into others. Each triangle gets a hash of the seed and two i, or two i plus one, as the two in a cell each want a shade of their own. So the shades are part of the island too, and come out alike on each machine. Then we upload them, and return the mesh for the renderer. And sea mesh is raylib's gen mesh plane, a flat square facing up, made of two triangles, as a flat sea needs no more than that. It comes with normals already, and no colours, so it's white, and k sea tints it when we draw it.
26:30 In the client's CMakeLists, the core library gets triangles and ground, and links with nanommo terrain. In renderer dot h, optional, for the models we're about to add, then camera dot h, which moves here from the source file, and our terrain dot h. The renderer drew from game camera until now, and next, it's going to follow the player. So it keeps a camera of its own, with a getter, which the nameplates are going to need, and set camera. Set terrain builds the terrain's meshes, in place of any we had. It takes the terrain by const reference, as the renderer keeps the meshes it builds, while main keeps the terrain, for the figures' heights. When the server sends a new seed, say after it restarts with another one, the client calls it again. In the private part, unload terrain is a helper for that, as the destructor needs it too. The camera starts out as game camera, so the tests draw what they drew before. And the ground and the sea are optional models, as there's no terrain until we know the seed. In renderer dot c c, ground dot h replaces camera dot h. The ground has colours of its own, and raylib passes a mesh's colours to a shader as vertex color. So both vertex shaders take it in, and hand it on to the fragment shader as color. A mesh without colours, such as our cube, gets white from raylib. The fragment shader multiplies the material's colour by the vertex's colour and the light, and the alphas together. The destructor unloads the terrain first, and then the cube and the shaders, as before. Set terrain unloads any terrain we had, then loads models from the ground mesh and the sea mesh. And above the shaders, we write that the ground and the sea have no bones either, so they're drawn with the cube's shader, like the sword. Unload terrain unloads both, and resets them, so a second call finds no terrain to unload. In draw, begin mode three D takes our camera, instead of game camera, and if there's a ground, we draw it first. Each figure stands at its height now, so the cube we draw without characters, and the white marker over you, go up by the figure's y. The characters and the monsters are drawn as before, after the ground. And the sea comes last, at sea level, in its colour: last, as we see the ground under it through the water. Blending a see-through colour works over what's drawn already, so the sea has to come after the ground, and after the figures that wade into it. In scene dot h and scene dot c c, the board's tiles go, and the player colours stay, as the renderer still uses them. In nameplates dot h, raylib, for the camera's type, as draw nameplates takes the camera now. Nameplates dot c c drops camera dot h, its definition takes the camera too, and each nameplate goes up by the figure's height. In main, optional and vector, and the terrain's header in place of the scene's. The ground cubes go, and the client keeps an optional terrain, empty until the server sends the seed. Each frame, once the world is in, if we have no terrain, or its seed isn't the world's, we make it from the world's seed. And emplace builds it in the optional, and then we hand it to the renderer. That takes a few milliseconds, and it happens once, when we join. Then the figures from the animator, each with its y from the terrain's height under it. The server sends x and z, which is enough, as the client knows the ground too. Further down, the nameplates get the renderer's camera, as their new argument. Last, in the renderer test, the terrain's header replaces the scene's, and the test that drew the board becomes draws the terrain below the sky. Its hidden window and offscreen target are sixty-four pixels square, as before, and it sets the terrain from seed one. The camera is ten metres up in the middle, looking a hundred metres along z at that height, so it looks out to sea, level with the horizon. So the top of the picture is the sky, which is the background, and the bottom is the island, which isn't. If set terrain didn't draw anything, the bottom would be sky too, and the test would fail.
30:39 Let's run it, and the server's terminal says it's on world seed one. And we're standing in the meadow in the middle of the island, on triangles you can see, each in a shade of its own. The camera is still the one from episode two, looking at the middle, and the server still keeps us on the old board, twelve metres across. So we can't go and see the rest of the island yet.
A camera that follows
31:04 For the camera, I want what a lot of online role-playing games do, a camera that follows your player from behind and above. It looks at your player's chest, so that you're in the middle of the screen. The mouse wheel brings it closer, or takes it further away, from four metres to forty. The further it is, the more it looks down, so close up, you see the terrain ahead, and far away, more of what's around you. Q and E turn it around you, and so does dragging with the right mouse button. Behind you, up a hill, it would end up inside the hill, so it stays a metre above the ground under it. And once the camera turns, the client turns the direction of the keys by the camera's angle before it sends it, so that W walks away from the camera.
31:48 In camera dot h, our terrain's header, as the camera has to know where the ground is. Game camera stays, still looking down at the middle of the board, from above and behind. Then a class, follow camera, which follows your player around, from behind and above, and k nearest and k farthest are the four and forty metres. Update turns the camera by an angle, in radians, and zooms in by a share of the distance, and then looks at the player from there, as its comment says. It takes those as numbers, instead of reading the keys, so the input stays in input dot c c, and a benchmark can drive the camera too. Relative turns the keys' direction from the screen's to the world's, so that W walks away from the camera. Camera is a getter, and the class keeps the yaw, which is the angle around the vertical, starting at zero. And it keeps the distance, fifteen metres, and the camera, starting as game camera. In camera dot c c, raymath, for lerp, algorithm, for clamp and max, and cmath. In an unnamed namespace, k chest goes with the comment: up to the player's chest, so that they're in the middle of the screen. Then the pitch, which is the angle it looks down at, twenty degrees closest, and fifty-five farthest, as the camera looks down more steeply the further it is. And k above ground, a metre, for how high above the ground the camera stays. Game camera's definition stays as it was, below the namespace. Update starts with the input, and adds the turn to the yaw, which can grow past a full turn, as sine and cosine work for any angle. The zoom multiplies the distance, instead of adding to it, so a click of the wheel feels alike close up and far away. The distance is clamped to the nearest and the farthest. Far is how far along that range the camera is, from zero to one, and the pitch goes from twenty to fifty-five degrees along with it, with raylib's lerp. The target is the player, raised up to the chest by k chest. The position is a point on a sphere around the target, worked out from the yaw, the pitch and the distance. Sine for x and cosine for z means that a yaw of zero puts the camera straight behind, along z, which is where game camera has been since episode two. Then max keeps the camera a metre above the terrain under it. And the reason goes over it: up a hill behind the player, it would see the inside of the hill. Last, the camera gets the position and the target, and keeps game camera's up and field of view. Relative turns the direction by the yaw, which is a rotation in x and z, with the cos and the sin of the angle. With a yaw of zero, it gives the direction back as it was, which is how the keys worked before. In input dot h, two functions, next to the ones for the keys. Camera turn is how far to turn the camera this frame, in radians, with Q and E, or the mouse, dragged with the right button down. And camera zoom is how much closer to bring it, as a share of its distance, from the mouse wheel. In input dot c c, imgui, to check whether the GUI has the mouse, and numbers, for pi. K turn speed is pi radians a second, which is half a turn, and k drag turn is how far a pixel of the mouse turns it. And each click of the wheel is a tenth of the distance. Camera turn takes the frame's seconds, so the keys turn the camera at a steady speed, whatever the frame rate. It starts at zero, and Q turns it one way, while E turns it the other. And when the right button is down, the mouse's movement across turns it, unless the GUI has the mouse. Get mouse delta is how far the mouse moved since the last frame, in pixels, so unlike the keys, it doesn't need the frame's seconds. As the comment says, the mouse is the GUI's while it's over one of its windows, so dragging a settings window around doesn't spin the world behind it. Camera zoom checks the GUI first, so scrolling a window doesn't zoom, and returns zero then. Otherwise, it's get mouse wheel move, which is how many clicks the wheel turned this frame, forward or back, times the step. In main, algorithm, for find if, and our camera's header. The client makes a follow camera, next to the terrain, and the direction we send is the keys' direction, made relative to the camera. And after the figures get their heights, we find the one that's us, with ranges find if, which takes a pointer to the member to check. If it's there, the camera updates from our position, with this frame's turn and zoom, and the renderer gets the camera.
36:21 Now that the camera can look across the island, we're going to see its edge, and the end of the sea. Games hide that with fog, and it's a few lines in our shaders. F is how much fog a point gets, from w, which after the projection is how far in front of the camera the point is. Up to fifty metres, there's none, and from there, it grows until a hundred and forty, where it's all fog. Then the pixel's colour mixes from what it would have been towards the sky, by f. The sky is the background colour, so far away things fade into it, and the settings window that changes the background changes the fog too.
36:57 In renderer dot h come the fog's two distances, in metres from the camera, where the fog starts at the first, and hides everything past the second. And the background starts as a sky blue, instead of the night we had, as a sky is what the fog fades into. In renderer dot c c, array, for the background's colour. The vertex shader for the cube and the ground gets fog near and fog far as uniforms, which are values we set from C plus plus for a whole draw. And it hands fog on to the fragment shader as an output. It works out the position first, as before, as the fog needs it. Then fog is the formula we just saw, from gl position dot w. Working it out for each corner, instead of each pixel, and letting the GPU blend it across the triangle, is enough, as fog changes slowly across a two-metre triangle. The skinned shader for the characters gets the two uniforms and the fog too, so they fade like the ground. The fragment shader takes fog in, and fog color, another uniform. Lit is the colour as it was, and the pixel mixes it towards the fog's colour, by fog, with GLSL's mix. That's the shaders done, and back in C plus plus, the uniforms need values. Set fog sets the three uniforms on a shader, with a comment: into the background's colour, so that far away things fade into the sky. Get shader location finds each uniform by name, and set shader value sets it. Each call takes the shader, the location, a pointer to the value, and its type, so set shader value knows how many bytes to send. First fog color, from the background's array, as a vec three, and then fog near and fog far, from the renderer's constants, as floats. And draw sets the fog on both shaders, each frame, after clearing and before it draws anything, so a change in the settings shows up right away. In menu dot c c, the background's presets get a fifth one, day, first, with the sky blue we start with. So you can come back to it after trying the night.
39:00 With the camera following us, E turns it around me, and in the distance, the hills and the mountains fade into the sky. W walks away from the camera now, wherever the camera looks. Q turns it the other way, back around behind me. And the server still stops us at the edge of the old board, five and a half metres out.
Walking the island
39:22 The server decides where players can go, and until now it clamped them to the board, so the terrain is going to say where a player can stand. In terrain dot h, k wading is how deep players wade into the sea, up to their knees. That's forty centimetres, so you can walk along the beach in the water, and not swim out to sea. Walkable is whether a player can stand at a point, and edge is half a metre short of each side, so that players stay on the grid. For the board, that's five and a half, like the constant the world had. In terrain dot c c, walkable checks that the point is within the edge, along x and along z, with std abs. And it checks that the ground there is no lower than sea level minus the wading depth. The board is at zero, which is sea level, so all of it is walkable. In the terrain test, the test for land and sea checks walkable too: the middle is walkable, the sea at the edge isn't, and neither is a point off the grid. In world dot h, the terrain's header, as the world is going to keep one. Walk is a free function, which the players and the monsters are both going to move with. As its comment says, it moves to the place, or if the terrain's in the way there, as far as it can along x, or along z, alone, so that it slides along a coast. Without the slide, a player running at the shore at an angle would stop dead, and you'd have to turn to get going again. The world's k edge goes, as the edge is the terrain's now. The constructor takes a terrain, the board by default, so our tests keep their board. A getter gives the terrain to the server, and the world keeps it, in terrain underscore. In world dot c c, walk takes x and z by reference, so it can update them, and where they're going, to x and to z. It tries the whole step first, then x alone, keeping z, and then z alone, keeping x. Sliding by axis is the simplest way, as it needs no direction for the coast. The cost is that along a coast at an angle, you slide at the speed of one axis. And if none of them is walkable, it stays where it is, so the caller can tell that it didn't move. The constructor moves the terrain into the world, so its quarter of a megabyte of corners isn't copied. Join is where a player comes in, with what we loaded for them from Redis or MySQL, and it checks that their position is walkable. The comment over the check gives the case: saved somewhere that's sea now, such as on a terrain from another seed. A player like that goes back to the middle, where the players start. In move, edge is the terrain's edge, instead of the constant. X and z start where the player is, and the new place is clamped to the edge, along each axis, as before. Walk takes it from there, and moves x and z as far as the terrain lets them, and the rest of move works out the motion, as it did. The comment we wrote in episode seven, on the motion coming from how far the player went, and not from the keys, now covers the sea too. So it reads: motion comes from how far it actually went, so that a player pushing against the edge, or the sea, stands still, instead of walking on the spot. And the monsters' hunt gets the terrain too, as monsters walk the island as well. In world test, the test that keeps players on the board reads the edge from the board terrain. And a new test, walks the island but not into the sea, makes a world on the island from seed one. A player who joins far out at sea, at a thousand metres, starts in the middle instead. Then the player runs along x for two minutes of the world's time, a lot further than the coast. The test still takes a fraction of a second, as it steps the world's time and doesn't wait for it. After each call to run one second, we look up the player, and assert true checks they're still somewhere walkable. Assert stops the test there, where an expect would go on and print each second that failed. At the end, they've gone more than fifty metres, and they're standing still, at the water.
43:15 The monsters walk the island too, so monsters dot h gets utility, for pairs, and vector, and the terrain's header. Camps returns places for camps of monsters, as pairs of x and z. Above it, we write where they go: the terrain's seed picks them, between the beach and the rock, and well away from the middle, where the players start. Further down, wander and hunt take the terrain, as they move the monsters, and the terrain says where they can go. In monsters dot c c, utility and vector too, for the camps. In the unnamed namespace, camps are at least thirty metres from the middle, and between one and ten metres high: above the beach, and below the rock. Pick target, which picks where a wandering monster goes next, used the world's edge. And now it takes the terrain, and clamps to the terrain's edge, along x and along z. It doesn't look for the sea, so a target can be out at sea, and walk is what stops the monster at the water. Then camps, which takes the terrain and how many camps we want. It makes a Mersenne twister like the world's, seeded with the terrain's seed, so a seed puts the camps in one place each time the server starts. Place is a uniform real distribution, like in episode ten, from minus the edge to the edge. The standard pins down the twister's numbers, but not how a distribution turns them into floats, so a server built with another standard library could put the camps elsewhere. That's fine here, as the camps are the server's alone, and the clients get the monsters from it. Over the loop goes a comment: most of the grid is sea, so it takes a few tries to find each one. A thousand tries is a lot more than eight camps need, and the limit is there in case a seed makes an island with little land between those heights. Each try picks a point anywhere on the grid, and the height there. A place is a camp if it's far enough from the middle, with std hypot, no lower than the lowest camp and no higher than the highest, and walkable. If it is, emplace back adds it to the end of the list. Wander, which a monster runs when it has nobody to chase, passes the terrain on to pick target. Then it remembers where the monster was, in x and z, and moves it with walk, instead of adding the step. And walk takes it to the target if it's that close, or otherwise a step towards it, while it turns to face the target. The check that follows has a comment: there, or stuck at the sea, it rests before it picks another place. Without the second half, a monster whose target is at sea would walk on the spot at the water until a player came by. Hunt passes the terrain on to wander, when there's nobody to hunt. And when it runs at a player, it moves with walk as well, so a skeleton chasing you along the beach stops at the water too. In monsters test, the tests pass the board terrain to wander and hunt. The test that keeps a monster on the board makes the board, and the corner it starts in comes from the board's edge. And the test on running at players passes the board to hunt as well. Then a new test, camps above the beach away from the middle, asks the island from seed one for eight camps, and gets eight. For each of them, a structured binding gives us its x and z, and it's at least thirty metres from the middle, with std hypot. Expect greater or equal checks that it's at least a metre high, so it's above the beach. And asking again gives those places again, so a server that restarts puts the camps back where they were. In the server's CMakeLists, the core library links with nanommo terrain, as the world keeps one now. In server dot c c, the terrain's header, and k camps, which is eight. Eight camps of three is twenty-four more monsters, and each player gets all of them in each world, which is a few hundred bytes more a tick for each player. And k camp skeletons is where a camp's three skeletons stand, each about a metre and a half from the middle of the camp. In the constructor, the world is made with the terrain from the seed, next to its random seed. In start, the three skeletons around the middle stay where they were. Then each camp spawns its three skeletons where they're walkable, so a camp on a narrow strip of land may have two. On the client, with monsters all over the island, the nameplates need two checks. In nameplates dot c c, raymath, for the vector maths, and k farthest, fifty metres. And the comment over it reads: further than this, figures fade into the fog, and their nameplates would float over the hills in front of them. Forward is the way the camera looks, from its position to its target, normalised. For each figure, away is the vector from the camera to it. For a figure that's close and in front, the nameplate is drawn as before. We skip figures further than k farthest, and ones behind the camera, where the dot product of away and forward isn't positive. The comment says why: behind the camera, get world to screen would put it on the screen. It projects through the camera, and a point behind it comes out mirrored, back onto the screen.
48:16 The terrain's three tests pass, with walkable in them now. Then dash dash gtest filter keeps to the world's and the monsters' tests, as several of the others need MySQL and Redis running. And they pass too, the old ones on the board, and the new ones on the island.
48:33 Now we can leave the middle, so let's run west. The ground rises and falls under us, and the player stands on the triangles, as the client puts each figure at the height of the ground it draws. Past the beach, there's a lake, and we run into it, up to our knees, where the server stops us. Running at it at an angle, we slide along the shore, instead of stopping. And as we turn the camera, the sand and the hills around the lake come into view.
49:01 Here we're joining the server again, but it starts with seed two this time. The client gets four different bytes for its world, and builds another coast and other hills from them, while the server puts its camps elsewhere. The meadow in the middle is still here, because the island formula makes the middle gentle and above the sea, whatever the seed.
Docs
49:25 Last, the README, where the opening says that the players are on an island now, with skeletons in camps, and how the camera works, with Q and E, the right mouse button and the wheel. A new section on the world has the seed's setting, nanommo world seed, so you can try another island. And it writes down why the server and the client have to agree to the last bit, and what keeps them agreeing, for whoever changes the noise next. The monsters' part says where the camps go, between the beach and the rock, and at least thirty metres from the middle. It also says that a monster the sea stops rests, and picks another place. And the fighting part says island where it said board.
Wrap-up
50:08 So, today we wrote value noise, from a hash of the seed, with octaves for the detail. With it, we made an island, five hundred and twelve metres across, in a library that the server and the client both build, so the seed is all that goes over the network. We saw that compilers fuse multiplies and adds, which moved the heights by up to ten micrometres between machines, and turned it off, with a test that pins the island's fingerprint. The client draws the island as triangles that share no corners, so each face is shaded flat, with a sea, and fog that fades into the sky. A camera follows your player around, and turns and zooms. On the server, players wade into the sea up to their knees, and slide along the coast, and the seed puts camps of skeletons around the island.
Playing it
51:54 Let me know in the comments what you'd like to see next. And if you want to follow along with the series, subscribe to the channel. I hope you find this useful, and thanks for watching.