2023 is over, and as a farewell to the year and to the game creation project that took a majority of my time this year, I feel like I have no other choice than to tell you everything. About how every single frame of the game works. About how raw nerd text like this can evolve into something like this in just 16.67 milliseconds.
The game should run at 60 frames per second, in order to look smooth and nice, which means a new image has to be shown on the screen every 16.67 milliseconds. This creates the animation you see when you play a game, which you may not think of as an "animation", but it is. And to draw the images of this animation we have to calculate how they will look based on what the player does in the game's world. So basically, a bunch of things gets done in a particular order, 60 times every second, and it's these things:
First, we save a time value for later (it will be used to check if we've actually reached the desired 60FPS or not):
let frame_start = performance.now(); // these small code snippets are from this file
Then, we check if the player is outside the world ("out of bounds"), and if so we put him back into the world:
if (player.position.x <= 26*49) { player.position.x = 26*49; sound_error.play(); }
if (player.position.x >= 41*49+49) { player.position.x = 41*49+49; sound_error.play(); }
if (player.position.z <= 26*49) { player.position.z = 26*49; sound_error.play(); }
if (player.position.z >= 41*49+49) { player.position.z = 41*49+49; sound_error.play(); }
Then we set the game's layout. Which means placing the buttons, a dialogue box and some other things at their correct places on the screen and set their sizes, depending on the user's device and screen orientation. Due to performance reasons this one only runs every 10th frame (if we aren't in a cutscene). There are 3 layout modes: Portrait, landscape for mobile, and landscape for desktop. The pause menu is also controlled here.
layout_set();
Then, we place
NPC characters and items at their respective positions in the world, and make them visible or invisible depending on how close they are to the player. We also do weather related things, like making it snow in a few areas, and play the correct music/atmosphere sound for each area.
chunk_set();
some characters and items
Then we run all the story events and dialogues. Racing functionality is also in here. This one deserves its own text, so I won't even bother trying to explain it:
cut_set();
After that we set the camera's position. Normally, the camera follows the player in a third-person perspective as he moves, but if we're in a cutscene the camera instead looks at whichever character is talking currently, and in the pause menu / intro splash screen there's a special setting:
camera_set();
the splash screen and the pause menu uses this kind of camera setting
Then, the closest terrain is made visible, and the rest invisible, so we don't draw unnecessary terrain to the screen (which is expensive for the computer). The 3D terrain of other areas get dynamically created as you play, so we don't have to create the whole 3D world at the game's start which would take too long time.
Here's a somewhat dated description of the terrain creation. Then we place the sun
1 at its correct height based on the in-game time of day, the water gets its position (it follows the player) and changes its height based on the time of day (tidewater!). We also place the skybox and clouds in a similar manner and lastly we animate the water and the clouds to create an illusion of a living world (fact: animate means "bring to life").
ascend_main();
sunset
We calculate how strong the "sunlight" should be and which color it should have, based on the time of day (this may come as a shock, but: It's brighter during the day, and darker during the night). The color change is basically: The sunsettier, the red:er.
light_set();
tidewater (see the difference?)
We add fog, or not, depending on where you are in the game. The fog's intensity (or really, it's
distance) and color get calculated based on where you are and on the time of day (there's more fog in the morning). I had to make a function that makes the fog fade in and out as you move between an area with fog and an area without. Making these fade-ins and -outs between two areas with differently colored fog was too hard, so all fog areas always have at least one small area without fog in between them.
fog_set();
blue fog
And NOW, we save the player's progress in 3 browser cookies (two for where you are in the world and one for where you are in the story). For some reason this takes much computer energy, so this work is divided into parts and spread out over different frames.
if (frame_counter % 30 === 0) document.cookie = "cookie_cut=" + cut + "; expires=Thu, 18 Dec 2099 12:00:00 UTC";
[...]
if (frame_counter % 30 === 5) document.cookie = "cookie_x=" + ci1 + "; expires=Thu, 18 Dec 2099 12:00:00 UTC";
if (frame_counter % 30 === 10) document.cookie = "cookie_z=" + cj1 + "; expires=Thu, 18 Dec 2099 12:00:00 UTC";
After that it's time for some car action. This function makes the player's and the
other characters' cars move, based on keyboard/touch input (for the player) and some AI code (for the other characters). In different parts of the story, different "goal positions" get set for the different characters. The characters always drive towards their respective goals, in a somewhat randomized direction, and when they arrive they stand still there. The randomized directions combined with good goal positions make the characters seem alive.
cars_control();
Then we calculate friction and gravity and let it affect the player's and the other characters' cars, to make them slow down in grass, slow down when driving uphill and fall down towards the ground. The player's car gets some extra treatment, for example it's also able to collide with houses and get rescued if it falls down into water. We also check for collisions between the player and other characters and play a "punch" sound if so.
cars_physics();
As the last piece of car action, we play the motor sounds of all cars. There are also specific "texture sounds" that get played depending on if you drive on the
ground or in
water. This one only runs every 5th frame because otherwise it creates lag.
cars_sound();
And finally, we draw the 3D graphics to the screen, based on all the information we've got for this frame:
renderer.render(scene, camera);
[...]
requestAnimationFrame(main);
But... we're not done yet! Remember the time value we saved in the beginning? Now we compare the current time to that first time value to see how long this whole frame took to run. It should have taken less than 16.67 seconds, but in reality that's not always the case. If the device you use is slow it may take longer time, and if that's the case, we turn on the "low resolution mode", which means: Motor sounds for other characters (not for the player) will be turned off, the screen's "pixel ratio" will be lowered (this means that the screen will have a lower, blurrier resolution), and less NPC characters, skyscrapers and trees will be drawn in the distance (for example, NPC characters will now turn invisible when they're 15 meters away from the player instead of from 35 meters). Small details that don't matter way too much and that make the game easier to run on worse devices.
2
if (frame_end-frame_start >= 16.67) lowres_count++;
else lowres_count -= 0.1;
if (frame_counter % (60*20) === 0) lowres_count = 0;
if (lowres_count >= 10) lowres = 0.75;
else if (lowres_count < -10) lowres = 1;
Another thing that gets done in the game's loop is checking for player input, and doing the corresponding actions based on that.
So, that's it! Or, that's a narrow selection of it, described in general terms. Yes, I said I was going to tell you everything, but I left out A LOT of things writing this. If you want to know everything, check out the game's "Behind the scenes" page.
Subscribe to Golden emails