I welcome users of Habr and casual readers. This is the story of the development of a browser-based online multiplayer game with low-poly 3D graphics and simple 2D physics.

Behind a lot of browser-based 2D mini-games, but a similar project is new to me. In gamedev, solving tasks that I have not yet encountered can be quite exciting and interesting. The main thing is not to get stuck with grinding parts and start a working game while there is a desire and motivation, so we will not waste time and proceed to development!

ITKarma picture

About the game in a nutshell

Survival Fight is the only game mode currently available. Battles from 2 to 6 ships without rebirths, where the last surviving player is considered the winner and gets x3 points and gold.

Arcade control : W, A, D buttons or arrows for movement, space for salvo on enemy ships. You do not need to aim, you cannot miss, the damage depends on the randomness and angle of the shot. Greater damage is accompanied by a “just on target” medal.

We earn gold taking first places in the ratings of players for 24 hours and 7 days (reset at 00:00 Moscow time) and completing daily tasks (one of three is issued for the day, in turn). There is also gold for the battles, but less.

We spend gold setting black sails on your ship for 24 hours. The plans include adding the ability to wake the Kraken, which will drag any enemy ship to the bottom with its giant tentacles :)

PVP or pissed off coward? A feature that I wanted to realize even before choosing the topic of pirates was the ability to fight friends in a couple of clicks. Without registration and unnecessary gestures, you can send an invitation link to friends and wait for them to enter the game using the link: a private room that can be opened for everyone is created automatically when someone clicks on the link, provided that the "author" of the link has not started another battle.

Technology stack

Three.js - one of the most popular libraries for working with 3D in a browser with good documentation and a lot of different examples. Also, I used Three.js before - the choice is obvious.

The lack of a game engine is due to the lack of relevant experience and the desire to learn something without which everything works well :) :)

Node.js because it’s simple, fast and convenient, although I had no experience directly in Node.js. I considered Java as an alternative, conducted a couple of local experiments, including with web sockets, but I did not dare to find out whether it was difficult to run Java on VPS. Another option - Go, its syntax drives me discouraged - has not advanced one iota in its study.

For web sockets, use the ws module in Node.js.

PHP and MySQL is less obvious choice, but the criterion is the same - quick and easy, as there is experience in these technologies.

It turns out such a scheme:

ITKarma picture

PHP is needed primarily for giving web pages to the client and for rare AJAX requests, but for the most part the client still communicates with the Node.js game server on web sockets.

I didn’t really want to connect the game server with the database, so everything goes through PHP. In my opinion, there are advantages here, although I’m not sure if they are significant. For example, since ready-made data arrives in the required form in Node.js, Node.js does not waste time processing and additional queries in the database, but is engaged in more important tasks - it “digests” the actions of players and changes the state of the game world in rooms.

Model first

Development began with the simplest and most important thing - a certain model of the game world that describes naval battles from a server point of view. Plain canvas 2D is perfect for sketching a model on the screen.

ITKarma picture

Initially, I set up the normal "physics" of physics, and took into account various resistance to the movement of the ship in different directions relative to the direction of the hull.But worrying about server performance, I replaced normal physics with simple physics, where the shape of the ship remained only in the visual, but physically the ships were round objects that did not even have inertia. Instead of inertia - limited acceleration of forward movement.

Shots and hits are reduced to simple operations with vectors of the direction of the ship and the direction of the shot. There are no shells. If the dot product of normalized vectors fits into the acceptable values ​​taking into account the distance to the target, then be shot and hit if the player pressed the button.

The client-side JavaScript for visualizing the model of the game world, processing ship movements and shots, I transferred the server to Node.js almost unchanged.

Game server

Node.js WebSocket server consists of only 3 scripts:

  • main.js - the main script that receives WS messages from players, creates rooms and makes the gears of this machine spin
  • room.js - a script responsible for the gameplay inside the room: updating the game world, sending updates to the players in the room
  • funcs.js - includes a class for working with vectors, a couple of auxiliary functions and a class that implements a doubly linked list

