I'm making a game on Playdate in pure C. Chapter 3

Chapter 1

Chapter 2

I'm writing a game for the Playdate game console in pure C. A survival game like Vampire Survivors. Since pure C lacks many modern object-oriented conveniences, I have to dodge in every possible way to adapt my ideas into code. In these notes you will learn first-hand how a game is created from scratch from idea to publication.

In the last chapter, I described how I initialize the scene, how I clear the resources, showed how I fill the scene with props, and even experimented with generating these very props. In this chapter I will tell you how the most important function works GameUpdatein particular, input processing and data processing.

GameUpdate This is a callback function that is called every tick. This means its task is to implement the holy trinity of any game:

  1. read player input

  2. update game state

  3. draw the updated game state.

If you have ever been on an interview with a game development office, then there is a 90% chance that you were asked about these three steps. Also, if you've ever written code for Arduino, then you probably remember two functions that should always be there: setup And loop. Here GameUpdate it's just analogous loop.

On the stage there is a car that moves when you press the cross, and there are props: cacti, sand mounds and tumbleweeds. The tumbleweed moves just like in life. That is, it changes its position horizontally (along the X axis) and also jumps up and down, as if bouncing off the ground. To realize movement we need to master time. To do this, for each tick we need to know exactly how much time has passed since the last tick. Out of the box this parameter in the event Update no, but we can calculate this parameter using the API function playdateApi->system->getElapsedTime();. This function returns the number of seconds that have passed since the game started. It's not a difference in ticks, but it's something. For the time difference in ticks, you also need to know the value obtained from the same function in the last tick. Because in the structure Game there is a field float previousElapsedTime;. At the end of the function GameUpdate we save the call result in this field getElapsedTimeand at the beginning GameUpdate we subtract the difference between the current value getElapsedTime And previousElapsedTime. This meaning is the one dt, which is equal to the number of seconds that have passed since the last tick. Since at the start of the game in the file main.c in the first chapter I set FPS to 30, then on average dt I have it equal to 0.033 seconds.

Start of GameUpdate feature

Start of GameUpdate feature

Next, we process the input – collect the values ​​of the pressed buttons and update the data depending on them.

Input processing

Input processing

PDButtons this is the bitmask declared in the Playdate SDK. Bit masks in Sishka are implemented either as enumor just like int unlike Swift, where a bitmask is a completely different special class of data.

Description of the PDButtons bitmask

Description of the PDButtons bitmask

Bit mask PDButtons contains a list of pressed and unpressed console buttons.

Also, perhaps you have a question: what kind of function is this? PlayerVehicleAngleCreateFromButtons on line 72. This is a way to determine one of the eight directions of the machine with the device buttons pressed:

Implementation of the PlayerVehicleAngleCreateFromButtons function

Implementation of the PlayerVehicleAngleCreateFromButtons function

Why is the parameter needed? oldValue in it? The point is that we need to return something even if no button is pressed. What to return if no button is pressed? Which direction? In Swift/C++/C# I would return a nullable value (Optional in Swift, std::optional in C++ and Nullablein C#), but in China it’s not so convenient because there are no generics/templates, so I decided to pass the old direction value because in the case when no button is pressed, the direction of the machine simply does not change. This is logical because in life, if you don’t touch the steering wheel, the direction of the car doesn’t change either. That’s why we pass the old value and return it when in Swift/C++/C# we would return null. If I worked in a corporation with development meetings, retrospectives, effective managers, team building and code reviews, then a reviewer would definitely appear and tell me that the argument oldValueif you look at the situation from a certain angle, it transfers the logic of how the car moves inside the function PlayerVehicleAngleCreateFromButtonsand this is wrong because if you follow SOLID, strive to write perfect code, brush your teeth morning and evening, go to yoga, participate in city marathons, give up meat, gluten, milk, sugar, salt, MSG and Coca-Cola, then this function should be solely responsible for creating the enumeration instance PlayerVehicleAngle and nothing more, and the logic of conveying the old meaning is mandatory, straight out of the nose, there is no will to be seen for a century, without conversations, discussions and negotiations, it must be outside the function PlayerVehicleAngleCreateFromButtons because purely theoretically, we can use this function not only for a car, but for something else that also has 8 directions, but if nothing is pressed, the direction will, for example, be reset to the top. And the reviewer doesn’t care that this will happen around the time the cancer whistles on the mountain, on Thursday after the rain and exactly the next second after the second coming.

