What tools did I use?
The game mechanics and gameplay is built using the client-side programming language JavaScript, whereas for the backend (the server functions); I used server-side technologies PHP and SQL to store and obtain information from a database, inside a local server Apache.
I used a library called ECSY.js, which allows you to work with an architecture based on entities, components, and systems rather than classes and methods. It is much more flexible to work with, so I decided to build my game based on it. In addition, the graphics were rendered using the canvas API that JavaScript offers.
Backend functionallity
It is important to clarify that there were many tasks to do in the project, such as login and register functions, form validation and session management; however I will only be focusing on the main feature which is how I did to implement the room system, to play using your phone as a controller instead of a keyboard.
-
Room Creator
A person can click a button to send a request to the server. The server calls sessionManager to check if the session has already been started and if the user token matches the one on the server. The server redirects the user to the login page in case the session isn't valid.
If the session is valid, we create an instance of roomManager, which opens a connection with the database and checks if the user has not created a room. If the user has already created a room, they are redirected to the waiting room of the existing room. Otherwise, the createRoom method is called, and a new room is created with a unique code of 8 characters. The user is then redirected to it.
-
Join Room
The user can add an 8-character code to a form that sends a request to the server. The server calls the roomManager, and the code is compared with The rooms are inside the database. If a room is found, we check its status. We also check if the user is not in a room already.
Room Status
- 0 : The game has not started yet
- 1 : The game has already started yet
- 2 : The game has come to an end (game over)
For a user to join a room, the status must be 0, otherwise, a message will be sent back to communicate the game is already started. If the code is, a new player is registered in the database, storing the code of the party it belongs to.
-
User Redirection
When a user enters a room, it sends a request to the server, this request tells the server to look at the room status every specified time interval; if there is a change in the status, send the new status back to the client. The user will be redirected to a new page, depending on the status of the room, the path it follows is:
0 : waiting_room >> 1: gameplay >> 2: game_over
-
Player Server Host
These are the three main components of the game architecture, players and room hosts can't communicate directly; the server acts similar to a proxy, we can see how it works in the diagram bellow.
Both players and the host need updated information about the room status and players inputs; we could simply use short polling, asking the server for new information every; that would be really inefficient. The clients send a single request to the server, and the server keeps the connection opened until there is a change in the room or players. When it gets updated information, it sends it back to the clients; this method is called long polling.
Game development
-
Snake movement and construction
The first time I built a game like Snake, I followed a very inefficient way of coding snakes. I used to have a queue of adajacent squares that swapped positions every frame; if a snake was 100 squares long, 100 squares had to be drawn and processed (imagine a 1000 square long snake!). I compared my code with the code of tutorials and they all followed the same process as mine (not a good one).
When I was in school, our teacher asked everyone in line to move one place to the left to make room; I thought to myself, "Why move everyone in line?" Could we just move the last person in line next to the person on the opposite edge? " At that moment, I had a brilliant idea to optimise the game using this analogy with the same principle.
So far, the game had been more optimized, but there was one thing that could be improved: the number of objects that had to be created in order to build a snake.Snakes were entities, built from smaller entities; every snake was a group of adjacent square-shaped entities that formed the body of it. When a snake grew, more squares were added to them, so the bigger they were, the more memory and time they consumed.
I wondered, "if I want to build a snake 100 units long, do I really need to create 100 squares? Why not just use one 100 unit long rectangle?". Well, we could just have created a rectangle from the beginning, but how do we make the snake bend?
The solution ended up being to create another rectangle when the snake bends, so every time the snake changes direction, a new rectangle is added; this is way more efficient than having a bunch of abjacent squares. In addition, we also need to change the length of rectangles to show the movement of nodes; the last rectangle is subtracted one unit from its length, and the first rectangle (the one closer to the head) is added up one to its length.
When the last rectangle reaches a length of zero, it is removed from the list, and the process continues, substracting from the next rectangle (which is now the last one). We ignore all rectangles that are between the last and first rectangles, similar to the way we did before when moving the last square to the front.
-
Collision detection between snakes
Collision detection is an essential topic that allows us to create interaction between overlapping objects; for example, we can check if two snakes collide with eachother, or if one of them collides with itself; this is a key part of snakespace.
In the game, entities can have position and shape components; shapes include circles, polygons, and you are right, also rectangles. I built a parametrised way to check if two entities overlap (collide), a collision detector. There are many types of collision that can be tested, but I'll explain the Box vs Box test used.
Self collision (intracollision)
We can test for collisions between the snake head and each of its nodes (rectangles), this operation takes linear time O(n), because we iterate through each node once. A snake can collide with its own body if it has atleast 4 rectangles, this is because the snake needs to turn 4 times to hit itself. It is pointless to test for collisions if the snake has less than 4 rectangles, so we only test collisions from the 4th node. (Note: The presence of portals could mean that a snake with less than 4 rectangles could collide with itself)
Collision with others (intercollision)
Testing for collisions between two snakes is far more complex. We could just check each rectangle from one snake against each rectangle from the other; this takes a lot of time when there are many rectangles. There are times when snakes are very far from each other, but we would still need to consume resources testing collisions.
One optimization we can do is by using composite colliders; we can put all the rectangular colliders inside a bigger collider. Before testing each rectangle, we can test if two composite colliders are overlapping; if so, we can calculate the overlapping region. The overlapping region represents the area where collisions are possible; we can test which rectangles are inside this region and then test collisions between these rectangles.
If I had to explain in detail every single part of the project snakespace, it would take half a book to do, because there are many topics to consider like rendering, primitives, portals, composite objects, etc. I won't explain the rest of topics, as most of them are general game concepts; I have explained the fundamental part, snakes.