As the development progressed, new classes were added - almost all of them were directly related to the gameplay and ended up in the room.js. Sometimes it’s convenient to work with the classes separately (in separate files), but the all-in-one option is also good, while there are not too many classes (it’s convenient to scroll up and remember what parameters the method of another class takes).

Current list of game server classes:

  • WaitRoom - the room where players awaiting the start of the battle fall, there is its own tick method, which sends out its updates and starts creating a game room when more than half of the players are ready for battle
  • Room - a game room where battles take place: the status of players/ships is updated, then possible collisions are processed, at the end a message with up-to-date data is generated and sent to everyone
  • Player - essentially a “wrapper” with some additional properties and methods for the following class:
  • Ship - this class makes the ships sail: implements movement and turns, damage data is also stored here, so that at the end of the game points are awarded to players who participated in the destruction of the ship
  • PhysicsEngine - a class that implements the simplest collisions of round objects
  • PhysicsBody - everything is round objects with their coordinates on the map and radius

The game loop in the Room class looks something like this
let upd={p: [], t: this.gamet}; let t=Date.now(); let dt=t - this.lt; let nalive=0; for (let i in this.players) { this.players[i].tick(t, dt); } this.physics.run(dt); for (let i in this.players) { upd.p.push(this.players[i].getUpd()); } this.chronology.addLast(clone(upd)); if (this.chronology.n > 30) this.chronology.remFirst(); let updjson=JSON.stringify(upd); for (let i in this.players) { let pl=this.players[i]; if (pl.ship.health > 0) nalive++; if (pl.deadLeave) continue; pl.cl.ws.send(updjson); } this.lt=t; this.gamet += dt; if (nalive <= 1) return false; return true; 

In addition to classes, there are such functions as receiving user data, updating a daily task, receiving a reward, and buying a skin. These functions basically send https requests to PHP, which executes one or more MySQL requests and returns the result.

Network Delays

Network delay compensation is an important part of developing an online game. On this topic I have repeatedly read the series of articles here on Habr . In the event of a sailing ship battle, lag compensation can be simple, but you still have to compromise.

The client is constantly interpolating - calculating the state of the game world between two moments in time, the data for which has already been received. There is a small margin of time that reduces the likelihood of sudden jumps, and with significant network delays and the absence of new data, interpolation is replaced by extrapolation. Extrapolation gives not very correct results, but it is cheap for the processor and does not depend on how the movement of ships on the server is implemented, and of course, sometimes it can save the situation.

When solving the problem of lags, a lot depends on the game and its pace. I sacrifice a quick response to the player’s actions in favor of smooth animation and the exact correspondence of the picture to the state of the game world at a certain point in time. The only exception is that a salvo of cannons is played immediately at the touch of a button.The rest can be attributed to the laws of the universe and the excess of rum from the crew of the ship :)

Front End

Unfortunately, there is no clear structure or hierarchy of classes and methods. All JS is divided into objects with their own functions, which in a sense are peer-to-peer. Almost all of my previous projects were more logical than this. Partly it happened because at first the goal was to debug the model of the game world on the server and network interaction without paying attention to the interface and the visual component of the game. When the time came to add 3D, I literally added it to the existing test version, roughly speaking, I replaced the 2D drawShip function with exactly the same, but 3D, although it was worth it to reconsider the whole structure and prepare the basis for future changes.

3D ship

Three.js supports the use of ready-made 3D models of various formats. I chose the GLTF/GLB format for myself, where textures and animations can be sewn, i.e. the developer should not be wondering, “have all the textures loaded?”.

I have never dealt with 3D editors before. The logical step was to turn to a specialist at the freelance exchange with the task of creating a 3D model of a sailing ship with a wired animation of a volley of guns. But I could not resist the small changes in the finished specialist model on my own, but it ended up creating my model from scratch in Blender. Creating a low-poly model with almost no textures is simple, difficult, without a ready-made model from a specialist, to study in a 3D editor what is needed for a specific task (at least morally :).

ITKarma picture

Shaders to the god of shaders

The main reason I need my shaders is the ability to manipulate the geometry of the object on the video card during the rendering process, which is notable for good performance. Three.js not only allows you to create your own shaders, but can also take part of the work on yourself.