If we put aside the irony and formulate the answer for a boring imaginary reviewer, then it (the answer) will be like this: meaning oldValue This is an excellent approach to implementing code, very similar to constructing electrical circuits. The value is like a current flowing through a circuit through a function, and under certain conditions it can change at the output, or it can remain the same. In general, code in the style of electrical circuits is popular in China, but not so popular in object-oriented languages. I, of course, do not encourage everyone to write in this particular paradigm, but I answer for myself in this way.

Phew. Further. There is another function GameAnyArrowIsPressed. It returns 1 if at least one button on the D-pad is pressed and 0 otherwise:

Implementation of the GameAnyArrowIsPressed function (perhaps the & operators should have been placed on separate lines to make the code more beautiful)

Shtosh, we have come to the next incredibly important step of our vital teak – processing tumbleweeds.

Processing tumbleweeds in teak

Processing tumbleweeds in teak

Constanttionthat screenSize It's not needed yet – it will come in handy later. Next, we go through the array using the old proven method: we get the number of objects in it and describe the cycle for. Having received another tumbleweed object on line 118, I’m ready to change it (because the pointer to Tumbleweed non-const). I stretch my hands and tell myself “do it beautifully!” First of all, we process the position because the tumbleweed rolls horizontally across the field. Every tick the moving object shifts (that's a rotation!), which means we have to do some simple manipulations with the position. This requires basic knowledge of the mechanics section of physics (the one about “speed equals distance traveled divided by time”, and I also adore the word “put”, which I picked up in Belarus when I lived there for three years. I try to say “put” everywhere instead of the word “the way” and I recommend that you do the same, as this will only make the Russian language more beautiful!). To better understand how the process of movement occurs in the code, first of all, we need to understand what we ultimately need to do per tick. Per tick we need to change the position of each tumbleweed object. More precisely, to understand how much the position of the tumbleweed object has changed relative to the old position per tick. This change is stored in a constant dTumbleweedPositionwhich is created on line 121. It is calculated very simply: the speed of the tumbleweed is multiplied by dt, that is, we multiply the speed by the elapsed time per tick. And then a change in position dTumbleweedPosition it is simply added to the position of the same tumbleweed.

In a similar way, movement works for everything everywhere, not only in my game, but in all games and not only games – all sorts of smoothly moving buttons in the user interface, a jumping loading icon in the Apple Safari browser, a pop-up window of Avast antivirus, falling push notifications on iOS and much more that can be listed here before midnight.

Okay, we've sorted out the movement. Let's move on. And then we process the jump. The fact is that tumbleweeds bounce as they move. This means that in our world, which we create with our thoughts and code, we need to program similar tumbleweed jumps and it is desirable that the result looks believable, and not clumsy like the animation in Heroes 4. But how can we make the jumps so that they look quite believable? Just linear like movement? But this will suck because in reality, any vertical movement involves the acceleration of gravity, and this makes the motion function quadratic, which means linear motion will not work. The function needs to be exactly quadratic, that is, the argument in it must be squared at least in one place. The most common thing is a parabola. It is the most suitable here because in real life everything falls along a parabola (of course, if you ignore the wind and in general if you experiment on spherical chickens in a vacuum). But if the tumbleweed lazily flies towards the ground in a parabola, then when it collides with the ground, I will need to have the logic of this very collision implemented to rebound. Here I appreciated and weighed it just like Action Man (remember this superhero? I adored the cartoon “Action Man” as a child, and I especially liked its three-dimensional computer drawing. Then it seemed to me that this was the best graphics in the world. Recently I decided to rewatch This cartoon was amazing because of how terrible the graphics actually are! RDR2 spoiled me! In general, at the climax of each episode, he said “evaluate, weigh”, calculated his movements to the smallest precision, and in the next 10 seconds he bent over. all enemies with an ult) and decided to make it simpler: I use the equation of a circle, more precisely, the equation of a cosine (or sunis, if you like, because a sine graph is a cosine graph shifted by 90 degrees).

Sine graph in person y = sin(x)

Sine graph in person y = sin(x)

But for our purposes, we’ll modernize the sine graph a little – we’ll put it in the module. Sticking any function into a module does an interesting trick to its visual display – displaying the bottom half up like an axis x turned into a mirror.

