Sea, pirates - 3D online game in the browser

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



There are a lot of browser-based 2D mini-games behind, but such a project is new to me. In gamedev, solving problems that you 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 let's not waste time and start developing!





The game in a nutshell



Survival Fight is the only game mode at the moment. Battles from 2 to 6 ships without rebirth, where the last surviving player is considered the winner and receives x3 points and gold.



Arcade controls : buttons W, A, D or arrows to move, space bar to fire at enemy ships. You don't need to aim, you can't miss, the damage depends on the randomness and the angle of the shot. Greater damage is accompanied by a "right on target" medal.



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



Spending goldsetting black sails on your ship for 24 hours. The plans to add the ability to wake the Kraken, who will carry off the bottom of any enemy ship its giant tentacles :)



PVP or zassal coward? A feature that I wanted to implement even before choosing a pirate theme is the ability to fight with friends in a couple of clicks. Without registration and unnecessary gestures, you can send an invitation link to your friends and wait until they enter the game using the link: a private room that can be opened for everyone is created automatically when someone follows the link, provided that the "author" of the link has not started another battle.



Technology stack



Three.js is one of the most popular libraries for working with 3D in the browser with good documentation and many different examples. Also, I've 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 anyway :)



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



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



PHP and MySQLless obvious choice, but the criterion is still the same - quickly and easily, since there is experience in these technologies.



It turns out like this:







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



I didn't want to link the game server to the database at all, so everything goes through PHP. In my opinion, there are pluses here, although I'm not sure if they are significant. For example, since ready-made data in the required form comes to Node.js, Node.js does not waste time processing and additional queries in the database, but deals with more important things - it “digests” the actions of players and changes the state of the game world in the rooms.



Model first



Development began with a simple and most important thing - a certain model of the game world, describing sea battles from a server point of view. Plain canvas 2D is ideal for schematic display of the model on the screen.







Initially, I set the normal "verlet" physics, and took into account the different resistance to the movement of the ship in different directions relative to the direction of the hull. But worrying about server performance, I replaced the normal physics with the simplest one, where the outlines of the ship remained only in the visual, but physically the ships are round objects that do not even have inertia. Instead of inertia, there is limited forward acceleration.



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



The client-side JavaScript for rendering the game world model, handling the movement of ships and shots, I ported to the Node.js server 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 helper functions and a class that implements a doubly linked list


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



The current list of game server classes:



  • WaitRoom - the room where players are waiting for the start of the battle, it has its own tick method that sends its updates and starts the creation of the game room when more than half of the players are ready for battle
  • Room — , : /, ,
  • Player — «» :
  • Ship — : , , ,
  • PhysicsEngine — ,
  • PhysicsBody —


Room
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 functions such as getting user data, updating a daily task, getting a reward, buying a skin. These functions basically send https requests to PHP, which executes one or more MySQL queries and returns the result.



Network delays



Network latency compensation is an important part of online game development. On this topic, I have repeatedly re-read a series of articles here on Habré . In the case of a battle of sailing ships, lag compensation can be simple, but you still have to make compromises.



Interpolation is constantly carried out on the client - the calculation of the state of the game world between two moments in time, the data for which has already been obtained. There is a small margin of time, which 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 is implemented on the server, 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 exact correspondence of the picture to the state of the game world at a certain point in time. The only exception is that a cannon salvo is played immediately at the push of a button. The rest can be attributed to the laws of the universe and the surplus of rum from the ship's crew :)



Front-end



Unfortunately, there is no clear structure or hierarchy of classes and methods. All JS is split into objects with their own functions, which in a sense are equal. Almost all of my previous projects were more logical than this one. This is partly because the first goal was to debug the game world model on the server and network interaction without paying attention to the interface and visual component of the game. When it came time 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 in an amicable way it was worth revising the entire structure and preparing the basis for future changes.



3D ship



Three.js supports the use of ready-made 3D models in various formats. I chose GLTF / GLB format for myself, where textures and animations can be embedded, i.e. the developer shouldn't be wondering "have all the textures loaded?"



I have never dealt with 3D editors before. The logical step was to contact a specialist on a freelance exchange with the task of creating a 3D model of a sailing ship with an embedded animation of a cannon salvo. But I could not resist small changes in the finished specialist model on my own, and ended up with the fact that I created my model from scratch in Blender. To create 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 :).







Shaders to the god of shaders



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



The mechanism or method that I used when creating a particle system for animating damage to a ship, a dynamic water surface or a static seabed is the same: the special 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 a similar 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);




Ship damage



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







Sea



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



To diversify the colors on the water and make the game more interesting and pleasing to the eye, it was decided to add the bottom and islands. The bottom color depends on 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 a graphical editor (in my case, the tools were render -> clouds and Gaussian blur), then islands were added in random order using Canvas JS online on jsFiddle drawing a circle and blurring. Some islands are low, through them you can shoot at opponents, others have a certain height, shots do not pass through them. In addition to the height map itself, at the output I receive data in json format about the islands (their position and size) for physics on the server.







What's next?



There are many plans for the development of the game. The major ones are new game modes. Smaller ones - come up with shadows / reflections on the water, taking into account the performance limitations of WebGL and JS. I have already mentioned the opportunity to wake up the Kraken :) The unification of players into rooms based on their accumulated 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 "into memory" and then combining all the data in one picture (in fact, it can be called post-processing), but my hand does not rise to increase the load on the client in this way, because the client is still a browser rather than a native app. Perhaps one day I will decide on this step.



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



Easter egg



Who doesn't like to remember old computer games that gave so many emotions? I love replaying the game Corsairs 2 (aka Sea Dogs 2) over and over again so far. I could not help but add a secret to my game and explicitly and indirectly reminiscent of "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 while exploring the sea (you don't need to sail far across the endless sea, the object is within reason, but still the probability of finding it is not high). The Easter egg completely repairs the damaged ship.



What happened



Minute video (test from 2 devices):





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



There is also a contact form, messages are sent to me in telegrams: https://sailfire.pw/feedback/

Links for those wishing to keep abreast of news and updates: VK Public , Telegram channel



All Articles