The mechanism or method that I used to create the particle system to animate damage to the ship, dynamic water surface or static seabed is the same: the special material ShaderMaterial provides a simplified interface for using its shader (its GLSL code), BufferGeometry allows you to create geometry from arbitrary data.

An empty blank, a code structure that was convenient for me to copy, supplement and modify to create my 3D object in this way:

Show code
let vs=` attribute vec4 color; varying vec4 vColor; void main(){ vColor=color; gl_Position=projectionMatrix * modelViewMatrix * vec4( position, 1.0 );//gl_PointSize=5.0;//for particles } `; let fs=` uniform float opacity; varying vec4 vColor; void main() { gl_FragColor=vec4(vColor.xyz, vColor.w * opacity); } `; let material=new THREE.ShaderMaterial( { uniforms: { opacity: {value: 0.5} }, vertexShader: vs, fragmentShader: fs, transparent: true }); let geometry=new THREE.BufferGeometry();//let indices=[]; let vertices=[]; let colors=[];/*... *///geometry.setIndex( indices ); geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) ); geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 4 ) ); let mesh=new THREE.Mesh(geometry, material); 

Damage to the ship

Animation of ship damage is moving particles that change their size and color, the behavior of which is determined by their attributes and the GLSL shader code. Particle generation (geometry and material) occurs in advance, then for each ship its own instance (Mesh) of damage particles is created (the geometry is common to all, the material is cloned). There were quite a lot of particle attributes, but the created shader implements both large, slowly moving dust clouds, quickly flying debris, and fire particles, the activity of which depends on the degree of damage to the ship.

ITKarma picture


The sea is also implemented using ShaderMaterial. Each vertex moves in all 3 directions along a sinusoid, forming random waves. Attributes determine the amplitudes for each direction of motion and the phase of the sine wave.

To diversify the colors on the water and make the game more interesting and more pleasing to the eye, it was decided to add a bottom and islands. The bottom color depends on the height/depth and shines through the water surface creating dark and light areas.

The seabed is created from a height map, which was created in 2 stages: first, the bottom without islands was created in the graphics editor (in my case, the tools were render - > clouds and Gaussian blur), then using Canvas JS online on jsFiddle we added randomly islands by drawing circles and blur.Some islands are low, you can shoot enemies through them, others have a certain height, shots do not pass through them. In addition to the height map itself, at the output I get data in json format about the islands (their position and sizes) for physics on the server.

ITKarma picture

What's next?

There are many plans for the development of the game. From large - new game modes. Smaller - come up with shadows/reflection on the water, taking into account the limitations of WebGL and JS performance. About the opportunity to wake up Kraken, I already mentioned :) The association of players into rooms based on their experience has not yet been implemented. An obvious, but not too high-priority improvement is to create several maps of the seabed and islands and choose one of them randomly for a new battle.

You can create a lot of visual effects by repeatedly drawing the scene “in memory” and then combining all the data in one picture (in fact it can be called post-processing), but my hand doesn’t rise to increase the load on the client in this way, because the client is still a browser, not a native application. Maybe one day I will decide on this step.

There are also questions that I find it difficult to answer now: how many players online can withstand a cheap virtual server, whether it will be possible to collect at least some number of interested players, and how to do it.

Easter Egg

Who does not like to remember old computer games that gave so many emotions? I like to replay the game Corsairs 2 (aka Sea Dogs 2) again and again until now. I could not help but add to my game a secret that clearly and indirectly recalls Corsairs 2. I will not reveal all the cards, but I will give a hint: my easter egg is a certain object that you can find when exploring the open sea (you don’t need to swim far across the endless sea, the object is within reason, but still the probability of finding it is not high). Easter Egg completely repairs a damaged ship.

What happened

Minute video (test from 2 devices):

Link to the game: https://sailfire.pw

There is also a contact form, messages fly to me in telegrams: https://sailfire.pw/feedback/
Links for those wishing to keep abreast of news and updates: Public VK , Telegram channel .