Graph of the modulus of sine y = abs(sin(x))

Graph of the modulus of sine y = abs(sin(x))

To adapt this mathematical trick into code, we need each tumbleweed object to have a jump “turn” value, as well as the speed of this turn (by how many radians the turn value will change in 1 second). Why the turn? Because the sine graph takes direction as a variable. To further understand this, you can look at this as a phase that is spinning. So the Tumbleweed structure has fields jumpVelocity And jumpAngle. On line 125 we calculate the value dTumbleweedJumpAngleequal to the number of radians by which it changed jumpAngleon line 126 we add this value to jumpAngleand on line 127 we normalize jumpAngle. Normalizing directions is a thing that you should sometimes do if you work with directions – kind of like picking up cat poop if you live with a cat (or she lives with you, lol). Since the direction value is cyclic (0 radians and 2π radian is the same value, for example), for the purity of the code, conscience and credit history, after operations on the direction, you can bring it into the range[0;2π)[0;2π) if suddenly this direction goes beyond the limits (if a cat poops past the tray, you need to wipe everything off, because the cat itself is unlikely to do this).

Implementation of the normalizeAngle function

Implementation of the normalizeAngle function

In general, if we had C++, I would probably put normalization right inside the class Angle into an assignment operator that can be freely overloaded. Or maybe not—implicitness sometimes makes the code worse. Be that as it may, this is how we process tumbleweed jumps.

So, we have figured out the processing of the position of tumbleweeds and jumps (in fact, processing jumps is only half the battle, we also need to draw them kosher, and I will show this later), all that remains is to process the frame. Yes, the tumbleweeds in my game have several frames for beauty. I did this because otherwise if the tumbleweed had one frame it would look like shit. And I don’t want my toy to look like shit. Here for frame processing I go to the structure Tumbleweedadded a field frameIndex . In general, many things in the game will have such a field and similar logic. Well, the speed of change frameIndexthere is also: this field frameIndexVelocity. Yes, every object has this field Tumbleweed , although it has the same meaning for all objects. It would be possible not to add this field because it seems to be redundant, but let it be – what if I decide to make the speed different for different tumbleweed instances (and I had such thoughts at the time of writing the code), and saving memory on matches is the way to a madhouse. A total of 4 frames were taken near the tumbleweed. In one of the previous chapters you saw a constant TumbleweedSpritesCount = 4 – this is about it. frameIndexis a floating point number that varies in a range [0;4)[0;4) at the speed indicated in frameIndexVelocity . The logic in lines 130 – 134 does just that.

This is how tumbleweed processing works. What do you think? It bothers me personally. Go ahead.

Sometimes you need to create tumbleweeds, and not just filter them. To do this, you need to decide by what logic it will be created. When I was playing Minecraft hard, I often read the wiki on it. And the Minecraft wiki explained how various entities spawn. And the logic of spawning is something like this: there is a one in ten thousand chance that an entity will spawn in a particular tick. I decided to introduce this same logic because it is simple and understandable.

Making Tumbleweeds

Making Tumbleweeds

Line 139 tells us that there is a 1 in 100 chance (tumbleweedSpawnChangePercentageis equal to 1) a new tumbleweed will be created in the tick. On line 154, a percati-field instance is created with the function TumbleweedCreateand on the next line this instance is sent (actually copied) to the array game>tumbleweeds.

To create a tumbleweed we need 4 arguments: position on the map, movement speed, bouncing speed and frame change speed. The position on the map is calculated in a super cunning way – a new tumbleweed simply appears exactly outside the left or right border of the screen. And it drives horizontally towards the player’s car. You can, of course, spawn “fairly” at a random point on a fairly large playing field, but then the player will simply rarely see tumbleweeds, especially at the beginning of the game, and this worsens the user experience. The bouncing speed is the number of radians passed per second for the value from which we calculate the sine graph which I previously showed. And you already know about the speed of frame change: a tumbleweed has 4 frames, as I said earlier, and they need to be changed at a certain speed.

Next, on line 158, the camera offset is assigned to the position of the car so that the car is always in the center of the screen no matter where it goes. And on line 160 the function is called GameDrawwhich draws the entire farce I’m describing here so that the player can see what’s happening, otherwise why all this?

We'll look at rendering in the next chapter, but thank you for reading. If you like the way I write and want to support me with some money, then I’m on patreon And busty.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *