Open a327ex opened 6 years ago
Love it. Thank you for the inspiration.
The mouse is a bit odd because there doesn't seem to be a way to get the state of the mouse through a >function
Er...it's called GetMouseState.
From the page it seemed to me like GetMouseState only returned a few results:
The current button state is returned as a button bitmask, which can be tested using the SDL_BUTTON(X) macros (where X is generally 1 for the left, 2 for middle, 3 for the right button)
So I assumed it didn't return mouse wheel state as well as other buttons. Maybe I was wrong and it does return everything. But now I already did it another way so... yea.
So I assumed it didn't return mouse wheel state as well as other buttons. Maybe I was wrong and it does return everything. But now I already did it another way so... yea.
Ah. Yeah, mouse wheels don't have a state, since they can turn infinitely far in either direction. So they're not really suited to that sort of interface. I see you're just keeping four flags for whether it moved up/down/left/right in the last frame or whatever. Which avoids the overflow problem, but throws away the information about how fast it's moving, which is often useful.
Nice article. A true programmer, building on top of the best libraries you can find, experimenting with code, not trying to design a 'future proof' architecture.
As you gain more experience in coding you set the bar higher for yourself, you won't accept the limitations of the existing solutions.
And finally you get fed up with layers of APIs that really only massage some data and call another layer.
I liked the link to the talk; it mentioned being a contrarian. You're becoming a contrarian. Not accepting the slow, hierarchical, way of developing code because that's the way that businesses do it; being subject to Conway's Law.
Keep coding.
Really like your coding philosophy, nice engine ! Focusing on the game first and the engine later is really the way to go. I know personally, that you can get lost making the ultimate engine for all games and never finish any game.
Btw: Do you plan on putting this aika engine in Github ?
(Só fui ver depois que você é BR ! Abraço!)
@rafaelvasco he actually posted it, but then removed it, cause as he said, he wants to finish it first, and then show off.
@egordorichev Ok. Fair enough. Thanks.
Fantastic read @adnzzzzZ, thanks. It looks like you've taken down your code though 😞.
Nice article @adnzzzzZ. Are you planning to upload the code again, because they are down.
@RednibCoding Not at the moment, sorry
Nice article @adnzzzzZ. Thanks for sharing. 👍 Will you share the code again? How soon?
That's awesome!. You know i'm thinking of making my own game engine but, i don't have the knowledge and i can't complete a SIMPLE ping-pong game.
Nice article @adnzzzzZ
btw: I'd buy the game 'BYTEPATH' last year on steam. It says support macOS, but I cann't even install it on macbook pro 2018 with macOS 10.14.6. Hope someday you will fix it.
Nice article, thank you!
Two months ago I wrote an article explaining why I'd write my own game engine this year, and in this post I'll explain how I did it. The source code for everything that will be talked about in this article is here.
Context
For context, I made my previous game using LÖVE. Because of this the main goal I have for making this engine is to gain control over the C part of the codebase without changing much of how the Lua part of it works.
And so I just decided to make it so that the engine's API is very very similar to LÖVE's in as many ways as possible. LÖVE has a very well defined and clean API, and I don't think I can come up with anything much better, so it just makes sense to copy it for the most part.
At a high level this looks like this:
What this means is that I can use a lot of code I wrote for my LÖVE games (libraries like this, this and this) and all the work I'll have to do is change the name of any
love.\*
calls to call the functions of my engine instead.For further context, I only plan on making 2D games with this engine and I don't really care that much about performance, since the next two games I'll make will be less performance intensive than the one I just made. And finally, I'm making this engine for myself. In this article I'll outline how I did everything and how these solutions work for me, but I'm not saying that everyone should do things like this.
Game Loop
The three main pieces of technology I used that affect what the game loop looks like is SDL2, SDL-gpu and luajit. Getting those libraries compiling and working in a C program is simple enough so I'm not going to spend any time on this, but you can see all the source code for that in this folder.
For the game loop itself, because SDL-gpu is built to work "on top" of SDL, I ended up using the recommended starter application for it instead of SDL's. According to this tutorial that looks like this:
Now because the goal is making this work somewhat like LÖVE, what I did next was to integrate Lua into this. LÖVE works by having a
main.lua
passed to the LÖVE executable. This file contains the definition of functionslove.load
,love.update
andlove.draw
, which contains code for loading resources and starting the game, updating the game every frame, and drawing the game every frame, respectively. The executable then takes these Lua-defined functions and hooks them in the correct place in its C++ code.What I did for my engine was the same, where the comments
Load game
,Update game
andDraw game
will be replaced by Lua functionsaika_load
,aika_update
andaika_draw
. This ends up looking like this:And so the same-ish piece of code it places into each slot. The first thing I did was that after creating the Lua VM we I also opened the file
aika.lua
. This is a file that is assumed to be on the same folder of the executable and that will be entirely responsible for the C/Lua interface of this engine, meaning that the definitions ofaika_load
,aika_update
,aika_draw
will go there.Then, for calling the Lua functions I simply follow the basic way in which the C/Lua API works. One interesting thing is that I also need to handle errors here. The way I managed to do it was to just pass a
traceback
function tolua_pcall
which gets called whenever an error occurs. This in turn calls a Lua function calledaika_error
, which is also defined inaika.lua
, and from there I can handle any errors that happen in Lua scripts however I want. For instance, for now I'm just printing the error to the console and then quitting the application:But in the future I can also do what LÖVE does, which is print the error to the screen instead, which makes the problem a bit more... visual. I can also have the error automatically be sent to a server, which would help a lot with getting accurate crash reports that were caused by any of my Lua scripts (which will be most errors since the way I'm making this engine is very "top-heavy" to the Lua side of things).
Now, for reference, this is what the
aika.lua
file would look like at this stage:Timer
With the structure of the game loop defined I can start focusing more on a few details. The first one is making it so that my game loop is like the 4th one described in this article. To achieve this we'll need two things out of our timer routines: how much time has passed since the game started, and how much time has passed since the last frame.
We can answer the second question by doing this inside the game's loop:
SDL_GetPerformanceCounter
returns a value which displays how much time has passed since some time in the past. Like the page says, the numbers returned are only useful when taken in reference to another number returned by the same function, which is what I'm doing here by calling it once every frame. Then I useSDL_GetPerformanceFrequency
to translate these values into actual seconds.Before the loop starts I initialize all timer values like this:
And then change the update function like this:
This gets me the game loop described in the article, which was the same method I used for my previous game which seemed to work well enough.
Input
Next, I focused on handling input. I wrote an Input library for LÖVE which has a cool API and so this is pretty much what I wanted to achieve for my engine. The main problem I had with the way LÖVE handled this was that they exposed input events through callbacks, which I didn't really like at all. I wanted to be able to do something like this:
Basically asking if an event happened, and then handling that event right there on the update function. This is the way most people do it I think too. And the way I managed to do this in LÖVE was to just keep the relevant state for the current and previous frame and then simply check the state of the keys:
If a key was down last frame but isn't on this one, then it means it was "released". If it was not down on the last frame but is in this one, then it means it was "pressed". And so this is what I did for my engine, but in C now:
To understand what the code above is doing let's focus on a single part of it, the keyboard. The keyboard states are composed of
input_current_keyboard_state
andinput_previous_keyboard_state
. Both arrays hold 512 spaces of the structure that represents a key. At the very start of the current frame, I copy the contentsinput_current_keyboard_state
toinput_previous_keyboard_state
, since at the start of the new frame this is true: the contents that were "current" are now "previous".After this, I update the current state of the keyboard with
SDL_GetKeyboardState
. And then I do the same process for the mouse and the gamepad. The mouse is a bit odd because there doesn't seem to be a way to get the state of the mouse through a function, like there is for the keyboard and gamepad, so I have to do it a bit differently inside theSDL_PollEvent
loop.In any case, after this is done for all input methods, I can call
aika_update
and in it I'll be able to get an accurate representation of the current state of different keys (by using the previous/current arrays like mentioned above). Next I neeeded to define the main input related functions that will get exposed to Lua, and those are:The way one of these functions would be use in Lua would be like this:
And whenever
a
was pressed, 1 would be printed to the console.One of these functions partially looks like this:
To break this down, let's start with the Lua related parts. All C functions that are supposed to be visible to Lua need to have the same signature:
static int function_name(lua_State *L)
. This is how the Lua API works so don't ask me why. The returned value should be the number of values that the function will return. For most functions it will be 0 or 1, but for some functions it will be higher than that, since Lua allows for multiple return values.L
represents the C/Lua stack, which is the main way that values are passed from/to C/Lua.So in the example above, the
luaL_checkstring
function is reading and checking if a value from the stack is a string, and if it is then it's placed in thekey
variable. Then from that key we get theSDL_Keycode
value from a map, which is initialized in the main function like this:And then this is repeated for all keys we care about. You can see the full version of it in aika.c#L771. Here I also used rxi/map, which is a hashmap library for C.
In any case, after we get the
SDL_Keycode
value we can check theinput_current_keyboard_state
andinput_previous_keyboard_state
arrays. In the case ofis_pressed
we want to check if the current state of this key is true and the previous one is false. If it is we push true to the stack, if it isn't we push false.lua_push*
functions are the functions that can push values to the stack, and those values are treated as the return values of the function in this situation.Now, the real version of the function above looks like this:
Because I need to do this same process for all input methods and not only the keyboard. Additionally, whenever we want to make a function visible to Lua we have to register it like this:
And you can see in aika.c#L678 the full version of this with all functions.
Now to finish the input part of the engine is simply a matter of using these 5 functions defined in C to build a new version of the library that I had already built. And in the end you can see that in aika.lua#L91. The API looks like this:
This API is the exact same as the one described in the github page for the library linked above so I'm not going to explain much of it, but needless to say it works exactly like it did before.
Graphics
After figuring out that my game loop worked properly and that I could press buttons and make things happen, I moved on to graphics. One of the goals I had with this engine was that I didn't want to deal with OpenGL at all. But most of the C/C++ solutions that allow me to do this for 2D games don't allow me to also have shaders, which isn't ideal, since I want to be able to write shaders for my games. The only piece of code I found that met both constraints was SDL-gpu.
The main concern with SDL-gpu is that it's written by a single random guy. So I have no way of knowing how well tested and/or how well supported it will be in the future, but I figured that I would take the risk and use it anyway as long as it did its job well enough, and the results were way beyond what I expected. As it turns out, SDL-gpu makes literally everything that I wanted to do extremely trivial, and I definitely didn't have to deal with that many low level graphics programming concepts at all.
Drawing + render targets
The first thing I tried was figuring out how to draw basic shapes, like a circle or a rectangle. This can be done using
GPU_Circle
. It takes in the usual values you'd expect, but also aGPU_Target
, which is the equivalent of aCanvas
in LÖVE.This is one thing that differed from LÖVE's API that I decided to change, since LÖVE's API has you call
love.graphics.setCanvas
to draw to a canvas, while SDL-gpu allows you to just pass in the canvas you want to draw to. I think the second way is better so this is what I did. And this looked something like this:And the function in Lua looks like:
Now, for this to work I also needed to create functions to create a new render target:
Using creating a
GPU_Image
usingGPU_CreateImage
and then creating the target on that image usingGPU_LoadTarget
seems to be the recommended way of getting this done in SDL-gpu, so this is what I did.It's also important to notice that in passing the
GPU_Image
struct to Lua, I'm passing it as light userdata withlua_pushlightuserdata
. This means that a pointer to this struct is being passed and nothing more. If the Lua code called this creation function and allocated some memory, then it will also be responsible for freeing it if necessary. This is important because it gives me more control over memory allocations on the Lua side of things, which differs from how most people seem to use the C/Lua API where they use full userdata and have things be garbage collected left and right, which isn't ideal IMO.On the Lua side of things, the render target creation function looks like this:
And so here I just create a little structure to hold some information about the render target, as well as it's pointer. This is the pointer that is passed back to the C code and that is read in the circle drawing function above, for instance. Most graphics functions follow this pattern, where a pointer to something is passed to Lua and then passed back to some other C function that will use it.
All the basic drawing functions defined that follow these ideas look like this:
The only additional thing to note here is the difference between
draw_to_screen
anddraw_to_render_target
. The first draws an image or render target to the "screen", which is the main render target that is only visible in C. The second draws an image or a render target to another render target. This other render target may then be drawn to the final "screen" using the first function. I think this is similar to how LÖVE did it, and even if it isn't it feels like the most natural way to do this.Shaders
Next I moved on to shaders. SDL-gpu provides a shader API that isn't as high level as I hoped, but with some work I was able to get what I wanted done to be done. For this part I had to look at a few of the examples the author of SDL-gpu provided and implement my own solution from there.
The shader functions are these so far:
These are pretty everything that I used from LÖVE in regards to shaders, except that the
send_*
functions (Shader:send
) could take more argument types than just floats and vec4s. This is another point that I'm going to get into later, but in general I'm doing the minimum necessary for things to work, and as I work on my game I'll fill out the rest. So in this case, adding other types of arguments to thegraphics.send_to_shader
function will happen on an as needed basis.As for the basics of how the implementation here looks:
Here I'm taking a filename, and then vertex and fragment shaders together using
GPU_LinkShaders
. I'm using adefault.vert
file for the vertex shader that looks like this:Because this is the recommended default vertex file for SDL-gpu. So far I haven't really written any vertex shaders so I'm not giving the
aika_graphics_create_shader
a way for this file to be changed for now, but in the future it might be necessary. Then the fragment shader is also passed in and the default version of that looks like this:Here the only difference from the recommended SDL-gpu settings is that I'm actively receiving the
color
parameter from the program, because I need to embed my own global color variable into the default shader. In Lua, whenever the program starts I have to do this as well:And so the values set from
graphics.set_color
will affect the default shader that is used by the program. This is useful because, for instance, I can set the current color and draw something like this:With an alpha value of 0.5, the box shadow image that is completely black originally is drawn like this:
Push/pop & Camera
A very common pattern in my LÖVE games is doing something like this:
Where
pushRotateScale
looks like this:This allows me to rotate and scale
something
andsomething_else
around their midpoint, while also rotating each of them locally around theirox, oy
pivots. This is a very very common pattern that appears in most entities in my games and so this is what I decided to focus on next. One result of implementing this is that I also implemented everything needed for my camera library to work.The functions implemented are:
And luckily these are very easy to get working because of SDL-gpu, here's an example of
aika_graphics_push_translate_rotate_scale
:And in Lua this function looks like this:
So here all I'm doing is some checking to see if I want to call the full translate/rotate/scale transforms or just push. Either way, SDL-gpu trivializes this and I could easily port my camera code over, which you can see in aika.lua#L563. After all this was implemented I could successfully bring some test code I wrote for LÖVE as well which looks like this:
Fonts
Finally, one last graphics related thing that I added was the ability to draw text. SDL-gpu doesn't come with any functionality for this, but SDL_ttf also makes this very easy. I ended up using this tutorial to learn how to use and everything turned out to be pretty easy. The functions defined were:
And here's what one of the draw function looks like:
The
TTF_RenderUTF8_Blended
function returns anSDL_Surface
, but if we want to draw this using SDL-gpu then we need to use aGPU_Image
. Luckily, SDL-gpu provides an easy way to transform one into the other withGPU_CopyImageFromSurface
. This was the only piece of "work" that I had to do for fonts, the rest are just direct calls from SDL_ttf's functions.So this Lua piece of code:
Results in this (blurry because I'm scaling a render target up using nearest neighbor):
Audio
Audio followed a similar path to fonts, since I just used an SDL focused library, in this case SDL_mixer. The functions I implemented were:
This is a very basic implementation, but it works well and it's what I find necessary to get the game off the ground. As time goes on I'll likely implement additional features, since SDL_mixer seems to have plenty of useful ones.
And this is all I did for now. Here's a list of functions that I implemented and their comparison to LÖVE's API:
One of the things that's important to notice is that I didn't implement literally everything that you would expect out of an engine, obviously. I implemented the basic amount of work needed to get things off the ground, and now I can focus going back to making my game:
There are additional things to implement, like Area/Level (aka Scenes or Rooms) objects, object management in general, collision, a library for animation, Steam/Twitch/Discord integration, and so on. But most of these I can either completely copy and paste from previous projects, or I only need to implement them when the time comes (they aren't critical). And as I started implementing more of my game, the engine will grow to fit that implementation and new features will get added.
Advice
Finally, I have 4 pieces of advice to give if you want to do something similar and make your own engine:
Make and finish games with other engines/frameworks first
This is something that should be obvious but that I see lots of people still do wrong. If you finish games using other people's tools, it will be that much easier to make your own tool because you'll have something to work from.
In my case, I finished a game with LÖVE, quickly identified some pain points that I wanted to fix, and decided to fix them. Those pain points were mainly: "I want more control over the C part of the codebase". So when making my engine I can this my entire focus and not worry about anything else, which means that I can reach conclusions like "let's just copy LÖVE's API because it's good enough for my purposes". And copying someone else's API saves a massive amount of work, because it gives you a very well defined direction that's hard to build from scratch if you're just aimlessly making an engine.
I see lots of people who don't follow this advice and they spend years making their engines because they're not just making their engines, they're also trying to solve the API definition problem, and they're also trying to make it super performant (because engine code needs to be performant, right??), and they're also trying to implement tons of features even though they don't know they'll need them, and so on. It's basically work without any direction that will never end because those people don't have a well defined goal other than "making an engine".
Don't implement every feature ever
One thing I did was to not implement every feature that I'll ever think I'll need. I implemented the very basics to know that the code around a certain area worked at a minimal level, but I'm leaving the implementation of additional features in that area for later as they become necessary. The best example are the shader
send_*
functions. Right now I can only send floats and vec4s, but ideally you want to be able to send any type of data to a shader, like integers, textures, matrixes and so on. But I decided to only implement those when they're actually needed. We can call this something like "lazy evaluation" if we want to borrow some terminology from the FP turbonerds.Like I said before, a lot of people who try writing their own engines fall into the trap of implementing every feature ever even though they don't really need them. This happens as a result of not having well defined goals with what they're going to do with their engines. My goals, on the other hand, are simple: I'm going to make a game with it and I'm the only one who's going to use it, so implementing features that my game won't need is a waste of time.
Code pragmatically
This is a point I made throughout the previous article I wrote on this, but I feel like this is important to say again: generally agreed upon coding practices are bad for solo indie developers. The only things that are universally good are naming your variables properly and putting some comments here and there, the rest is questionable and should be questioned.
For instance, all the code talked about in this article is in two files: aika.c and aika.lua. Both are about 1K lines long and will grow as time goes on. The advice people normally give would be something like: "yea this is totally bad you should separate them into their proper logical pieces everything shouldn't be here you're fucking stupid!!", and while this person may have some kind of point, at the end of the day it doesn't matter.
It's just a few thousand lines, it's code that's written once and then will rarely be changed, and because I architected it correctly any changes will be somewhat contained, since changing the C API doesn't require any gameplay code changes, only changes to the how the Lua interface (in the aika.lua) handles it.
Just focus on writing code that works and don't worry about good practices, most of them are useless for the purposes of solo indie game development.
Don't listen to reddit
When I first posted my previous article to reddit on both /r/programming and /r/gamedev a lot of people responded to it, but most response regarding me writing my own engine was negative. People said all sorts of things that were on this general train of thought:
Which is basically, "making an engine is hard, it will take up a ton of time, don't bother". And this is one of those cases where the general wisdom is just wrong, like it is wrong for most coding practices that people promote. I don't wanna get into why this general wisdom is wrong (maybe it's a big conspiracy by Big ECS to keep us little yolocoders down), but I do wanna say that people generally don't know what they're talking about and whenever you read people saying something online, it's good to check if there's any substance behind their opinions.
In this case, for instance, it's clear that I'm right and the guy above was wrong, because I reached most of my goal in 1-2 months. But in the more general case where you can't really tell it's just good to be aware that this is happening all the time and that the most upvoted advice you're reading on reddit is probably wrong in some fatal way. Jonathan Blow said this better than me with this little story:
END
Hopefully this article was useful and hopefully more people will be encouraged to try making their own engines. I'm not a particularly good coder, nor am I particularly disciplined. I didn't work on this every day, and most of the days I worked on it were for 1-2 hours. None of my commits were particularly large, so I'm really not doing that much work per day. It's just a matter of having a well defined goal, and slowly working towards it (almost) every day.
Comments on /r/gamedev