This is the first entry in my attempt to create a game from scratch.
Firstly, I feel like justifying myself. I don't have any illusions of grandeur, no great idea's for a game "that will change the world!". I was just curious about the challenges and problem solving that goes into the creation of games.
It started out innocently enough, "I wonder what a simple client/server game protocol would look like." Being that most of my programming experience has been in python I figured it was a good enough place to start. I realise that a lot of people consider python to be pretty slow for these sorts of things, and well, it probably is. But I don't need it to be fast. I just need a functional prototype, and python with it's sockets, select calls, and structs seemed comparable to the more commonly used C-family of languages.
Needing a starting point I did some googling to see what people recommended, and the following is what I've ended up with. On the server side, I have a multi-threaded socket servers that spawns a new thread for each socket connection. I was originally using
system.fork() instead of threading but I had trouble sharing resources between the separate processes.
This server is connected to using a socket on the client side which sends packed structs over TCP. The server then interprets these structs and sends back a similar packet containing the information requested (or in possible future cases, simply an acknowledgement).
The layout of the structs is currently three unsigned ints followed by the payload.The first unsigned int is the length of the packet (the number of bytes in the message), this number includes the length of the length. This was a late addition to the protocol that I found I needed in order to distinguish between separate messages (as they would sometimes all come in at the same time).
The second unsigned int is the protocol version number (which is currently 256).
I chose 256 because in hex 256 this is represented as
0x100. This structure allows me up to 255 minor versions. Specifically, I can use the first value as a major version, the second as a minor version, and the third as a minor-minor version number (allowing each a value between 0 and 15 inclusive). In most common programs a major version increase indicates changes that aren't backwards compatible, a minor version increase indicates new features that are backwards compatible, and a minor-minor version increase would be a bug fix or some other non-breaking change.
The third unsigned is just an identifier so we know what the packet is regarding and how we should interpret any commands given. In some cases, this value is enough to know what they're asking. For example, a
2 identifier indicates they're asking to know what player they are without us having to look up a command.
With that issue solved, I needed to actually create a world that I could have two separate clients move around in, allowing me to test the protocol.
This world has ended up being a simple 2 dimensional array, creating a kind of matrix. An example usage would be
matrix[x][y] = Grass(). However that's not quite how things ended up. Because I wanted to have sensible access to the matrix in the form of:
for line in matrix: for tile in line: print(tile)
what I've actually ended up with is backwards co-ordinates
matrix[y][x] = Grass(). Which while not ideal, is still functional so long as I'm consistent.
I quickly decided that an empty grid wasn't much fun, so had to find a way to make it more interesting. With that in mind, I turned the grid into a sort of maze with the following.
When we first generate the world we fill it with Walls. Afterwards, we generate a number of rooms into the matrix, and then we fill in the rest with a maze. We then clean up any dead ends in the maze. Ideally what I'd like to do is connect each room together instead of building a maze, but the algorithms for doing so were a bit hard to get my head around.
The rooms were mostly what it sounds like, I would randomly choose a width and height from within a given set, by supplying a minimum and maximum value, and then randomly selecting a number from between the two. Then we'd randomly select a point on the map and check that each tile within the square steaming from that point was a wall. If any tile was already empty it meant our new room was going to overlap with an existing room, so we'd stop and try again. Otherwise we'd replace each of the tiles within that square with an empty tile.
The Maze generation was a bit more complicated. I mostly ended up stealing an algorithm for this from else where in the web. For it, we started by selecting a point on the map and then moving in a direction from there. Each time we moved, we check each of the four surrounding tiles from our selected tile, from these we using a weighted number, choose a direction to go in (somewhat favouring the previously used direction so that it doesn't devolve into a windy mess) and then we replace that grid position and the next 2 with an empty tile. I haven't done a very good job of explaining this because I'm still a bit vague on how it works exactly.
In any case, after we've done that we go through and remove all the paths that lead to a dead end, which makes the maze a bit neater and less random.
The end result is this:
To get all of this actually playing nicely I had to create a client that could connect to our server. I briefly discussed it earlier. The client is made up of three threads, a networking thread, a draw thread, and an input thread.
The networking thread pulls commands from a network queue and sends them over a socket to the server. It also reads responses from the server and uses them to update the world, after which it tells the draw thread to redraw the world.
The draw thread draws the world to the terminal using simple ASCII characters. Currently it uses a 'clear' command to clear the screen before the redraw.
The Input thread handles character input. Because I'm doing all of this in the terminal I'm beholden to the rules of the terminal, with the main one being that by default the input is read on a per line basis. Meaning any key press has to be followed by a new line (the enter button by default) before the game will pick it up. I can hack around this by setting the terminal to raw mode, but this breaks the formatting of my draw calls, which I'm too lazy to look into and fix.
So while the input situation isn't ideal, it's at least functional.
The resulting code can be found Here.
Next week, I'll have a go at real OpenGL rendering and maybe port the whole thing over to c++, which will be fun as I have no experience at all with c++ (and only minor experience with C)
When I started this I had no idea what I was going to end up with, or how far I'd even get. But after creating a world, a network protocol, and a client/server application, I think I'll keep going. Not sure what the end product will be, be at the moment I'm leaning towards either something Dungeon Keeper-y, or something like the original Pokemon games.