Agario Clone

Simple Agar.io clone

Introduction

The current game files are being hosted on my GitHub repository.

After the first week of winter break, I had nothing to do so I poorly slapped together a simple Agar.io clone in a week for a side project.

For my server, I utilized Python's Flask and Socket.io libraries for the client server Web Socket and API connections. The player and room indexing was supplied by a MySQL Docker container. The client side is made with HTML, CSS, and JS, specifically utilizing the P5.js library for rendering the game canvas.

The game in it's current state is not complete but the following features have been implemented:

Features below will be added eventually.

Most of the rendering and game logic issues I had were quickly fixed by Andrew Li

AndrewTheBigManLi.png

Server Setup

Database

I utilized MySQL for the creation of players and rooms. As of right now, it doesn't do much.

For testing I setup the MySQL container using Docker Desktop. I had to set the root password environment variable to access the server. 

MYSQL_ROOT_PASSWORD=123

I also hard coded the connection credentials in the python app, but for production I would use the python OS environment variables.

In the following code, I create the Database "Agario" which contains the Rooms and Players table.

Since the PlayerID and RoomID need to be unique, I use the Primary Key and Auto Increment keyword when creating the tables.

import mysql.connector


def createDB():
    try:
        global db
        db = mysql.connector.connect(host="localhost", user="root", password="123")
    except:
        print("Database connection failed, exiting")
        exit()

    global dbCursor
    dbCursor = db.cursor(buffered=True)

    dbCursor.execute("DROP DATABASE Agario") # for testing, clears database on start

    dbCursor.execute("CREATE DATABASE IF NOT EXISTS Agario")
    dbCursor.execute("USE Agario")
    dbCursor.execute(
        """
                     CREATE TABLE IF NOT EXISTS Players (
                     PlayerID int NOT NULL AUTO_INCREMENT,
                     Name VARCHAR(255) NOT NULL,
                     UNIQUE (Name),
                     PRIMARY KEY (PlayerID)
                     )
                     """
    )
    dbCursor.execute(
        """
                     CREATE TABLE IF NOT EXISTS Rooms (
                     RoomID int NOT NULL AUTO_INCREMENT,
                     PRIMARY KEY (RoomID)
                     )
                     """
    )
    db.commit()

We can insert a Player using the following code:

def createPlayer(Name):
    query = """
                INSERT INTO Players (Name)
                VALUES (%s)
            """
    try:
        dbCursor.execute(query, (Name,))
        db.commit()
        return dbCursor.lastrowid
    except:
        return -1

API

Using the Flask and Socket.io libraries in python, I was able to quickly put together a working API and WebSocket connection between the server and client.

In hindsight, using just the WebSocket connection would have sufficed for this game.

I used the API for all the menu items such as:

The code below is an example of how I created the /players/create endpoint for my API. I used the Flask blueprints because I wanted the endpoints for players and rooms to be in separate files and URLs. If the server receives data on the /players/create endpoint, the createPlayer function will be able to display the payload that the client sent over. The client also gets returned a message along with a HTTP response code.

# app.py

from flask import Flask
from flask_cors import CORS
from flask_restful import Api
from flask_socketio import SocketIO, join_room, leave_room

from players import players
from rooms import rooms

app = Flask(__name__)
app.register_blueprint(players)
app.register_blueprint(rooms)
CORS(app)

api = Api(app)
socketio = SocketIO(app, cors_allowed_origins="*")

if __name__ == "__main__":
    socketio.run(app, debug=True, use_reloader=False)
# player.py

from flask import Blueprint, request

players = Blueprint("players", __name__)

@players.route("/players")
@players.route("/players/create", methods=["POST"])
def createPlayer():
    print(request.json.get("playerName"))
    return "Hello", 200

Web Socket

Since I wanted the server to be able to handle multiple rooms, I had to be able to group client web socket connections into rooms for each unique game. The Flask Socket.io library includes the implementation of rooms which allow the server to group client connections into separate broadcast domains. 

Using the same app.py from the API sections will host a web socket server using flask.

The following code block will show an example of how clients get placed and removed from rooms. When a player broadcasts joinGame to the global room, the server will take the client payload, which includes the RoomID that was given to them from the API. The on_join function then calls join_room from the Flask library to place that client connection in that RoomID. Finally the server broadcasts the update to the clients room using socketio.emit and the to=<room> argument.

@socketio.on("joinGame")
def on_join(data):
    print("Player is joining:")
    print(data)
    PlayerID = data["PlayerID"]
    RoomID = data["RoomID"]
    join_room(RoomID)
    socketio.emit("sendMsgToClient", PlayerID + " has entered the room.", to=RoomID)

@socketio.on("leaveGame")
def on_leave(data):
    print("Player is leaving:")
    print(data)
    PlayerID = data["PlayerID"]
    RoomID = data["RoomID"]
    leave_room(RoomID)
    socketio.emit("sendMsgToClient", PlayerID + " has left the room.", to=RoomID)

Game Logic

Each game in session will have its own game loop. Each tick will update player positions as well as game conditions, to make the game consistent, each tick will have to take the same time as the others. To achieve this, I keep track of the start time, and calculate the sleep time needed for consistent ticks at the end of the game loop.

def gameLoop(game):
    ticks = 0
    secondCounter = 0
    while game.Status:
        startTimeSeconds = time.time()
        ticks += 1

        tick(game) # Updates everything

        endTimeSeconds = time.time()
        sleepTime = (1 / tickRate) - (endTimeSeconds - startTimeSeconds)
        # print(sleepTime)
        if sleepTime > 0:
            time.sleep(sleepTime)

        secondCounter = secondCounter + time.time() - startTimeSeconds
        if secondCounter > 1:
            print("tps: " + str(ticks))
            secondCounter -= 1
            ticks = 0

        game.deltaTime = time.time() - startTimeSeconds

The updates done by the tick can be seen below.

def tick(game):
    game.counter += 1

    # stuff
    # print("Tick")
    # print("Current Players:")
    # print(*game.Entities, sep='\n')

    # print(len(game.Entities))
    if len(game.Entities) == 0:
        game.Status = False
        print("Room: " + game.RoomID + " empty, ending game")

    for entity in game.Entities:
        entity.updatePos(game.deltaTime, mapSizeX, mapSizeY)

    if game.counter == foodSpawnFrequency:
        food = Entity(
            -1,
            True,
            Vec2(
                random.randrange(-int(mapSizeX), int(mapSizeX)),
                random.randrange(-int(mapSizeY), int(mapSizeY)),
            ),
            Vec2(0, 0),
            Vec2(0, 0),
            defaultFoodSize,
        )
        game.Entities.append(food)
        game.counter = 0

    # if (bruh):
    #     food = Entity(-1, True, False, 0, 1, 0, 0, defaultFoodSize)
    #     game.Entities.append(food)
    #     bruh = False

    # Check Collisions, broadcast to players, players will know state of player, then remove dead
    game.checkCollisons()
    update = json.dumps([player.toDict() for player in game.Entities])
    socketio.emit("sendUpdateToClient", update, to=game.RoomID)
    game.removeEntities()

To expand on the updates each tick, we'll have to define two classes, the Entity and the Room. Since Entities have to be able to move around, we can model their movement using a forces.

Python doesn't seem to have a built-in vector class, so I created one for convenience. This will greatly help with movement of Entities.

class Vec2:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"x: {self.x}, y: {self.y}"

    def add(self, other):
        v = Vec2(self.x, self.y)
        return v.addi(other)

    def addi(self, other):
        self.x += other.x
        self.y += other.y
        return self

    def sub(self, other):
        v = Vec2(self.x, self.y)
        return v.subi(other)

    def subi(self, other):
        self.x -= other.x
        self.y -= other.y
        return self

    def mul(self, b):
        v = Vec2(self.x, self.y)
        return v.muli(b)

    def muli(self, b):
        self.x *= b
        self.y *= b
        return self

    def div(self, b):
        v = Vec2(self.x, self.y)
        return v.divi(b)

    def divi(self, b):
        self.x /= b
        self.y /= b
        return self

    def magsq(self):
        return self.x**2 + self.y**2

    def mag(self):
        return math.sqrt(self.magsq())

    def norm(self):
        self.divi(self.mag())

The movement of the players mouse position, which is given by the client, will act as a force to move the player around the map. We limit the speed with "friction" which was cheaply done by exponentially decreasing the players velocity each tick. The impulse was added in when I was implementing the split mechanism, however it is not complete yet. The update movement method also keeps the player inside the map bounds by doing checks every tick.

class Entity:
    def __init__(self, PlayerID, state, position, velocity, inputVec, size):
        self.PlayerID = PlayerID
        self.state = state
        self.position = position
        self.velocity = velocity
        self.inputVec = inputVec
        self.size = size  # Diameter

    def __str__(self):
        return f"PlayerID: {self.PlayerID} state: {self.state} pos: {self.position} vel: {self.velocity} inVec: {self.inputVec} size: {self.size}"

    def toDict(self):
        return {
            "PlayerID": self.PlayerID,
            "state": self.state,
            "xpos": self.position.x,
            "ypos": self.position.y,
            "size": self.size,
        }

    def applyForce(self, force, deltaTime):
        self.velocity.addi(force.div(self.size).mul(deltaTime))

    def applyImpulse(self, force):
        self.velocity.addi(force.div(self.size))

    def updatePos(self, deltaTime, mapSizeX, mapSizeY):
        if self.state:
            self.applyForce(self.inputVec.mul(5000), deltaTime)

            self.velocity.muli(0.98)

            self.position.addi(self.velocity.mul(deltaTime))

            if self.position.x > mapSizeX:
                self.position.x = mapSizeX
                self.velocity.x = 0

            if self.position.x < -mapSizeX:
                self.position.x = -mapSizeX
                self.velocity.x = 0

            if self.position.y > mapSizeY:
                self.position.y = mapSizeY
                self.velocity.y = 0

            if self.position.y < -mapSizeY:
                self.position.y = -mapSizeY
                self.velocity.y = 0

    def updateSize(self, delta):
        self.size += delta

The Room class will handle the interactions between players.

class Room:
    def __init__(self, RoomID, Status, Entities):
        self.RoomID = RoomID
        self.Status = Status
        self.Entities = Entities

    def removeEntities(self):
        for i in range(len(self.Entities) - 1, -1, -1):
            if self.Entities[i].state == False:
                print(
                    "Player: "
                    + str(self.Entities[i].PlayerID)
                    + " is dead, removing from player list"
                )
                self.Entities.pop(i)

    def checkCollisons(self):
        deaths = [-1] * len(self.Entities)
        for i in range(0, len(self.Entities)):
            for j in range(i + 1, len(self.Entities)):
                # print("Loop check collisions")

                if self.Entities[i].state and self.Entities[j].state:
                    e1Radius = self.Entities[i].size / 2
                    e2Radius = self.Entities[j].size / 2
                    radiiDistance = math.sqrt(
                        math.pow(self.Entities[i].xpos - self.Entities[j].xpos, 2)
                        + math.pow(self.Entities[i].ypos - self.Entities[j].ypos, 2)
                    )
                    if radiiDistance <= 1e-9:
                        if e1Radius > e2Radius:
                            deaths[j] = i
                        else:
                            deaths[i] = j
                        continue
                    x = (
                        math.pow(e1Radius, 2)
                        - math.pow(e2Radius, 2)
                        + math.pow(radiiDistance, 2)
                    ) / (2 * radiiDistance)
                    # print("x value " + str(x))
                    y = math.pow(e1Radius, 2) - math.pow(x, 2)
                    if y > 0:
                        y = math.sqrt(y)
                        # print("y value " + str(y))
                        theta1 = 2 * math.asin(y / e1Radius)
                        # print("theta1 " + str(theta1))
                        area1 = (math.pow(e1Radius, 2) / 2) * (
                            theta1 - math.sin(theta1)
                        )
                        theta2 = 2 * math.asin(y / e2Radius)
                        area2 = (math.pow(e2Radius, 2) / 2) * (
                            theta2 - math.sin(theta2)
                        )
                        area = area1 + area2
                        # print("area 1 " + str(area1))
                        # print("area 2 " + str(area2))
                        # print("Total area " + str(area))
                        if e1Radius > e2Radius:
                            if area >= (math.pow(e2Radius, 2) * math.pi) / 2:
                                deaths[j] = i
                        else:
                            if area >= (math.pow(e1Radius, 2) * math.pi) / 2:
                                deaths[i] = j
                    else:
                        if radiiDistance < max(e1Radius, e2Radius):
                            if e1Radius > e2Radius:
                                deaths[j] = i
                            else:
                                deaths[i] = j
        # print(deaths)
        for i in range(0, len(deaths)):
            if deaths[i] != -1:
                self.Entities[deaths[i]].size = (
                    self.Entities[deaths[i]].size + self.Entities[i].size
                )

        for i in range(len(deaths) - 1, -1, -1):
            if deaths[i] != -1:
                print(str(self.Entities[i].PlayerID) + " has died, updating state")
                self.Entities[i].state = False
                print(self.Entities[i])

    def __str__(self):
        temp = f"RoomID: {self.RoomID} Status: {self.Status}"
        for entity in self.Entities:
            temp = temp + "\n" + str(entity)
        return temp

Client Setup

The HTML and CSS are also included in the repository, Most of it was poorly put together.

API Calls

The server was designed to handle the creation of player and rooms. In hindsight, I think that the API routes were not needed as the transfer of this data could also be handled by the WebSocket. The JavaScript code below sends the player name to the server using a fetch request asynchronously. The server will send back a PlayerID upon success, which causes the JS promise to resolve which leads to the global PlayerID variable to be set. The rest of the requests act similarly and are of the same structure.

const serverIP = "http://127.0.0.1:5000";

function createPlayer() {
  PlayerName = document.getElementById("input_create_player").value;
  clearInputs();

  var myHeaders = new Headers();
  myHeaders.append("Content-Type", "application/json");

  var raw = JSON.stringify({
    playerName: PlayerName,
  });

  var requestOptions = {
    method: "POST",
    headers: myHeaders,
    body: raw,
    redirect: "follow",
  };

  fetch(serverIP + "/players/create", requestOptions)
    .then((response) => {
      // console.log(response.status);
      if (!response.ok) {
        throw new Error("Player Creation Failed");
      }
      return response.text();
    })
    .then((result) => {
      console.log("Created player with ID: " + result);
      PlayerID = result;
      document.getElementById("div_player").style.display = "none";
      document.getElementById("div_menu").style.display = "flex";
    })
    .catch((error) => console.log("error", error));
}

WebSocket

The concept of rooms in Socket.io was explained earlier in the server section. Since the server

var socket = io.connect(serverIP);

socket.on("sendUpdateToClient", function (msg) {
  data = JSON.parse(msg);

  Players = data;

  for (let i = 0; i < data.length; i++) {
    if (data[i]["PlayerID"] == PlayerID) {
      if (!data[i]["state"]) {
        PlayerStatus = false;
      }
    }
  }
});

 

Mouse Tracking

P5.js Rendering