Git Product home page Git Product logo

blog's Introduction

2021-01-15 Game Ideas

2020-06-23 Springs

2019-09-16 Easy Modes

2018-04-27 Luck Isn't Real

2016-01-01 Downwell Trails

blog's People

Contributors

a327ex avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

BYTEPATH #15 - Final

Introduction

In this final article we'll talk about a few subjects that didn't fit into any of the previous ones but that are somewhat necessary for a complete game. In order, what we'll cover will be: saving and loading data, achievements, shaders and audio.

Saving and Loading

Because this game doesn't require us to save level data of any kind, saving and loading becomes very very easy. We'll use a library called bitser to do it and two of its functions: dumpLoveFile and loadLoveFile. These functions will save and load whatever data we pass it to a file using love.filesystem. As the link states, the files are saved to different directories based on your operating system. If you're on Windows then the file will be saved in C:\Users\user\AppData\Roaming\LOVE. We can use love.filesystem.setIdentity to change the save location. If we set the identity to BYTEPATH instead, then the save file will be saved in C:\Users\user\AppData\Roaming\BYTEPATH.

In any case, we'll only need two functions: save and load. They will be defined in main.lua. Let's start with the save function:

function save()
    local save_data = {}
    -- Set all save data here
    bitser.dumpLoveFile('save', save_data)
end

The save function is pretty straightforward. We'll create a new save_data table and in it we'll place all the data we want to save. For instance, if we want to save how many skill points the player has, then we'll just say save_data.skill_points = skill_points, which means that save_data.skill_points will contain the value that our skill_points global contains. The same goes for all other types of data. It's important though to keep ourselves to saving values and tables of values. Saving full objects, images, and other types of more complicated data likely won't work.

In any case, after we add everything we want to save to save_data then we simply call bitser.dumpLoveFile and save all that data to the 'save' file. This will create a file called save in C:\Users\user\AppData\Roaming\BYTEPATH and once that file exists all the information we care about being saved is saved. We can call this function once the game is closed or whenever a round ends. It's really up to you. The only problem I can think of in calling it only when the game ends is that if the game crashes then the player's progress will likely not be saved, so that might be a problem.

Now for the load function:

function load()
    if love.filesystem.exists('save') then
        local save_data = bitser.loadLoveFile('save')
	-- Load all saved data here
    else
        first_run_ever = true
    end
end

The load function works very similarly except backwards. We call bitser.loadLoveFile using the name of our saved file (save) and then put all that data inside a local save_data table. Once we have all the saved data in this table then we can assign it to the appropriate variables. So, for instance, if now we want to load the player's skill points we'll do skill_points = save_data.skill_points, which means we're assigning the saved skill points to our global skill points variable.

Additionally, the load function needs a bit of additional logic to work properly. If it's the first time the player has run the game then the save file will not exist, which means that when try to load it we'll crash. To prevent this we check to see if it exists with love.filesystem.exists and only load it if it does. If it doesn't then we just set a global variable first_run_ever to true. This variable is useful because generally we want to do a few things differently if it's the first time the player has run the game, like maybe running a tutorial of some kind or showing some message of some kind that only first timers need. The load function will be called once in love.load whenever the game is loaded. It's important that this function is called after the globals.lua file is loaded, since we'll be overwriting global variables in it.

And that's it for saving/loading. What actually needs to be saved and loaded will be left as an exercise since it depends on what you decided to implement or not. For instance, if you implement the skill tree exactly like in article 13, then you probably want to save and load the bought_node_indexes table, since it contains all the nodes that the player bought.


Achievements

Because of the simplicity of the game achievements are also very easy to implement (at least compared to everything else xD). What we'll do is simply have a global table called achievements. And this table will be populated by keys that represent the achievement's name, and values that represent if that achievement is unlocked or not. So, for instance, if we have an achievement called '50K', which unlocks whenever the player reaches 50.000 score in a round, then achievements['50K'] will be true if this achievements has been unlocked and false otherwise.

To exemplify how this works let's create the 10K Fighter achievement, which unlocks whenever the player reaches 10.000 score using the Fighter ship. All we have to do to achieve this is set achievements['10K Fighter'] to true whenever we finish a round, the score is above 10K and the ship currently being used by the player is 'Fighter'. This looks like this:

function Stage:finish()
    timer:after(1, function()
        gotoRoom('Stage')

        if not achievements['10K Fighter'] and score >= 10000 and device = 'Fighter' then
            achievements['10K Fighter'] = true
            -- Do whatever else that should be done when an achievement is unlocked
        end
    end)
end

As you can see it's a very small amount of code. The only thing we have to make sure is that each achievement only gets triggered once, and we do that by checking to see if that achievement has already been unlocked or not first. If it hasn't then we proceed.

I don't know how Steam's achievement system work yet but I'm assuming that we can call some function or set of functions to unlock an achievement for the player. If this is the case then we would call this function here as we set achievements['10K Fighter'] to true. One last thing to remember is that achievements need to be saved and loaded, so it's important to add the appropriate code back in the save and load functions.


Shaders

In the game so far I've been using about 3 shaders and we'll cover only one. However since the others use the same "framework" they can be applied to the screen in a similar way, even though the contents of each shader varies a lot. Also, I'm not a shaderlord so certainly I'm doing lots of very dumb things and there are better ways of doing all that I'm about to say. Learning shaders was probably the hardest part of game development for me and I'm still not comfortable enough with them to the extend that I am with the rest of my codebase.

With all that said, we'll implement a simple RGB shift shader and apply it only to a few select entities in the game. The basic way in which pixel shaders work is that we'll write some code and this code will be applied to all pixels in the texture passed into the shader. You can read more about the basics here.

One of the problems that I found when trying to apply this pixel shader to different objects in the game is that you can't apply it directly in that object's code. For whatever reason (and someone who knows more would be able to give you the exact reason here), pixel shaders aren't applied properly whenever we use basic primitives like lines, rectangles and so on. And even if we were using sprites instead of basic shapes, the RGB shift shader wouldn't be applied in the way we want either because the effect requires us to go outside the sprite boundaries. But because the pixel shader is only applied to pixels in the texture, when we try to apply it it will only read pixels inside the sprite's boundary so our effect doesn't work.

To solve this I've defaulted to drawing the objects that I want to apply effect X to to a new canvas, and then applying the pixel shader to that entire canvas. In a game like this where the order of drawing doesn't really matter this has almost no drawbacks. However in a game where the order of drawing matters more (like a 2.5D top-downish game) doing this gets a bit more complicated, so it's not a general solution for anything.


rgb_shift.frag

Before we get into coding all this let's get the actual pixel shader out of the way, since it's very simple:

extern vec2 amount;
vec4 effect(vec4 color, Image texture, vec2 tc, vec2 pc) {
    return color*vec4(Texel(texture, tc - amount).r, Texel(texture, tc).g, 
    Texel(texture, tc + amount).b, Texel(texture, tc).a);
}

I place this in a file called rgb_shift.frag in resources/shaders and loaded it in the Stage room using love.graphics.newShader. The entry point for all pixel shaders is the effect function. This function receives a color vector, which is the one set with love.graphics.setColor, except that instead of being in 0-255 range, it's in 0-1 range. So if the current color is set to 255, 255, 255, 255, then this vec4 will have values 1.0, 1.0, 1.0, 1.0. The second thing it receives is a texture to apply the shader to. This texture can be a canvas, a sprite, or essentially any object in LÖVE that is drawable. The pixel shader will automatically go over all pixels in this texture and apply the code inside the effect function to each pixel, substituting its pixel value for the value returned. Pixel values are always vec4 objects, for the 4 red, green, blue and alpha components.

The third argument tc represents the texture coordinate. Texture coordinates range from 0 to 1 and represent the position of the current pixel inside the pixel. The top-left corner is 0, 0 while the bottom-right corner is 1, 1. We'll use this along with the texture2D function (which in LÖVE is called Texel) to get the contents of the current pixel. The fourth argument pc represents the pixel coordinate in screen space. We won't use this for this shader.

Finally, the last thing we need to know before getting into the effect function is that we can pass values to the shader to manipulate it in some way. In this case we're passing a vec2 called amount which will control the size of the RGB shift effect. Values can be passed in with the send function.

Now, the single line that makes up the entire effect looks like this:

return color*vec4(
    Texel(texture, tc - amount).r, 
    Texel(texture, tc).g, 
    Texel(texture, tc + amount).b, 
    Texel(texture, tc).a);

What we're doing here is using the Texel function to look up pixels. But we don't wanna look up the pixel in the current position only, we also want to look for pixels in neighboring positions so that we can actually to the RGB shifting. This effect works by shifting different channels (in this case red and blue) in different directions, which gives everything a glitchy look. So what we're doing is essentially looking up the pixel in position tc - amount and tc + amount, and then taking and red and blue value of that pixel, along with the green value of the original pixel and outputting it. We could have a slight optimization here since we're grabbing the same position twice (on the green and alpha components) but for something this simple it doesn't matter.


Selective drawing

Since we want to apply this pixel shader only to a few specific entities, we need to figure out a way to only draw specific entities. The easiest way to do this is to mark each entity with a tag, and then create an alternate draw function in the Area object that will only draw objects with that tag. Defining a tag looks like this:

function TrailParticle:new(area, x, y, opts)
    TrailParticle.super.new(self, area, x, y, opts)
    self.graphics_types = {'rgb_shift'}
    ...
end

And then creating a new draw function that will only draw objects with certain tags in them looks like this:

function Area:drawOnly(types)
    table.sort(self.game_objects, function(a, b) 
        if a.depth == b.depth then return a.creation_time < b.creation_time
        else return a.depth < b.depth end
    end)

    for _, game_object in ipairs(self.game_objects) do 
        if game_object.graphics_types then
            if #fn.intersection(types, game_object.graphics_types) > 0 then
                game_object:draw() 
            end
        end
    end
end

So this is exactly like that the normal Area:draw function except with some additional logic. We're using the intersection to figure out if there are any common elements between the objects graphics_types table and the types table that we pass in. For instance, if we decide we only wanna draw rgb_shift type objects, then we'll call area:drawOnly({'rgb_shift'}), and so this table we passed in will be checked against each object's graphics_types. If they have any similar elements between them then #fn.intersection will be bigger than 0, which means we can draw the object.

Similarly, we will want to implement an Area:drawExcept function, since whenever we draw an object to one canvas we don't wanna draw it again in another, which means we'll need to exclude certain types of objects from drawing at some point. That looks like this:

function Area:drawExcept(types)
    table.sort(self.game_objects, function(a, b) 
        if a.depth == b.depth then return a.creation_time < b.creation_time
        else return a.depth < b.depth end
    end)

    for _, game_object in ipairs(self.game_objects) do 
        if not game_object.graphics_types then game_object:draw() 
        else
            if #fn.intersection(types, game_object.graphics_types) == 0 then
                game_object:draw()
            end
        end
    end
end

So here we draw the object if it doesn't have graphics_types defined, as well as if its intersection with the types table is 0, which means that its graphics type isn't one of the ones specified by the caller.


Canvases + shaders

With all this in mind now we can actually implement the effect. For now we'll just implement this on the TrailParticle object, which means that the trail that the player and projectiles creates will be RGB shifted. The main way in which we can apply the RGB shift only to objects like TrailParticle looks like this:

function Stage:draw()
    ...
    love.graphics.setCanvas(self.rgb_shift_canvas)
    love.graphics.clear()
    	camera:attach(0, 0, gw, gh)
    	self.area:drawOnly({'rgb_shift'})
    	camera:detach()
    love.graphics.setCanvas()
    ...
end

This looks similar to how we draw things normally, except that now instead of drawing to main_canvas, we're drawing to the newly created rgb_shift_canvas. And more importantly we're only drawing objects that have the 'rgb_shift' tag. In this way this canvas will contain all the objects we need so that we can apply our pixel shaders to later. I use a similar idea for drawing Shockwave and Downwell effects.

Once we're done with drawing to all our individual effect canvases, we can draw the main game to main_canvas with the exception of the things we already drew in other canvases. So that would look like this:

function Stage:draw()
    ...
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        camera:attach(0, 0, gw, gh)
        self.area:drawExcept({'rgb_shift'})
        camera:detach()
	love.graphics.setCanvas()
  	...
end

And then finally we can apply the effects we want. We'll do this by drawing the rgb_shift_canvas to another canvas called final_canvas, but this time applying the RGB shift pixel shader. This looks like this:

function Stage:draw()
    ...
    love.graphics.setCanvas(self.final_canvas)
    love.graphics.clear()
        love.graphics.setColor(255, 255, 255)
        love.graphics.setBlendMode("alpha", "premultiplied")
  
        self.rgb_shift:send('amount', {
      	random(-self.rgb_shift_mag, self.rgb_shift_mag)/gw, 
      	random(-self.rgb_shift_mag, self.rgb_shift_mag)/gh})
        love.graphics.setShader(self.rgb_shift)
        love.graphics.draw(self.rgb_shift_canvas, 0, 0, 0, 1, 1)
        love.graphics.setShader()
  
  	love.graphics.draw(self.main_canvas, 0, 0, 0, 1, 1)
  	love.graphics.setBlendMode("alpha")
  	love.graphics.setCanvas()
  	...
end

Using the send function we can change the value of the amount variable to correspond to the amount of shifting we want the shader to apply. Because the texture coordinates inside the pixel shader are between values 0 and 1, we want to divide the amounts we pass in by gw and gh. So, for instance, if we want a shift of 2 pixels then rgb_shift_mag will be 2, but the value passed in will be 2/gw and 2/gh, since inside the pixel shader, 2 pixels to the left/right is represented by that small value instead of actually 2. We also draw the main canvas to the final canvas, since the final canvas should contain everything that we want to draw.

Finally outside this we can draw this final canvas to the screen:

function Stage:draw()
    ...
    love.graphics.setColor(255, 255, 255)
    love.graphics.setBlendMode("alpha", "premultiplied")
    love.graphics.draw(self.final_canvas, 0, 0, 0, sx, sy)
    love.graphics.setBlendMode("alpha")
    love.graphics.setShader()
end

We could have drawn everything directly to the screen instead of to the final_canvas first, but if we wanted to apply another screen-wide shader to the final screen, like for instance the distortion, then it's easier to do that if everything is contained in a canvas properly.

And so all that would end up looking like this:

And as expected, the trail alone is being RGB shifted and looks kinda glitchly like we wanted.


Audio

I'm not really big on audio so while there are lots of very interesting and complicated things one could do, I'm going to stick to what I know, which is just playing sounds whenever appropriate. We can do this by using ripple.

This library has a pretty simple API and essentially it boils down to loading sounds using ripple.newSound and playing those sounds by calling :play on the returned object. For instance, if we want to play a shooting sound whenever the player shoots, we could do something like this:

-- in globals.lua
shoot_sound = ripple.newSound('resources/sounds/shoot.ogg')
function Player:shoot()
    local d = 1.2*self.w
    self.area:addGameObject('ShootEffect', ...
    shoot_sound:play()
    ...
end

And so in this very simple way we can just call :play whenever we want a sound to happen. The library also has additional goodies like changing the pitch of the sound, playing sounds in a loop, creating tags so that you can change properties of all sounds with a certain tag, and so on. In the actual game I ended up doing some additional stuff on top of this, but I'm not going to go over all that here. If you've bought the tutorial you can see all that in the sound.lua file.


END

And this is the end of this tutorial. By no means have we covered literally everything that we could have covered about this game but we went over the most important parts. If you followed along until now you should have a good grasp on the codebase so that you can understand most of it, and if you bought the tutorial then you should be able to read the full source code with a much better understanding of what's actually happening there.

Hopefully this tutorial has been helpful so that you can get some idea of what making a game actually entails and how to go from zero to the final result. Ideally now that you have all this done you should use what you learned from this to make your own game instead of just changing this one, since that's a much better exercise that will test your "starting from zero" abilities. Usually when I start a new project I pretty much copypaste a bunch of code that I know has been useful between multiple projects, generally that's a lot of the "engine" code that we went over in articles 1 through 5.

Anyway, I don't know how to end this so... bye!



BYTEPATH #14 - Console

Introduction

In this article we'll go over the Console room. The Console is considerably easier to implement than everything else we've been doing so far because in the end it boils down to printing some text on the screen.

The Console room will be composed of 3 different types of objects: lines, input lines and modules. Lines are just normal colored text lines that appear on the screen. In the example above, for instance, ":: running BYTEPATH..." would be a line. As a data structure this will just be a table holding the position of the line as well as its text and colors.

Input lines are lines where the player can type things into. In the example above they are the ones that have "arch" in them. Typing certain commands in an input line will trigger those commands and usually they will either create more lines and modules. As a data structure this will be just like a line, except there's some additional logic needed to read input whenever the last line added to the room was an input one.

Finally, a module is a special object allows the user to do things that are a bit more complex than just typing commands. The whole set of things that appear when the player has to pick a ship, for instance, is one of those modules. A lot of commands will spawn these objects, so, for instance, if the player wants to change the volume of the game he will type "volume" and then the Volume module will appear and will let the player choose whatever level of sound he wants. These modules will all be objects of their own and the Console room will handle creating and deleting them when appropriate.


Lines

So let's start with lines. The basic way in which we can define a line is like this:

{
    x = x, y = y, 
    text = love.graphics.newText(font, {boost_color, 'blue text', default_color, 'white text'}
}

So it has a x, y position as well as a text attribute. This text attribute is a Text object. We'll use LÖVE's Text objects because they let us define colored text easily. But before we can add lines to our Console room we have to create it, so go ahead and do that. The basics of it should be the same as the SkillTree one.

We'll add a lines table to hold all the text lines, and then in the draw function we'll go over this table and draw each line. We'll also add a function named addLine which will add a new text line to the lines table:

function Console:new()
    ...
  
    self.lines = {}
    self.line_y = 8
    camera:lookAt(gw/2, gh/2)

    self:addLine(1, {'test', boost_color, ' test'})
end

function Console:draw()
    ...
    for _, line in ipairs(self.lines) do love.graphics.draw(line.text, line.x, line.y) end
    ...
end

function Console:addLine(delay, text)
    self.timer:after(delay, function() 
    	table.insert(self.lines, {x = 8, y = self.line_y, 
        text = love.graphics.newText(self.font, text)}) 
      	self.line_y = self.line_y + 12
    end)
end

There are a few additional things happening here. First there's the line_y attribute which will keep track of the y position where we should add a new line next. This is incremented by 12 every time we call addLine, since we want new lines to be added below the previous one, like it happens in a normal terminal.

Additionally the addLine function has a delay. This delay is useful because whenever we're adding multiple lines to the console, we don't want them to be added all the same time. We want a small delay between each addition because it makes everything feel better. One extra thing we could do here is make it so that on top of each line being added with a delay, its added character by character. So that instead of the whole line going in at once, each character is added with a small delay, which would give it an even nicer effect. I'm not doing this for the sake of time but it's a nice challenge (and we already have part of the logic for this in the InfoText object).

All that should look like this:

And if we add multiple lines it also looks like expected:


Input Lines

Input lines are a bit more complicated but not by much. The first thing we wanna do is add an addInputLine function, which will act just like the addLine function, except it will add the default input line text and enable text input from the player. The default input line text we'll use is [root]arch~ , which is just some flavor text to be placed before our input, like in a normal terminal.

function Console:addInputLine(delay)
    self.timer:after(delay, function()
        table.insert(self.lines, {x = 8, y = self.line_y, 
        text = love.graphics.newText(self.font, self.base_input_text)})
        self.line_y = self.line_y + 12
        self.inputting = true
    end)
end

And base_input_text looks like this:

function Console:new()
    ...
    self.base_input_text = {'[', skill_point_color, 'root', default_color, ']arch~ '}
    ...
end

We also set inputting to true whenever we add a new input line. This boolean will be used to tell us when we should be picking up input from the keyboard or not. If we are, then we'll simply add all characters that the player types to a list, put this list together as a string, and then add that string to our Text object. This looks like this:

function Console:textinput(t)
    if self.inputting then
        table.insert(self.input_text, t)
        self:updateText()
    end
end

function Console:updateText()
    local base_input_text = table.copy(self.base_input_text)
    local input_text = ''
    for _, character in ipairs(self.input_text) do input_text = input_text .. character end
    table.insert(base_input_text, input_text)
    self.lines[#self.lines].text:set(base_input_text)
end

And Console:textinput will get called whenever love.textinput gets called, which happens whenever the player presses a key:

-- in main.lua
function love.textinput(t)
    if current_room.textinput then current_room:textinput(t) end
end

One last thing we should do is making sure that the enter and backspace keys work. The enter key will turn inputting to false and also take the contents of the input_text table and do something with them. So if the player typed "help" and then pressed enter, we'll run the help command. And the backspace key should just remove the last element of the input_text table:

function Console:update(dt)
    ...
    if self.inputting then
        if input:pressed('return') then
            self.inputting = false
	    -- Run command based on the contents of input_text here
            self.input_text = {}
        end
        if input:pressRepeat('backspace', 0.02, 0.2) then 
            table.remove(self.input_text, #self.input_text) 
            self:updateText()
        end
    end
end

Finally, we can also simulate a blinking cursor for some extra points. The basic way to do this is to just draw a blinking rectangle at the position after the width of base_input_text concatenated with the contents of input_text.

function Console:new()
    ...
    self.cursor_visible = true
    self.timer:every('cursor', 0.5, function() 
    	self.cursor_visible = not self.cursor_visible 
    end)
end

In this way we get the blinking working, so we'll only draw the rectangle whenever cursor_visible is true. Next for the drawing the rectangle:

function Console:draw()
    ...
    if self.inputting and self.cursor_visible then
        local r, g, b = unpack(default_color)
        love.graphics.setColor(r, g, b, 96)
        local input_text = ''
        for _, character in ipairs(self.input_text) do input_text = input_text .. character end
        local x = 8 + self.font:getWidth('[root]arch~ ' .. input_text)
        love.graphics.rectangle('fill', x, self.lines[#self.lines].y,
      	self.font:getWidth('w'), self.font:getHeight())
        love.graphics.setColor(r, g, b, 255)
    end
    ...
end

In here the variable x will hold the position of our cursor. We add 8 to it because every line is being drawn by default starting at position 8, so if we don't take this into account the cursor's position will be wrong. We also consider that the cursor rectangle's width is the width of the 'w' letter with the current font. Generally w is the widest letter to use so we'll go with that. But this could also be any other fixed number like 10 or 8 or whatever else.

And all that should look like this:


Modules

Modules are objects that contain certain logic to let the player do something in the console. For instance, the ResolutionModule that we'll implement will let the player change the resolution of the game. We'll separate modules from the rest of the Console room code because they can get a bit too involved with their logic, so having them as separate objects is a good idea. We'll implement a module that looks like this:

This module in particular gets created and added whenever the player has pressed enter after typing "resolution" on an input line. Once it's activated it takes control away from the console and adds a few lines with Console:addLine to it. It then also has some selection logic on top of those added lines so we can pick our target resolution. Once the resolution is picked and the player presses enter, the window is changed to reflect that new resolution, we add a new input line with Console:addInputLine and disable selection on this ResolutionModule object, giving control back to the console.

All modules will work somewhat similarly to this. They get created/added, they do what they're supposed to do by taking control away from the Console room, and then when their behavior is done they give it control back. We can implement the basics of this on the Console object like this:

function Console:new()
    ...
    self.modules = {}
    ...
end

function Console:update(dt)
    self.timer:update(dt)
    for _, module in ipairs(self.modules) do module:update(dt) end

    if self.inputting then
    ...
end
  
function Console:draw()
    ...
    for _, module in ipairs(self.modules) do module:draw() end
    camera:detach()
    ...
end

Because we're mostly coding this by ourselves we can skip some formalities here. Even though I just said we'll have this sort of rule/interface between Console object and Module objects where they exchange control of the player's input with each other, in reality all we have to do is simply add modules to the self.modules table, update and draw them. Each module will take care of activating/deactivating itself whenever appropriate, which means that on the Console side of things we don't really have to do much.

Now for the creation of the ResolutionModule:

function Console:update(dt)
    ...
    if self.inputting then
        if input:pressed('return') then
            self.line_y = self.line_y + 12
            local input_text = ''
            for _, character in ipairs(self.input_text) do 
                input_text = input_text .. character 
      	    end
            self.input_text = {}

            if input_text == 'resolution' then
                table.insert(self.modules, ResolutionModule(self, self.line_y))
            end
        end
        ...
    end
end

In here we make it so that the input_text variable will hold what the player typed into the input line, and then if this text is equal to "resolution" we create a new ResolutionModule object and add it to the modules list. Most modules will need a reference to the console as well as the current y position where lines are added to, since the module will be placed below the lines that exist currently in the console. So to achieve that we pass both self and self.line_y when we create a new module object.

The ResolutionModule itself is rather straightforward. For this one in particular all we'll have to do is add a bunch of lines as well as some small amount of logic to select between each line. To add the lines we can simply do this:

function ResolutionModule:new(console, y)
    self.console = console
    self.y = y

    self.console:addLine(0.02, 'Available resolutions: ')
    self.console:addLine(0.04, '    480x270')
    self.console:addLine(0.06, '    960x540')
    self.console:addLine(0.08, '    1440x810')
    self.console:addLine(0.10, '    1920x1080')
end

To make things easy for now all the resolutions we'll concern ourselves with are the ones that are multiples of the base resolution, so all we have to do is add those 4 lines.

After this is done all we have to do is add the selection logic. The selection logic feels like a hack but it works well: we'll just place a rectangle on top of the current selection and move this rectangle around as the player presses up or down. We'll need a variable to keep track of which number we're in now (1 through 4), and then we'll draw this rectangle at the appropriate y position based on this variable. All this looks like this:

function ResolutionModule:new(console, y)
    ...
    self.selection_index = sx
    self.selection_widths = {
        self.console.font:getWidth('480x270'), self.console.font:getWidth('960x540'),
        self.console.font:getWidth('1440x810'), self.console.font:getWidth('1920x1080')
    }
end

The selection_index variable will keep track of our current selection and we start it at sx. sx is either 1, 2, 3 or 4 based on the size we chose in main.lua when we called the resize function. selection_widths holds the widths for the rectangle on each selection. Since the rectangle will end up covering each resolution, we need to figure out its size based on the size of the characters that make up the string for that resolution.

function ResolutionModule:update(dt)
    ...
    if input:pressed('up') then
        self.selection_index = self.selection_index - 1
        if self.selection_index < 1 then self.selection_index = #self.selection_widths end
    end

    if input:pressed('down') then
        self.selection_index = self.selection_index + 1
        if self.selection_index > #self.selection_widths then self.selection_index = 1 end
    end
    ...
end

In the update function we'll handle the logic for when the player presses up or down. We just need to increase or decrease selection_index and take care to not go below 1 or above 4.

function ResolutionModule:draw()
    ...
    local width = self.selection_widths[self.selection_index]
    local r, g, b = unpack(default_color)
    love.graphics.setColor(r, g, b, 96)
    local x_offset = self.console.font:getWidth('    ')
    love.graphics.rectangle('fill', 8 + x_offset - 2, self.y + self.selection_index*12, 
    width + 4, self.console.font:getHeight())
    love.graphics.setColor(r, g, b, 255)
end

And in the draw function we just draw the rectangle at the appropriate position. Again, this looks terrible and full of weird numbers all over but we need to place the rectangle in the appropriate location, and there's no "clean" way of doing it.

The only thing left to do now is to make sure that this object is only reading input whenever it's active, and that it's active only right after it has been created and before the player has pressed enter to select a resolution. After the player presses enter it should be inactive and not reading input anymore. A simple way to do this is like this:

function ResolutionModule:new(console, y)
    ...
    self.console.timer:after(0.02 + self.selection_index*0.02, function() 
        self.active = true 
    end)
end

function ResolutionModule:update(dt)
    if not self.active then return end
	...
  	if input:pressed('return') then
    	self.active = false
    	resize(self.selection_index)
    	self.console:addLine(0.02, '')
    	self.console:addInputLine(0.04)
   end
end

function ResolutionModule:draw()
    if not self.active then return end
    ...
end

The active variable will be set to true a few frames after the module is created. This is to avoid having the rectangle drawn before the lines are added, since the lines are added with a small delay between each other. If this active variable is not active then the update nor the draw function won't run, which means we won't be reading input for this object nor drawing the selection rectangle. Additionally, whenever we press enter we set active to false, call the resize function and then give control back to the Console by adding a new input line. All this gives us the appropriate behavior and everything should work as expected now.


Exercises

227. (CONTENT) Make it so that whenever there are more lines than the screen can cover in the Console room, the camera scrolls down as lines and modules are added.

228. (CONTENT) Implement the AchievementsModule module. This displays all achievements and what's needed to unlock them. Achievements will be covered in the next article, so you can come back to this exercise later!

229. (CONTENT) Implement the ClearModule module. This module allows for clearing of all saved data or the clearing of the skill tree. Saving/loading data will be covered in the next article as well, so you can come back to this exercise later too.

230. (CONTENT) Implement the ChooseShipModule module. This modules allows the player to choose and unlocks ships with which to play the game. This is what it looks like:

231. (CONTENT) Implement the HelpModule module. This displays all available commands and lets the player choose a command without having to type anything. The game also has to support gamepad only players so forcing the player to type things is not good.

232. (CONTENT) Implement the VolumeModule module. This lets the player change the volume of sound effects and music.

233. (CONTENT) Implement the mute, skills, start ,exit and device commands. mute mutes all sound. skills changes to the SkillTree room. start spawns a ChooseShipModule and then starts the game after the player chooses a ship. exit exits the game.


END

And this is it for the console. With only these three ideas (lines, input lines and modules) we can do a lot and use this to add a lot of flavor to the game. The next article is the last one and in it we'll cover a bunch of random things that didn't fit in any other articles before.



BYTEPATH #13 - Skill Tree

Introduction

In this article we'll focus on the creation of the skill tree. This is what the skill tree looks like right now. We'll not place each node hand by hand or anything like that (that will be left as an exercise), but we will go over everything needed to make the skill tree happen and work as one would expect.

First we'll focus on how each node will be defined, then on how we can read those definitions, create the necessary objects and apply the appropriate passives to the player. Then we'll move on to the main objects (Nodes and Links), and after that we'll go over saving and loading the tree. And finally the last thing we'll do is implement the functionality needed so that the player can spend his skill points on it.


Skill Tree

There are many different ways we can go about defining a skill tree, each with their advantages and disadvantages. There are roughly three options we can go for:

  • Create a skill tree editor to place, link and define the stats for each node visually;
  • Create a skill tree editor to place and link nodes visually, but define stats for each node in a text file;
  • Define everything in a text file.

I'm someone who likes to keep the implementation of things simple and who has no problem with doing lots of manual and boring work, which means that I'll solve problems in this way generally. When it comes to the options above it means I'll pick the third one.

The first two options require us to build a visual skill tree editor. To understand what this entails exactly we should try to list the high level features that a visual skill tree editor would have:

  • Placing new nodes
  • Linking nodes together
  • Deleting nodes
  • Moving nodes
  • Text input for defining each node's stats

These are pretty much the only high level features I can think of initially, and they imply a few more things:

  • Nodes will probably have to be aligned in relation to each other in some way, which means we'll need some sort of alignment system in place. Maybe nodes can only be placed according to some sort of grid system.
  • Linking, deleting and moving nodes around implies that we need an ability to select certain nodes to which we want to apply each of those actions. This means node selection is another feature we'd have to implement.
  • If we go for the option where we also define stats visually, then text input is necessary. There are many ways we can get a proper TextInput element working in LÖVE for little work (https://github.com/keharriso/love-nuklear), so we just need to add the logic for when a text input element appears, and how we read information from it once its been written to.

As you can see, adding a skill tree editor doesn't seem like a lot of work compared to what we've done so far. So if you want to go for that option it's totally viable and may make the process of building the skill tree better for you. But like I said, I generally have no problem with doing lots of manual and boring work, which means that I have no problem with defining everything in a text file. So for this article we will not do any of those skill tree editor things and we will define the entirety of the skill tree in a text file.


Tree Definition

So to get started with the tree's definition we need to think about what kinds of things make up a node:

  • Passive's text:
    • Name
    • Stats it changes (6% Increased HP, +10 Max Ammo, etc)
  • Position
  • Linked nodes
  • Type of node (normal, medium or big)

So, for instance, the "4% Increased HP" node shown in the gif below:

Could have a definition like this:

tree[10] = {
    name = 'HP', 
    stats = {
        {'4% Increased HP', 'hp_multiplier' = 0.04}
    }
    x = 150, y = 150,
    links = {4, 6, 8},
    type = 'Small',
}

We're assuming that (150, 150) is a reasonable position, and that the position on the tree table of the nodes linked to it are 4, 6 and 8 (its own position is 10, since its being defined in tree[10]). In this way, we can easily define all the hundreds of nodes in the tree, pass this huge table to some function which will read all this, create Node objects and link those accordingly, and then we can apply whatever logic we want to the tree from there.


Nodes and Camera

Now that we have an idea of what the tree file will look like we can start building from it. The first thing we have to do is create a new SkillTree room and then use gotoRoom to go to it at the start of the game (since that's where we'll be working for now). The basics of this room should be exactly the same as the Stage room, so I'll assume you're capable of doing that with no guidance.

We'll define two nodes in the tree.lua file but we'll do it only by their position for now. Our goal will be to read those nodes from that file and create them in the SkillTree room. We could define them like this:

tree = {}
tree[1] = {x = 0, y = 0}
tree[2] = {x = 32, y = 0}

And we could read them like this:

function SkillTree:new()
    ...

    self.nodes = {}
    for _, node in ipairs(tree) do table.insert(self.nodes, Node(node.x, node.y)) end
end

Here we assume that all objects for our SkillTree will not be inside an Area, which means we don't have to use addGameObject to add a new game object to the environment, and it also means we need to keep track of existing objects ourselves. In this case we're doing that in the nodes table. The Node object could look like this:

Node = Object:extend()

function Node:new(x, y)
    self.x, self.y = x, y
end

function Node:update(dt)
    
end

function Node:draw()
    love.graphics.setColor(default_color)
    love.graphics.circle('line', self.x, self.y, 12)
end

So it's a simple object that doesn't extend from GameObject at all. And for now we'll just draw it at its position as a circle. If we go through the nodes list and call update/draw on each node we have in it, assuming we're locking the camera at position 0, 0 (unlike in Stage where we locked it at gw/2, gh/2) then it should look like this:

And as expected, both the nodes we defined in the tree file are shown here.


Camera

To make the skill tree work properly we have to change the way the camera works a bit. Right now we should have the same behavior we have from the Stage room, which means that the camera is simply locked to a position but doesn't do anything interesting. But on the SkillTree we want the camera to be able to be moved around with the mouse and for the player to be able to zoom out (and also back in) so he can see more of the tree at once.

To move it around, we want to make it so that whenever the player is holding down the left mouse button and dragging the screen around, it moves in the opposite direction. So if the player is holding the button and moves the mouse up, then we want to move the camera down. The basic way to achieve this is to keep track of the mouse's position on the previous frame as well as on this frame, and then move the camera in the opposite direction of the current_frame_position - previous_frame_position vector. All that looks like this:

function SkillTree:update(dt)
    ...
  
    if input:down('left_click') then
        local mx, my = camera:getMousePosition(sx, sy, 0, 0, sx*gw, sy*gh)
        local dx, dy = mx - self.previous_mx, my - self.previous_my
        camera:move(-dx, -dy)
    end
    self.previous_mx, self.previous_my = camera:getMousePosition(sx, sy, 0, 0, sx*gw, sy*gh)
end

And if you try this out it should behave as expected. Note that the camera:getMousePosition has been slightly changed from the default because of the way we're handling our canvases, which is different than what the library expected. I changed this a long long time ago so I don't remember why it is like this exactly, so I'll just go with it. But if you're curious you should look into this more clearly and examine if it needs to be this way, or if there's a way to use the default camera module without any changes that I just didn't figure it out properly.

As for the zooming in/out, we can simply change the camera's scale properly whenever the user presses wheel up/down:

function SKillTree:update(dt)
    ...
  	
    if input:pressed('zoom_in') then 
        self.timer:tween('zoom', 0.2, camera, {scale = camera.scale + 0.4}, 'in-out-cubic') 
    end
    if input:pressed('zoom_out') then 
        self.timer:tween('zoom', 0.2, camera, {scale = camera.scale - 0.4}, 'in-out-cubic') 
    end
end

We're using a timer here so that the zooms are a bit gentle and look better. We're also sharing both timers under the same 'zoom' id, since we want the other tween to stop whenever we start another one. The only thing left to do in this piece of code is to add limits to how low or high the scale can go, since we don't want it to go below 0, for instance.


Links and Stats

With the previous code we should be able to add nodes and move around the tree. Now we'll focus on linking nodes together and displaying their stats.

To link nodes together we'll create a Line object, and this Line object will receive in its constructors the id of two nodes that it's linking together. The id represents the index of a certain node on the tree object. So the node created from tree[2] will have id = 2. We can change the Node object like this:

function Node:new(id, x, y)
    self.id = id
    self.x, self.y = x, y
end

And we can create the Line object like this:

Line = Object:extend()

function Line:new(node_1_id, node_2_id)
    self.node_1_id, self.node_2_id = node_1_id, node_2_id
    self.node_1, self.node_2 = tree[node_1_id], tree[node_2_id]
end

function Line:update(dt)
    
end

function Line:draw()
    love.graphics.setColor(default_color)
    love.graphics.line(self.node_1.x, self.node_1.y, self.node_2.x, self.node_2.y)
end

Here we use our passed in ids to get the relevant nodes and store then in node_1 and node_2. Then we simply draw a line between the position of those nodes.

Back in the SkillTree room, we need to now create our Line objects based on the links table of each node in the tree. Suppose we now have a tree that looks like this:

tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 32, y = 0, links = {1, 3}}
tree[3] = {x = 32, y = 32, links = {2}}

We want node 1 to be linked to node 2, node 2 to be linked to 1 and 3, and node 3 to be linked to node 2. Implementation wise we want to over each node and over each of its links and then create Line objects based on those links.

function SkillTree:new()
    ...
  
    self.nodes = {}
    self.lines = {}
    for id, node in ipairs(tree) do table.insert(self.nodes, Node(id, node.x, node.y)) end
    for id, node in ipairs(tree) do 
        for _, linked_node_id in ipairs(node.links) do
            table.insert(self.lines, Line(id, linked_node_id))
        end
    end
end

One last thing we can do is draw the nodes using the 'fill' mode, otherwise our lines will go over them and it will look off:

function Node:draw()
    love.graphics.setColor(background_color)
    love.graphics.circle('fill', self.x, self.y, self.r)
    love.graphics.setColor(default_color)
    love.graphics.circle('line', self.x, self.y, self.r)
end

And after doing all that it should look like this:


As for the stats, supposing we have a tree like this:

tree[1] = {
    x = 0, y = 0, stats = {
    '4% Increased HP', 'hp_multiplier', 0.04, 
    '4% Increased Ammo', 'ammo_multiplier', 0.04
    }, links = {2}
}
tree[2] = {x = 32, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.04}, links = {1, 3}}
tree[3] = {x = 32, y = 32, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {2}}

We want to achieve this:

No matter how zoomed in or zoomed out, whenever the user mouses over a node we want to display its stats in a small rectangle.

The first thing we can focus on is figuring out if the player is hovering over a node or not. The simplest way to do this is to just check is the mouse's position is inside the rectangle that defines each node:

function Node:update(dt)
    local mx, my = camera:getMousePosition(sx*camera.scale, sy*camera.scale, 0, 0, sx*gw, sy*gh)
    if mx >= self.x - self.w/2 and mx <= self.x + self.w/2 and 
       my >= self.y - self.h/2 and my <= self.y + self.h/2 then 
      	self.hot = true
    else self.hot = false end
end

We have a width and height defined for each node and then we check if the mouse position mx, my is inside the rectangle defined by this width and height. If it is, then we set hot to true, otherwise it will be set to false. hot then is just a boolean that tells us if the node is being hovered over or not.

Now for drawing the rectangle. We want to draw the rectangle above everything else on the screen, so doing this inside the Node class doesn't work, since each node is drawn sequentially, which means that our rectangle would end up behind one or another node sometimes. So we'll do it directly in the SkillTree room. And perhaps even more importantly, we'll do it outside the camera:attach and camera:detach block, since we want the size of this rectangle to remain the same no matter how zoomed in or out we are.

The basics of it looks like this:

function SkillTree:draw()
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        ...
        camera:detach()

        -- Stats rectangle
        local font = fonts.m5x7_16
        love.graphics.setFont(font)
        for _, node in ipairs(self.nodes) do
            if node.hot then
                -- Draw rectangle and stats here
            end
        end
        love.graphics.setColor(default_color)
    love.graphics.setCanvas()
    ...
end

Before drawing the rectangle we need to figure out its width and height. The width is based on the size of the longest stat, since the rectangle has to be bigger than it by definition. To do that we can try something like this:

function SkillTree:draw()
    ...
        for _, node in ipairs(self.nodes) do
            if node.hot then
                local stats = tree[node.id].stats
                -- Figure out max_text_width to be able to set the proper rectangle width
                local max_text_width = 0
                for i = 1, #stats, 3 do
                    if font:getWidth(stats[i]) > max_text_width then
                        max_text_width = font:getWidth(stats[i])
                    end
                end
            end
        end
    ...
end

The stats variable will hold the list of stats for the current node. So if we're going through the node tree[2], stats would be {'4% Increased HP', 'hp_multiplier', 0.04, '4% Increased Ammo', 'ammo_multiplier', 0.04}. The stats table is divided in 3 elements always. First there's the visual description of the stat, then what variable it will change on the Player object, and then the amount of that effect. We want the visual description only, which means that we should go over this table in increments of 3, which is what we're doing in the for loop above.

Once we do that we want to find the width of that string given the font we're using, and for that we'll use font:getWidth. The maximum width of all our stats will be stored in the max_text_width variable and then we can start drawing our rectangle from there:

function SkillTree:draw()
    ...
        for _, node in ipairs(self.nodes) do
            if node.hot then
                ...
                -- Draw rectangle
                local mx, my = love.mouse.getPosition() 
                mx, my = mx/sx, my/sy
                love.graphics.setColor(0, 0, 0, 222)
                love.graphics.rectangle('fill', mx, my, 16 + max_text_width, 
        		font:getHeight() + (#stats/3)*font:getHeight())  
            end
        end
    ...
end

We want to draw the rectangle at the mouse position, except that now we don't have to use camera:getMousePosition because we're not drawing with the camera transformations. However, we can't simply use love.mouse.getPosition directly either because our canvas is being scaled by sx, sy, which means that the mouse position as returned by LÖVE's function isn't correct once we change the game's scale from 1. So we have to divide that position by the scale to get the appropriate value.

After we have the proper position we can draw the rectangle with width 16 + max_text_width, which gives us about 8 pixels on each side as a border, and then with height font:getHeight() + (#stats/3)*font:getHeight(). The first element of this calculation (font:getHeight()) serves the same purpose as 16 in the width calculation, which is to be just some value for a border. In this case the rectangle will have font:getHeight()/2 as a top and bottom border. The second part of is simply the amount of height each stat line takes. Since stats are grouped in threes, it makes sense to count each stat as #stats/3 and then multiply that by the line height.

Finally, the last thing to do is to draw the text. We know that the x position of all texts will be 8 + mx, because we decided we wanted 8 pixels of border on each side. And we also know that the y position of the first text will be my + font:getHeight()/2, because we decided we want font:getHeight()/2 as border on top and bottom. The only thing left to figure out is how to draw multiple lines, but we also already know this since we decided that the height of the rectangle would be (#stats/3)*font:getHeight(). This means that each line is drawn 1*font:getHeight(), 2*font:getHeight(), and so on. All that looks like this:

function SkillTree:draw()
    ...
        for _, node in ipairs(self.nodes) do
            if node.hot then
                ...
                -- Draw text
                love.graphics.setColor(default_color)
                for i = 1, #stats, 3 do
                    love.graphics.print(stats[i], math.floor(mx + 8), 
          			math.floor(my + font:getHeight()/2 + math.floor(i/3)*font:getHeight()))
                end
            end
        end
    ...
end

And this should get us the result we want. As a small note on this, if you look at this code as a whole it looks like this:

function SkillTree:draw()
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        ...
  
        -- Stats rectangle
        local font = fonts.m5x7_16
        love.graphics.setFont(font)
        for _, node in ipairs(self.nodes) do
            if node.hot then
                local stats = tree[node.id].stats
                -- Figure out max_text_width to be able to set the proper rectangle width
                local max_text_width = 0
                for i = 1, #stats, 3 do
                    if font:getWidth(stats[i]) > max_text_width then
                        max_text_width = font:getWidth(stats[i])
                    end
                end
                -- Draw rectangle
                local mx, my = love.mouse.getPosition() 
                mx, my = mx/sx, my/sy
                love.graphics.setColor(0, 0, 0, 222)
                love.graphics.rectangle('fill', mx, my, 
        		16 + max_text_width, font:getHeight() + (#stats/3)*font:getHeight())
                -- Draw text
                love.graphics.setColor(default_color)
                for i = 1, #stats, 3 do
                    love.graphics.print(stats[i], math.floor(mx + 8), 
          			math.floor(my + font:getHeight()/2 + math.floor(i/3)*font:getHeight()))
                end
            end
        end
        love.graphics.setColor(default_color)
    love.graphics.setCanvas()
  
    ...
end

And I know that if I looked at code like this a few years ago I'd be really bothered by it. It looks ugly and unorganized and perhaps confusing, but in my experience this is the stereotypical game development drawing code. Lots of small and seemingly random numbers everywhere, pixel adjustments, lots of different concerns instead of the whole thing feeling cohesive, and so on. I'm very used to this type of code by now so it doesn't bother me anymore, and I'd advise you to get used to it too because trying to make it "cleaner", in my experience, only leads to things that are even more confusing and less intuitive to work with.


Gameplay

Now that we can place nodes and link them together we have to code in the logic behind buying nodes. The tree will have one or multiple "entry points" from which the player can start buying nodes, and then from there he can only buy nodes that adjacent to one he already bought. For instance, in the way I set my own tree up, there's a central starting node that provides no bonuses and then from it 4 additional ones connect out to start the tree:

Suppose now that we have a tree that looks like this initially:

tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}}

The first thing we wanna do is make it so that node #1 is already activated while the others are not. What I mean by a node being activated is that it has been bought by the player and so its effects will be applied in gameplay. Since node #1 has no effects, in this way we can create an "initial node" from where the tree will expand.

The way we'll do this is through a global table called bought_node_indexes, which will just contain a bunch of numbers pointing to which nodes of the tree have already been bought. In this case we can just add 1 to it, which means that tree[1] will be active. We also need to change the nodes and links visually a bit so we can more easily see which ones are active or not. For now we'll simply show locked nodes as grey (with alpha = 32 instead of 255) instead of white:

function Node:update(dt)
    ...

    if fn.any(bought_node_indexes, self.id) then self.bought = true
    else self.bought = false end
end

function Node:draw()
    local r, g, b = unpack(default_color)
    love.graphics.setColor(background_color)
    love.graphics.circle('fill', self.x, self.y, self.w)
    if self.bought then love.graphics.setColor(r, g, b, 255)
    else love.graphics.setColor(r, g, b, 32) end
    love.graphics.circle('line', self.x, self.y, self.w)
    love.graphics.setColor(r, g, b, 255)
end

And for the links:

function Line:update(dt)
    if fn.any(bought_node_indexes, self.node_1_id) and 
       fn.any(bought_node_indexes, self.node_2_id) then 
      	self.active = true 
    else self.active = false end
end

function Line:draw()
    local r, g, b = unpack(default_color)
    if self.active then love.graphics.setColor(r, g, b, 255)
    else love.graphics.setColor(r, g, b, 32) end
    love.graphics.line(self.node_1.x, self.node_1.y, self.node_2.x, self.node_2.y)
    love.graphics.setColor(r, g, b, 255)
end

We only activate a line if both of its nodes have been bought, which makes sense. If we say that bought_node_indexes = {1} in the SkillTree room constructor, now we'd get something like this:

And if we say that bought_node_indexes = {1, 2}, then we'd get this:

And this is working as we expected. Now what we want to do is add the logic necessary so that whenever we click on a node it will be bought if its connected to another node that has been bought. Figuring out if we have enough skill points to buy a certain node, or to add a confirmation step before fully committing to buying the node will be left as an exercise.

Before we make it so that only nodes connected to other bought nodes can be bought, we first must fix a small problem with the way we're defining our tree. This is the definition we have now:

tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}}

One of the problems with this definition is that it's unidirectional. And this is a reasonable thing to expect, since if it were unidirectional we'd have to define connections multiple times across multiple nodes like this:

tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {1, 3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {2, 4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04, links = {3}}}

And while there's no big problem in having to do this, we can make it so that we only have to define connections once (in either direction) and then we can apply an operation that will automatically make connections also be defined in the opposing direction.

The way we can do this is by going over the list of all nodes, and then for each node going over its links. For each link we find, we go over to that node and add the current node to its links. So, for instance, if we're on node 1 and we see that it's linked to 2, then we move over to node 2 and add 1 to its links list. In this way we'll make sure that whenever we have a definition going one way it will also go the other. In code this looks like this:

function SkillTree:new()
    ...
    self.tree = table.copy(tree)
    for id, node in ipairs(self.tree) do
        for _, linked_node_id in ipairs(node.links or {}) do
            table.insert(self.tree[linked_node_id], id)
        end
    end
    ...
end

The first thing to notice here is that instead of using the global tree variable now, we're copying it locally to the self.tree attribute and then using that attribute instead. Everywhere on the SkillTree, Node and Line objects we should change references to the global tree to the local SkillTree tree attribute instead. We need to do this because we're going to change the tree's definition by adding numbers to the links table of some nodes, and generally (because of what I outlined in article 10) we don't want to be changing global variables in that way. This means that every time we enter the SkillTree room, we'll copy the global definition over to a local one and use the local one instead.

Given this, we now go over all nodes in the tree and back-link nodes to each other like we said we would. It's important to use node.links or {} inside the ipairs call because some nodes might not have their links table defined. It's also important to note that we do this before creating Node and Line objects, even though it's not really necessary to do that.

An additional thing we can do here is to note that sometimes a links table will have repeated values. Depending on how we define the tree table sometimes we'll place nodes bi-directionally, which means that they'll already be everywhere they should be. This isn't really a problem, except that it might result in the creation of multiple Line objects. So to prevent that, we can go over the tree again and make it so that all links tables only contain unique values:

function SkillTree:new()
    ...
    for id, node in ipairs(self.tree) do
        if node.links then
            node.links = fn.unique(node.links)
        end
    end
    ...
end

Now the only thing left to do is making it so that whenever we click a node, we check to see if its linked to an already bought node:

function Node:update(dt)
    ...
    if self.hot and input:pressed('left_click') then
        if current_room:canNodeBeBought(self.id) then
            if not fn.any(bought_node_indexes, self.id) then
                table.insert(bought_node_indexes, self.id)
            end
        end
    end
    ...
end

And so this means that if a node is being hovered over and the player presses the left click button, we'll check to see if this node can be bought through SkillTree's canNodeBeBought function (which we still have to implement), and then if it can be bought we'll add it to the global bought_node_indexes table. Here we also take care to not add a node twice to that table. Although if we add it more than once it won't really change anything or cause any bugs.

The canNodeBeBought function will work by going over the linked nodes to the node that was passed in and seeing if any of them are inside the bought_node_indexes table. If that's true then it means this node is connected to an already bought node which means that it can be bought:

function SkillTree:canNodeBeBought(id)
    for _, linked_node_id in ipairs(self.tree[id]) do
        if fn.any(bought_node_indexes, linked_node_id) then return true end
    end
end

And this should work as expected:

The very last idea we'll go over is how to apply our selected nodes to the player. This is simpler than it seems because of how we decided to structure everything in articles 11 and 12. The tree definition looks like this now:

tree = {}
tree[1] = {x = 0, y = 0, links = {2}}
tree[2] = {x = 48, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}, links = {3}}
tree[3] = {x = 96, y = 0, stats = {'6% Increased HP', 'hp_multiplier', 0.06}, links = {4}}
tree[4] = {x = 144, y = 0, stats = {'4% Increased HP', 'hp_multiplier', 0.04}}

And if you notice, we have the second stat value being a string that should point to a variable defined in the Player object. In this case the variable is hp_multiplier. If we go back to the Player object and look for where hp_multiplier is used we'll find this:

function Player:setStats()
    self.max_hp = (self.max_hp + self.flat_hp)*self.hp_multiplier
    self.hp = self.max_hp
    ...
end

It's used in the setStats function as a multiplier for our base HP added by some flat HP value, which is what we expected. The behavior we want out of the tree is that for all nodes inside bought_node_indexes, we'll apply their stat to the appropriate player variable. So if we have nodes 2, 3 and 4 inside that table, then the player should have an hp_multiplier that is equal to 1.14 (0.04+0.06+0.04 + the base which is 1). We can do this fairly simply like this:

function treeToPlayer(player)
    for _, index in ipairs(bought_node_indexes) do
        local stats = tree[index].stats
        for i = 1, #stats, 3 do
            local attribute, value = stats[i+1], stats[i+2]
            player[attribute] = player[attribute] + value
        end
    end
end

We define this function in tree.lua. As expected, we're going over all bought nodes and then going over all their stats. For each stat we're taking the attribute ('hp_multiplier') and the value (0.04, 0.06) and applying it to the player. In the example we talked the player[attribute] = player[attribute] + value line is parsed to player.hp_multiplier = player.hp_multiplier + 0.04 or player.hp_multiplier = player.hp_multiplier + 0.06, depending on which node we're currently looping over. This means that by the end of the outer for, we'll have applied all passives we bought to the player's variables.

It's important to note that different passives will need to be handled slightly differently. Some passives are booleans, others should be applied to variables which are Stat objects, and so on. All those differences need to be handled inside this function.


224. (CONTENT) Implement skill points. We have a global skill_points variable which holds how many skill points the player has. This variable should be decreased by 1 whenever the player buys a new node in the skill tree. The player should not be allowed to buy more nodes if he has no skill points. The player can buy a maximum of 100 nodes. You may also want to change these numbers around a bit if you feel like it's necessary. For instance, in my game the cost of each node increases based on how many nodes the player has already bought.

225. (CONTENT) Implement a step before buying nodes where the player can cancel his choices. This means that the player can click on nodes as if they were being bought, but to confirm the purchase he has to hit the "Apply Points" button. All selected nodes can be cancelled if he clicks the "Cancel" button instead. This is what it looks like:

226. (CONTENT) Implement the skill tree. You can implement this skill tree to whatever size you see fit, but obviously the bigger it is the more possible interactions there will be and the more interesting it will be as well. This is what my tree looks like for reference:

Don't forget to add the appropriate behaviors for each different type of passive in the treeToPlayer function!


END

And with that we end this article. The next article will focus on the Console room and the one after that will be the final one. In the final article we'll go over a few things, one of them being saving and loading things. One aspect of the skill tree that we didn't talk about was saving the player's bought nodes. We want those nodes to remain bought through playthroughs as well as after the player closes the game, so in the final article we'll go over this in more detail.

And like I said multiple times before, if you don't feel like it you don't need to implement a skill tree. If you've followed along so far then you already have all the passives implemented from articles 11 and 12 and you can present them to the player in whatever way you see fit. I chose a tree, but you can choose something else if you don't feel like doing a big tree like this manually is a good idea.



Procedural Dungeon Generation #1

2013-06-30 23:43

This post briefly explains a technique for generating randomized dungeons that is loosely based on the one explained here:


Grid Generation

Generate a grid:


Grid Difficulty Coloring

Color the grid with x red rooms (hard), y blue rooms (medium) and z green rooms (easy) such that x+y+z = n rooms in the grid. x, y and z will be used as controls for difficulty.

  1. First color the grid with x red rooms such that no red room has another red neighbor;
  2. Then for each red room color one neighbor blue and one neighbor green;
  3. Color the rest of the rooms randomly with the remaining number of rooms for each color.


Dungeon Path Creation

Choose two nodes that are far apart enough and then find a path between them while mostly avoiding red rooms. If you choose a proper x, since red rooms can't be neighbors to themselves and the pathfinding algorithm doesn't go for diagonals, it should create a not so direct path from one node to the other.

For all nodes in the path, add their red, blue or green neighbors. This should add the possibility of side paths and overall complexity/difficulty in the dungeon.


Room Creation and Connection

Join smaller grids into bigger ones according to predefined room sizes. Use a higher chances for smaller widths/heights and lower chances for bigger widths/heights.

Generate all possible connections between rooms.

Randomly remove connections until a certain number of connections per room is met. If n = total rooms, then set n*a rooms with >=4 connections, n*b with 3, n*c with 2 and n*d with 1, such that a+b+c+d=1. Controlling a, b, c and d lets you control how mazy the dungeon gets. If c or d are considerably higher than a or b then there won't be many hub rooms that connect multiple different paths, so it's gonna have lots of different thin paths with dead ends. If a or b are higher then the dungeon will be super connected and therefore easier.

Reconnect isolated islands. Since the last step is completely random, there's a big chance that rooms or groups of rooms will become unreachable.

To fix that:

  1. Flood fill to figure out how many isolated groups exist;
  2. Pick a group at random and go through its rooms. For each room check if it neighbors a room belonging to another group, if it does then connect them and end the search;
  3. Repeat the previous steps until there's only one group left (everyone's connected).


END

And that's it, I guess. There's more stuff like adding special rooms, but that's specific to each game, so whatever. But basically I have a few parameters I can control when creating a new dungeon: dungeon width/height; percentage of hard, medium and easy rooms; what color neighbors get added to the original path and the percentage of rooms with >=4, 3, 2 or 1 connections. I think that's enough to have control of how easy/hard the dungeon will be...

Procedural Dungeon Generation #3

This post will explain a technique for generating an overmap lightly inspired by the one in Slay the Spire. The general way it works is like this:


Physics Step

The first thing we want to do is generate the nodes that will make up our graph. The way I decided to do this was by spawning a bunch of box2d circles and letting the physics engine separate them as the simulation moves forwards. We create all those circles inside a rectangular boundary for this example and the way it looks is like this:

The code to make this happen is fairly simple. For instance, the function to create a circle looks like this:

circles = {}
local function create_physics_circle(x, y, radius)
    local circle = {}
    circle.body = love.physics.newBody(world, x, y, 'dynamic')
    circle.shape = love.physics.newCircleShape(radius)
    circle.fixture = love.physics.newFixture(circle.body, circle.shape)
    circle.radius = radius
    table.insert(circles, circle)
end

This is a pretty standard way of creating a physics circle with box2d and should be the same or similar everywhere. And then we create multiple of those physics circles like this:

radii = {}
for i = 1, 32 do table.insert(radii, 6) end
for i = 1, 18 do table.insert(radii, 8) end
for i = 1, 12 do table.insert(radii, 10) end
for i = 1, #radii do create_physics_circle(game_width/2 + 4*(2*love.math.random()-1), game_height/2 + 4*(2*love.math.random()-1), table.remove(radii, love.math.random(1, #radii))) end

Here there's a simple table full of different values 6, 8 and 10 which represent the different sizes for all possible circles. This way we can create a graph that already has differently sized circles which will correspond to different types of nodes in the overmap. One of the advantages of using the physics engine here is that we can create objects of different shapes and sizes, as well as play with the shape and size of the bounding object, and everything will still be neatly separated without any extra work.

After this we can move the simulation forward until we're happy with it. I choose 500 steps but you may choose a different number:

for i = 1, 500 do world:update(dt) end

After the simulation is done we can transform these physics objects into nodes that will make up the graph that represents the overmap:

graph = {}
for _, circle in ipairs(circles) do 
    circle.x, circle.y = circle.body:getPosition()
    table.insert(graph, {x = circle.x, y = circle.y, radius = circle.radius}) 
end
world:destroy()

Additionally we also destroy the physics world we previously created and all the bodies, fixtures and shapes with it. The only data structure we care about now is graph, which contains all the nodes as tables with x, y and radius attributes.


Pathing Step

In this step we'll take this graph, connect all nodes to their nearest neighbors, and then create the paths in the graph that will represent our final overmap. The first step is the connection of all nodes which looks like this:

And the code to achieve that is this:

for i, node_1 in ipairs(graph) do
    for j, node_2 in ipairs(graph) do
        if i ~= j and distance(node_1.x, node_1.y, node_2.x, node_2.y) < 1.1*(node_1.radius + node_2.radius) then
            if not node_1.neighbors then node_1.neighbors = {} end
            table.insert(node_1.neighbors, node_2)
        end
    end
end

Here we're just going through all nodes and for each node we're checking which other nodes are close enough and adding those nodes to a neighbors attribute. Now each node is a table with x, y, radius and neighbors, which is another table containing references to other nodes in the graph table.

The next two things we'll do is select the nodes that will make up our overmap. This will be divided into vertical and horizontal steps. The vertical step will pick a random at the top of our graph and walk randomly down until it can't do that anymore. We'll repeat this step twice.

The horizontal step will pick a randomly already selected node from one of the vertical steps and then walk randomly either left or right based on how far away the picked node is from left/right edge. If a node is further away from the left edge then we will walk left, otherwise we'll walk right.

All this put together looks like this:

For the purposes of these gifs each vertical step is colored red and green, and the horizontal steps are colored blue. The code to achieve the vertical step looks like this:

local function create_path_from_top_to_bottom(i)
    -- Pick a new node until one that isn't selected is picked
    local node = get_random_top_node()
    while node.selected do node = get_random_top_node() end
    node.selected = i

    -- Pick random neighbor down until no more random neighbors down can be picked
    local down = get_random_neighbor_down(node)
    while down do
        down.selected = i
        down = get_random_neighbor_down(down)
    end
end

This picks a top node until it picks one that isn't selected, and here we also set the selected attribute to the number passed in to the function. For the vertical step we'll call create_path_from_top_to_bottom(1) and create_path_from_top_to_bottom(2), and then when we draw each node we'll use these numbers to tell which step of the algorithm each node belongs to; this is done simply for educational/visual purposes. In reality we'd just set node.selected = true instead of a number and the logic would work the same.

After we pick a random node we walk down from it until we can't anymore. get_random_neighbor_down will go through the neighbors attribute of a node and return a random one that has a higher y attribute. If there are no neighbors nodes with a higher y attribute then it simply returns nil, and based on how we set this while loop up it means it will stop picking more nodes.

The same exact logic applies to the horizontal steps, just instead of picking a random top node we pick a random selected node, and we also need some additional logic to decide if we should walk left or right.


Final step

In the final step we'll take the nodes in our graph data structure and create the objects that will make our overmap in the game. The end result looks like this:

I'm not going to focus much on this step since it's the one that's most specific to whatever game, but the most important thing is the mapping from the physics world positions to the positions in the actual game. The way we initially created the boundary was like this:

boundary.x1, boundary.y1 = game_width/2 + 40, game_height/2 + 81
boundary.x2, boundary.y2 = game_width/2 - 40, game_height/2 - 81 

And so what this means is that the rectangle that defines the boundaries of our nodes have width 80 and height 162, centered at game_width, game_height. These numbers were not chosen randomly, because I want the overmap to have a width of 400 and a height of 810, as the size of one screen is 480x270 and this means that in terms of width the overmap will cover almost the whole screen (leaving some left over for potentially extra information on one of the sides) and in terms of height it will cover exactly 3 screens. All this means that when translating the positions of our graph nodes to actual world objects we just need to take these measurements into account, and that would look like this:

for _, node in ipairs(graph) do
    if node.selected then
        node.x = remap(node.x, game_width/2 - 40, game_width/2 + 40, 0, 400), 
        node.y = remap(node.y, game_height/2 - 81, game_height/2 + 81, -540, 270)
    end
end

Here the remap function changes the first value from its previous range to a new one. So, for instance, remap(5, 0, 10, 5, 0) returns 2.5. In this simple way we can change the position of our nodes to the position they should be in the world. And finally, now that we have all our nodes in their proper places we can create our game objects from the graph data structure. I won't go over this since it's specific to each game, but you should be able to continue from here by yourself. Good luck!


The source code for all this is available here. You can run it by using LÖVE and it's written in Lua. I tried to write it in the simplest way possible and without using anything too specific to Lua, so it should be easy to follow if you use other languages. To stop at each stage of the tutorial, simply remove the call to the next stage from the previous one, for example, remove the call to pathing_step if you want to stop at the end of physics_step to see what the circles look like after they're simulated.


Procedural Dungeon Generation #2

2015-08-30 22:29

This post explains a technique for generating randomized dungeons that was first described by TinyKeepDev here. I'll go over it in a little more detail than the steps in the original post. The general way the algorithm works is this:


Generate Rooms

First you wanna generate some rooms with some width and height that are placed randomly inside a circle. TKdev's algorithm used the normal distribution for generating room sizes and I think that this is generally a good idea as it gives you more parameters to play with. Picking different ratios between width/height mean and standard deviation will generally result in different looking dungeons.

One function you might need to do this is getRandomPointInCircle:

function getRandomPointInCircle(radius)
  local t = 2*math.pi*math.random()
  local u = math.random()+math.random()
  local r = nil
  if u > 1 then r = 2-u else r = u end
  return radius*r*math.cos(t), radius*r*math.sin(t)
end

You can get more info on how that works exactly here. And after that you should be able to do something like this:

One very important thing that you have to consider is that since you're (at least conceptually) dealing with a grid of tiles you have to snap everything to that same grid. In the gif above the tile size is 4 pixels, meaning that all room positions and sizes are multiples of 4. To do this I wrap position and width/height assignments in a function that rounds the number to the tile size:

function roundm(n, m)
  return math.floor(((n + m - 1)/m))*m
end

-- Now we can change the returned value from getRandomPointInCircle to:
function getRandomPointInCircle(radius)
  ...
  return roundm(radius*r*math.cos(t), tile_size), roundm(radius*r*math.sin(t), tile_size)
end

Separate Rooms

Now we can move on to the separation part. There's a lot of rooms mashed together in one place and they should not be overlapping somehow. TKdev used the separation steering behavior to do this but I found that it's much easier to just use a physics engine. After you've added all rooms, simply add solid physics bodies to match each room's position and then just run the simulation until all bodies go back to sleep. In the gif I'm running the simulation normally but when you're doing this between levels you can advance the physics simulation faster.

The physics bodies themselves are not tied to the tile grid in any way, but when setting the room's position you wrap it with the roundm call and then you get rooms that are not overlapping with each other and that also respect the tile grid. The gif below shows this in action as the blue outlines are the physics bodies and there's always a slight mismatch between them and the rooms since their position is always being rounded:

One issue that might come up is when you want to have rooms that are skewed horizontally or vertically. For instance, consider the game I'm working on:

Combat is very horizontally oriented and so I probably want to have most rooms being bigger in width than they are in height. The problem with this lies in how the physics engine decides to resolve its collisions whenever long rooms are near each other:

As you can see, the dungeon becomes very tall, which is not ideal. To fix this we can spawn rooms initially inside a thin strip instead of on a circle. This ensures that the dungeon itself will have a decent width to height ratio:

To spawn randomly inside this strip we can just change the getRandomPointInCircle function to spawn points inside an ellipse instead (in the gif above I used ellipse_width = 400 and ellipse_height = 20):

function getRandomPointInEllipse(ellipse_width, ellipse_height)
  local t = 2*math.pi*math.random()
  local u = math.random()+math.random()
  local r = nil
  if u > 1 then r = 2-u else r = u end
  return roundm(ellipse_width*r*math.cos(t)/2, tile_size), 
         roundm(ellipse_height*r*math.sin(t)/2, tile_size)
end

Main Rooms

The next step simply determines which rooms are main/hub rooms and which ones aren't. TKdev's approach here is pretty solid: just pick rooms that are above some width/height threshold. For the gif below the threshold I used was 1.25*mean, meaning, if width_mean and height_mean are 24 then rooms with width and height bigger than 30 will be selected.


Delaunay Triangulation + Graph

Now we take all the midpoints of the selected rooms and feed that into the Delaunay procedure. You either implement this procedure yourself or find someone else who's done it and shared the source. In my case I got lucky and Yonaba already implemented it. As you can see from that interface, it takes in points and spits out triangles:

After you have the triangles you can then generate a graph. This procedure should be fairly simple provided you have a graph data structure/library at hand. In case you weren't doing this already, it's useful that your Room objects/structures have unique ids to them so that you can add those ids to the graph instead of having to copy them around.


Minimum Spanning Tree

After this we generate a minimum spanning tree from the graph. Again, either implement this yourself or find someone who's done it in your language of choice.

The minimum spanning tree will ensure that all main rooms in the dungeon are reachable but also will make it so that they're not all connected as before. This is useful because by default we usually don't want a super connected dungeon but we also don't want unreachable islands. However, we also usually don't want a dungeon that only has one linear path, so what we do now is add a few edges back from the Delaunay graph:

This will add a few more paths and loops and will make the dungeon more interesting. TKdev arrived at 15% of edges being added back and I found that around 8-10% was a better value. This may vary depending on how connected you want the dungeon to be in the end.


Hallways

For the final part we want to add hallways to the dungeon. To do this we go through each node in the graph and then for each other node that connects to it we create lines between them. If the nodes are horizontally close enough (their y position is similar) then we create a horizontal line. If the nodes are vertically close enough then we create a vertical line. If the nodes are not close together either horizontally or vertically then we create 2 lines forming an L shape.

The test that I used for what close enough means calculates the midpoint between both nodes' positions and checks to see if that midpoint's x or y attributes are inside the node's boundaries. If they are then I create the line from that midpoint's position. If they aren't then I create two lines, both going from the source's midpoint to the target's midpoint but only in one axis.

In the picture above you can see examples of all cases. Nodes 62 and 47 have a horizontal line between them, nodes 60 and 125 have a vertical one and nodes 118 and 119 have an L shape. It's also important to note that those aren't the only lines I'm creating. They are the only ones I'm drawing but I'm also creating 2 additional lines to the side of each one spaced by tile_size, since I want my corridors to be at least 3 tiles wide in either width or height.

Anyway, after this we check to see which rooms that aren't main/hub rooms that collide with each of the lines. Colliding rooms are then added to whatever structure you're using to hold all this by now and they will serve as the skeleton for the hallways:

Depending on the uniformity and size of the rooms that you initially set you'll get different looking dungeons here. If you want your hallways to be more uniform and less weird looking then you should aim for low standard deviation and you should put some checks in place to keep rooms from being too skinny one way or the other.

For the last step we simply add 1 tile sized grid cells to make up the missing parts. Note that you don't actually need a grid data structure or anything too fancy, you can just go through each line according to the tile size and add grid rounded positions (that will correspond to a 1 tile sized cell) to some list. This is where having 3 (or more) lines happening instead of only 1 matters.

And then after this we're done! 🌀


END

The data structures I returned from this entire procedure were: a list of rooms (each room is just a structure with a unique id, x/y positions and width/height); the graph, where each node points to a room id and the edges have the distance between rooms in tiles; and then an actual 2D grid, where each cell can be nothing (meaning its empty), can point to a main/hub room, can point to a hallway room or can be hallway cell. With these 3 structures I think it's possible to get any type of data you could want out of the layout and then you can figure out where to place doors, enemies, items, which rooms should have bosses, and so on.

Creators and Mechanists in Indie Gamedev

One of the things I've noticed over time after watching myself and other people progress as indie game developers is that there are roughly two main types of personalities that indie gamedevs fall into. I'm going to call them "creators" and "mechanists".

Also, I decided to write this article as a somewhat of a response to this blog post by one of the itch.io people.


Creators

Creators are generally people who have an open and free-form way of looking at the world and doing things. They derive their sense of meaning from exploring the unknown and finding out what works from their explorations. Because they're dealing with the unknown their processes are likely to be undefined, murky and adaptable. Creator types also have a sense for the artistic appeal/value of something and generally they want to create games with that in mind.

Most indie developers are creator types. This is because indie game development is a very new and interesting field with lots of unknowns, but also because it's a field that enables the exercise of many different disciplines in an artistic way that creators really care about, so it will attract lots of them.

Here are some common things I've noticed about creator types:

  • Lack of consistency in productivity, works in bursts
  • Likely to work on and drop many many projects before completing a single one
  • Favors unique projects
  • Prefers the start of development on a project and dislikes the middle/end
  • Prefers creating games that are story/narrative based
  • Places the artistic value of a game above other concerns
  • If a game is successful, unlikely to work on sequels and likely to move on to a next project quickly

Mechanists

Mechanists are generally people who have a conservative and process oriented way of looking at the world and doing things. They derive their sense of meaning from operating on well defined processes as well as making sure that those processes are optimized. They're the types of people who will feel really bad if they aren't being productive every single day. Unlike creators, they have a lower sense for the artistic and they don't derive that much pleasure from exploring the unknown.

A minority of indie developers are mechanist types, most of them being programmers. This is simply because indie game development is too undefined a field for most mechanist types to try to venture into yet.

Here are some common things I've noticed about mechanists:

  • Consistent productivity, works about the same amount of time every day
  • Likely to stick with a single project for a long time
  • Favors safe projects
  • Dislikes the start of development and prefers the middle/end
  • Prefers creating games that are system based
  • Places the system depth of a game above other concerns
  • If a game is successful, either works on sequels or keeps developing the successful game for a long time

Now let's go part by part:

Productivity

Creators are by their nature undefined, unorganized and adaptable. They're built for dealing with the unknown, which means that attempts to bind them to more well defined processes is a direct attack in how they place themselves in the world. The way this is reflected in productivity is that, by default, they will work in bursts rather than in a consistent manner. Everyone knows someone (or is that someone) who gets a burst of progress done in a few weeks and then goes silent for months. This is a very common trait of creator types and often times one of the biggest challenge they face is just learning how to become more disciplined.

Mechanists generally don't have this problem. In indie gamedev, from my observations, lots of mechanists tend to be programmers who have worked as programmers in the real world, and so they already have some sense of discipline built into them because of their experiences (if not because of their personality).


Projects

Creators have a very disturbing propensity to never see projects through to the end. Everyone knows someone like that. They've been trying to make game for years, everything they make is extremely polished and looks really look, but they've never really finished and released anything for real. It's very common for creator types to be like this and to always be stuck in "exploration" mode, without ever committing for too long on a single project.

Because the majority of indie developers are creators, the advice around this is skewed towards what will work for creators. And that advice is generally something like, you try out a bunch of prototypes until you find something really fun and really promising (and this process is very fun for creators), and then you commit to that project and try to finish it. And of course, this fun prototype you settled on needs to be somewhat unique otherwise it's not good enough.

Mechanists are the opposite. Unlike creators they're more likely to spend less time deciding on what type of game to make. They don't like the process of exploring the unknown, and so instead of trying out many prototypes they'll simply pick something that is safe and proven enough and just execute it well. They will have a harder time getting their game off the ground, since at the start there's a lot of uncertainty and lots of high level decisions have to be made without knowing the details yet. But once this phase is through, mechanists will get more and more productive with time until release.

The mechanist way of making games isn't talked about much, but if it were it would be something like: pick a game mechanic that is proven to work and that you want to execute better and execute it better. The size of the project should be small enough that you can handle but you should aim high, since you want a game that has enough meat to it.


Start/end of development

Creators prefer the start of development. Like I just mentioned, they derive their sense of meaning from exploring the unknown, and the phase of development that has the most unknown exploration is the first. The idea of trying out of many prototypes until you find something fun is an idea that appeals a lot to creators because of this. The weakness of creators lies in the middle/end of development, when enough of the game is settled and decided on and you just have to do a lot of boring tasks to see the project through completion. Creators will often say that the last 10% feel like an eternity.

Mechanists dislike the start of development and prefer the other phases. Because they derive their sense of meaning from executing on well defined processes, the start of development feels too chaotic for them to enjoy. Picking safe and well-tested mechanics helps with this, as well as with giving them a realistic vision early on of where they can shine (what they need to execute better). Unlike creators, mechanists thrive as the project becomes more and more complete and they have no problem with doing the boring tasks that are needed to succeed.

An additional thing that follows from this is that creators will love game jams, while mechanists will generally not participate in them, especially if they have a very short time window like Ludum Dare.


Art and systems

Because creators value the artistic appeal of things a lot, they're more likely to create games where that appeal can be elevated, which means that they will generally favor games that are story/narrative based. This creates a problem for creators because, in general, gamers tend to not value narrative-type of games that much. There are many ways in which this conflict manifests itself in reality, but the one that comes to my mind now were concerns from narrative-based indie devs complaining to Valve that the 2 hour window for refunds would kill off games that had less than 2 hours of gameplay. I don't think that's what ended up happening, but the concerns were raised because it was an (unintentional) attack on creator types.

Mechanists on the other hand don't have that much concern for the artistic aspect of their games in the same way creators do. They tend to focus much more on creating games with many systems that come together in different ways to create a lot of depth, and generally also hook players into the game for a long time. In this sense, mechanists have an inherent advantage over creators, since the games they want to make are generally games the audience wants to play for a long time.


Success

If a creator's game is successful, they're much more likely to move on to another project quickly rather than stay with the one project for a long time. This is because they derive no pleasure from doing maintenance type of work, and they dislike being too far away from working on "new". Creators are unlikely to create sequels to their successful games, and if they do, the sequels have to be different enough to justify it.

If a mechanist's game is successful, they're much more like to keep working on it for a long time or to work on a very similar sequel. Mechanists don't want to play around with new ideas as much as creators, and once they find a formula that works and an audience willing to play games of that type, they see no reason to deviate too much from it. Creators see this pattern of behavior as a weakness or corruption of the creative process and they very much dislike it.

One of the things that follows from this is that mechanists can find a lot of success by just making spiritual sequels to successful types of games that were abandoned by successful creators.


Creators vs Mechanists

Now that we have all this in mind, the reason I disagree a lot with the itch.io blog post is because that post only cares about elevating creator types of games and completely rejects (unintentionally) what appeals to mechanists.

One of the things I mentioned is that mechanists have an inherent advantage compared to creators, since their games are generally aiming to be deep and played for very long amounts of time. The article mentioned explicitly rejects this notion:

Sure there will always be a place for mass-market experiences, but let’s also place value in intimate conversations between developer. Let’s look toward games that don’t stretch on for 100 hours but ones that can be completed on your lunch break.

Sentences like these are a direct attack on what mechanists care about. There are many others like it in the rest of the article. And coming from anyone we could just chalk it up to differences in taste, but itch.io has mentioned before that they manually curate their store and the person who wrote this article (and if not him someone else who shares the viewpoint) is doing that curation. This creates an inherent hostility from the platform to anyone who is paying attention and doesn't share those viewpoints.

Valve, on the other hand, seems to understand this wonderfully which at first surprised me, but now it doesn't anymore. You'd expect the big corporation to be out of touch and the small business to be nimble and agile, but sadly that's not the case here and Valve is just handling this way better than itch.io by not favoring any type of game over another and just letting the market decide.

This "conflict" between both mentalities also makes itself clear in other parts of the field. Journalists tend to side with creator types more often because they're creator types themselves. And since most gamers aren't like that and enjoy games made by mechanists, this increases the separation between journalists and gamers, which has resulted and continues to result in all sorts of problems.

In any case, I'm not going to push on these points much more because hopefully by now I've made what I mean clear. But I would urge everyone in the indie gamedev community to not make the mistake of ignoring mechanists. We are a minority but our games provide a lot of value! ^-^


END

And here are some examples of well known indie developers and where I think they fit:

Examples of likely creators

Examples of likely mechanists

Note that these are guesses. I don't know any of these people so I can just guess by their games and the way they act online. And there's also the fact that most people are both types to varying degrees. I think that ideally you wanna be slightly tilted towards the mechanist side of things because the overall environment is tilted to the creator side, and so there are advantages to being a contrarian in this way. Most people reading this are probably creators, which means that taking notes on what mechanists excel at and what they care about and trying to tilt yourself more that way might be advantageous as well.

BYTEPATH #11 - Passives

Introduction

In this article we'll go over the implementation of all passives in the game. There are a total of about 120 different things we will implement and those are enough to be turned into a very big skill tree (the tree I made has about 900 nodes, for instance).

This article will be filled with exercises tagged as content, and the way that will work is that I'll show you how to do something, and then give you a bunch of exercises to do that same thing but applying it to other stats. For instance, I will show you how to implement an HP multiplier, which is a stat that will multiply the HP of the player by a certain percentage, and then the exercises will ask for you to implement Ammo and Boost multipliers. In reality things will get a bit more complicated than that but this is the basic idea.

After we're done with the implementation of everything in this article we'll have pretty much have most of the game's content implemented and then it's a matter of finishing up small details, like building the huge skill tree out of the passives we implemented. :-D


Types of Stats

Before we start with the implementation of everything we need to first decide what kinds of passives our game will have. I already decided what I wanted to do so I'm going to just follow that, but you're free to deviate from this and come up with your own ideas.

The game will have three main types of passive values: resources, stat multipliers and chances.

  • Resources are HP, Boost and Ammo. These are values that are described by a max_value variable as well as a current_value variable. In the case of HP we have the maximum HP the player has, and then we also have the current amount.
  • Stat multipliers are multipliers that are applied to various values around the game. As the player goes around the tree picking up nodes, he'll be picking up stuff like "10% Increased Movement Speed", and so after he does that and starts a new match, we'll take all the nodes the player picked, pack them into those multiplier values, and then apply them in the game. So if the player picked nodes that amounted to 50% increased movement speed, then the movement speed multiplier will be applied to the max_v variable, so some mvspd_multiplier variable will be 1.5 and our maximum velocity will be multiplied by 1.5 (which is a 50% increase).
  • Chances are exactly that, chances for some event to happen. The player will also be able to pick up added chance for certain events to happen in different circumstances. For instance, "5% Added Chance to Barrage on Cycle", which means that whenever a cycle ends (the 5 second period we implemented), there's a 5% chance for the player to launch a barrage of projectiles. If the player picks up tons of those nodes then the chance gets higher and a barrage happens more frequently.

The game will have an additional type of node and an additional mechanic: notable nodes and temporary buffs.

  • Notable nodes are nodes that change the logic of the game in some way (although not always). For instance, there's a node that replaces your HP for Energy Shield. And with ES you take double damage, your ES recharges after you don't take damage for a while, and you have halved invulnerability time. Nodes like these are not as numerous as others but they can be very powerful and combined in fun ways.
  • Temporary buffs are temporary boosts to your stats. Sometimes you'll get a temporary buff that, say, increases your attack speed by 50% for 4 seconds.

Knowing all this we can get started. To recap, the current resource stats we have in our codebase should look like this:

function Player:new(...)
    ...
  
    -- Boost
    self.max_boost = 100
    self.boost = self.max_boost
    ...

    -- HP
    self.max_hp = 100
    self.hp = self.max_hp

    -- Ammo
    self.max_ammo = 100
    self.ammo = self.max_ammo
  
    ...
end

The movement code values should look like this:

function Player:new(...)
    ...
  	
    -- Movement
    self.r = -math.pi/2
    self.rv = 1.66*math.pi
    self.v = 0
    self.base_max_v = 100
    self.max_v = self.base_max_v
    self.a = 100
	
    ...
end

And the cycle values should look like this (I renamed all previous references to the word "tick" to be "cycle" now for consistency):

function Player:new(...)
    ...
  
    -- Cycle
    self.cycle_timer = 0
    self.cycle_cooldown = 5

    ...
end

HP multiplier

So let's start with the HP multiplier. In a basic way all we have to do is define a variable named hp_multiplier that starts as the value 1, and then we apply the increases from the tree to this variable and multiply it by max_hp at some point. So let's do the first thing:

function Player:new(...)
    ...
  	
    -- Multipliers
    self.hp_multiplier = 1
end

Now the second thing is that we have to assume we're getting increases to HP from the tree. To do this we have to assume how these increases will be passed in and how they'll be defined. Here I have to cheat a little (since I already wrote the game once) and say that the tree nodes will be defined in the following format:

tree[2] = {'HP', {'6% Increased HP', 'hp_multiplier', 0.06}}

This means that node #2 is named HP, has as its description 6% Increased HP, and affects the variable hp_multiplier by 0.06 (6%). There is a function named treeToPlayer which takes all 900~ of those node definitions and then applies them to the player object. It's important to note that the variable name used in the node definition has to be the same name as the one defined in the player object, otherwise things won't work out. This is a very thinly linked and error-prone method of doing it I think, but as I said in the previous article it's the kind of thing you can get away with because you're coding by yourself.

Now the final question is: when do we multiply hp_multiplier by max_hp? The natural option here is to just do it on the constructor, since that's when a new player is created, and a new player is created whenever a new Stage room is created, which is also when a new match starts. However, we'll do this at the very end of the constructor, after all resources, multipliers and chances have been defined:

function Player:new(...)
    ...
  
    -- treeToPlayer(self)
    self:setStats()
end

And so in the setStats function we can do this:

function Player:setStats()
    self.max_hp = self.max_hp*self.hp_multiplier
    self.hp = self.max_hp
end

And so if you set hp_multiplier to 1.5 for instance and run the game, you'll notice that now the player will have 150 HP instead of its default 100.

Note that we also have to assume the existence of the treeToPlayer function here and pass the player object to that function. Eventually when we write the skill tree code and implement that function, what it will do is set the values of all multipliers based on the bonuses from the tree, and then after those values are set we can call setStats to use those to change the stats of the player.


123. (CONTENT) Implement the ammo_multiplier variable.

124. (CONTENT) Implement the boost_multiplier variable.


Flat HP

Now for a flat stat. Flat stats are direct increases to some stat instead of a percentage based one. The way we'll do it for HP is by defining a flat_hp variable which will get added to max_hp (before being multiplied by the multiplier):

function Player:new(...)
    ...
  	
    -- Flats
    self.flat_hp = 0
end
function Player:setStats()
    self.max_hp = (self.max_hp + self.flat_hp)*self.hp_multiplier
    self.hp = self.max_hp
end

Like before, whenever we define a node in the tree we want to link it to the relevant variable, so, for instance, a node that adds flat HP could look like this:

tree[15] = {'Flat HP', {'+10 Max HP', 'flat_hp', 10}}

125. (CONTENT) Implement the flat_ammo variable.

126. (CONTENT) Implement the flat_boost variable.

127. (CONTENT) Implement the ammo_gain variable, which adds to the amount of ammo gained when the player picks one up. Change the calculations in the addAmmo function accordingly.


Homing Projectile

The next passive we'll implement is "Chance to Launch Homing Projectile on Ammo Pickup", but for now we'll focus on the homing projectile part. One of the attacks the player will have is a homing projectile so we'll just implement that as well now.

A projectile will have its homing function activated whenever the attack attribute is set to 'Homing'. The code that actually does the homing will be the same as the code we used for the Ammo resource:

function Projectile:update(dt)
    ...
    self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r))

    -- Homing
    if self.attack == 'Homing' then
    	-- Move towards target
        if self.target then
            local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized()
            local angle = math.atan2(self.target.y - self.y, self.target.x - self.x)
            local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized()
            local final_heading = (projectile_heading + 0.1*to_target_heading):normalized()
            self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y)
        end
    end
end

The only thing we have to do differently is defining the target variable. For the Ammo object the target variable points to the player object, but in the case of a projectile it should point to a nearby enemy. To get a nearby enemy we can use the getAllGameObjectsThat function that is defined in the Area class, and use a filter that will only select objects that are enemies and that are close enough. To do this we must first define what objects are enemies and what objects aren't enemies, and the easiest way to do that is to just have a global table called enemies which will contain a list of strings with the name of the enemy classes. So in globals.lua we can add the following definition:

enemies = {'Rock', 'Shooter'}

And as we add more enemies into the game we also add their string to this table accordingly. Now that we know which object types are enemies we can easily select them:

local targets = self.area:getAllGameObjectsThat(function(e)
    for _, enemy in ipairs(enemies) do
    	if e:is(_G[enemy]) then
            return true
        end
    end
end)

We use the _G[enemy] line to access the class definition of the current string we're looping over. So _G['Rock'] will return the table that contains the class definition of the Rock class. We went over this in multiple articles so it should be clear by now why this works.

Now for the other condition we want to select only enemies that are within a certain radius of this projectile. Through trial and error I came to a radius of about 400 units, which is not small enough that the projectile will never have a proper target, but not big enough that the projectile will try to hit offscreen enemies too much:

local targets = self.area:getAllGameObjectsThat(function(e)
    for _, enemy in ipairs(enemies) do
    	if e:is(_G[enemy]) and (distance(e.x, e.y, self.x, self.y) < 400) then
            return true
        end
    end
end)

distance is a function we can define in utils.lua which returns the distance between two positions:

function distance(x1, y1, x2, y2)
    return math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2))
end

And so after this we should have our enemies in the targets list. After that all we want to do is get a random one of them and point that as the target that the projectile will move towards:

self.target = table.remove(targets, love.math.random(1, #targets))

And all that should look like this:

function Projectile:update(dt)
    ...

    self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r))

    -- Homing
    if self.attack == 'Homing' then
        -- Acquire new target
        if not self.target then
            local targets = self.area:getAllGameObjectsThat(function(e)
                for _, enemy in ipairs(enemies) do
                    if e:is(_G[enemy]) and (distance(e.x, e.y, self.x, self.y) < 400) then
                        return true
                    end
                end
            end)
            self.target = table.remove(targets, love.math.random(1, #targets))
        end
        if self.target and self.target.dead then self.target = nil end

        -- Move towards target
        if self.target then
            local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized()
            local angle = math.atan2(self.target.y - self.y, self.target.x - self.x)
            local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized()
            local final_heading = (projectile_heading + 0.1*to_target_heading):normalized()
            self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y)
        end
    end
end

There's an additional line at the end of the block where we acquire a new target, where we set self.target to nil in case the target has been killed. This makes it so that whenever the target for this projectile stops existing, self.target will be set to nil and a new target will be acquired, since the condition not self.target will be met and then the whole process will repeat itself. It's also important to mention that once a target has been acquired we don't do any more calculations, so there's no big need to worry about the performance of getAllGameObjectsThat, which is a function that naively loops over all objects currently alive in the game.

One extra thing we have to do is change how the projectile object behaves whenever it's not homing or whenever there's no target. Intuitively using setLinearVelocity first to set the projectile's velocity once, and then using it again inside the if self.attack == 'Homing' loop would make sense, since the velocity would only be changed if the projectile is in fact homing and if a target exists. But for some reason doing that results in all sorts of problems, so we have to make sure we only call setLinearVelocity once, and that implies something like this:

-- Homing
if self.attack == 'Homing' then
    ...
-- Normal movement
else self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end

This is a bit more confusing than the previous setup but it works. And if we test all this and create a projectile with the attack attribute set to 'Homing' it should look like this:


128. (CONTENT) Implement the Homing attack. Its definition on the attacks table looks like this:

attacks['Homing'] = {cooldown = 0.56, ammo = 4, abbreviation = 'H', color = skill_point_color}

And the attack itself looks like this:

Note that the projectile for this attack (as well as others that are to come) is slightly different. It's a rhombus half colored as white and half colored as the color of the attack (in this case skill_point_color), and it also has a trail that's the same as the player's.


Chance to Launch Homing Projectile on Ammo Pickup

Now we can move on to what we wanted to implement, which is this chance-type passive. This one is has a chance to be triggered whenever we pick the Ammo resource up. We'll hold this chance in the launch_homing_projectile_on_ammo_pickup_chance variable and then whenever an Ammo resource is picked up, we'll call a function that will handle rolling the chances for this event to happen.

But before we can do that we need to specify how we'll handle these chances. As I introduced in another article, here we'll also use the chanceList concept. If an event has 5% probability of happening, then we want to make sure that it will actually follow that 5% somewhat reasonably, and so it just makes sense to use chanceLists.

The way we'll do it is that after we call the setStats function on the Player's constructor, we'll also call a function called generateChances which will create all the chanceLists we'll use throughout the game. Since there will be lots and lots of different events that will need to be rolled we'll put all chanceLists into a table called chances, and organize things that so whenever we need to roll for a chance of something happening, we can do something like:

if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then
    -- launch homing projectile
end

We could set up the chances table manually, so that every time we add a new _chance type variable that will hold the chances for some event to happen, we also add and generate its chanceList in the generateChances function. But we can be a bit clever here and decide that every variable that deals with chances will end with _chance, and then we can use that to our advantage:

function Player:generateChances()
    self.chances = {}
    for k, v in pairs(self) do
        if k:find('_chance') and type(v) == 'number' then
      
        end
    end
end

Here we're going through all key/value pairs inside the player object and returning true whenever we find an attribute that contains in its name the _chance substring, as well as being a number. If both those things are true then based on our own decision this is a variable that is dealing with chances of some event happening. So now all we have to do is then create the chanceList and add it to the chances table:

function Player:generateChances()
    self.chances = {}
    for k, v in pairs(self) do
        if k:find('_chance') and type(v) == 'number' then
      	    self.chances[k] = chanceList({true, math.ceil(v)}, {false, 100-math.ceil(v)})
      	end
    end
end

And so this will create a chanceList of 100 values, with v of them being true, and 100-v of them being false. So if the only chance-type variable we had defined in our player object was the launch_homing_projectile_on_ammo_pickup_chance one, and this had the value 5 attached to it (meaning 5% probability of this event happening), then the chanceList would have 5 true values and 95 false ones, which gets us what we wanted.

And so if we call generateChances on the player's constructor:

function Player:new(...)
    ...
  
    -- treeToPlayer(self)
    self:setStats()
    self:generateChances()
end

Then everything should work fine. We can now define the launch_homing_projectile_on_ammo_pickup_chance variable:

function Player:new(...)
    ...
  	
    -- Chances
    self.launch_homing_projectile_on_ammo_pickup_chance = 0
end

And if you wanna test that the roll system works, you can set that to a value like 50 and then call :next() a few times to see what happens.

The implementation of the actual launching will happen through the onAmmoPickup function, which will be called whenever Ammo is picked up:

function Player:update(dt)
    ...
    if self.collider:enter('Collectable') then
        ...
    
        if object:is(Ammo) then
            object:die()
            self:addAmmo(5)
            self:onAmmoPickup()
      	...
    end
end

And that function then would look like this:

function Player:onAmmoPickup()
    if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then
        local d = 1.2*self.w
        self.area:addGameObject('Projectile', 
      	self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), 
      	{r = self.r, attack = 'Homing'})
        self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'})
    end
end

And then all that would end up looking like this:


129. (CONTENT) Implement the regain_hp_on_ammo_pickup_chance passive. The amount of HP regained is 25 and should be added with the addHP function, which adds the given amount of HP to the hp value, making sure that it doesn't go above max_hp. Additionally, an InfoText object should be created with the text 'HP Regain!' in hp_color.

130. (CONTENT) Implement the regain_hp_on_sp_pickup_chance passive. he amount of HP regained is 25 and should be added with the addHP function. An InfoText object should be created with the text 'HP Regain!' in hp_color. Additionally, an onSPPickup function should be added to the Player class and in it all this work should be done (like we did with the onAmmoPickup function).


Haste Area

The next passives we want to implement are "Chance to Spawn Haste Area on HP Pickup" and "Chance to Spawn Haste Area on SP Pickup". We already know how to do the "on Resource Pickup" part, so now we'll focus on the "Haste Area". A haste area is simply a circle that boosts the player's attack speed whenever he is inside it. This boost in attack speed will be applied as a multiplier, so it makes sense for us to implement the attack speed multiplier first.


ASPD multiplier

We can define an ASPD multiplier simply as the aspd_multiplier variable and then multiply this variable by our shooting cooldown:

function Player:new(...)
    ...
  	
    -- Multipliers
    self.aspd_multiplier = 1
end
function Player:update(dt)
    ...
  
    -- Shoot
    self.shoot_timer = self.shoot_timer + dt
    if self.shoot_timer > self.shoot_cooldown*self.aspd_multiplier then
        self.shoot_timer = 0
        self:shoot()
    end
end

The main difference that this one multiplier in particular will have is that lower values are better than higher values. In general, if a multiplier value is 0.5 then it's cutting whatever stat it's being applied to by half. So for HP, movement speed and pretty much everything else this is a bad thing. However, for attack speed lower values are better, and this can be simply explained by the code above. Since we're applying the multiplier to the shoot_cooldown variable, lower values means that this cooldown will be lower, which means that the player will shoot faster. We'll use this knowledge next when creating the HasteArea object.


Haste Area

And now that we have the ASPD multiplier we can get back to this. What we want to do here is to create a circular area that will decrease aspd_multiplier by some amount as long as the player is inside it. To achieve this we'll create a new object named HasteArea which will handle the logic of seeing if the player is inside it or not and setting the appropriate values in case he is. The basic structure of the object looks like this:

function HasteArea:new(...)
    ...
  
    self.r = random(64, 96)
    self.timer:after(4, function()
        self.timer:tween(0.25, self, {r = 0}, 'in-out-cubic', function() self.dead = true end)
    end)
end

function HasteArea:update(dt)
    ...
end

function HasteArea:draw()
    love.graphics.setColor(ammo_color)
    love.graphics.circle('line', self.x, self.y, self.r + random(-2, 2))
    love.graphics.setColor(default_color)
end

For the logic behind applying the actual effect we have to keep track of when the player enters/leaves the area and then modify the aspd_multiplier value once that happens. The way to do this looks something like this:

function HasteArea:update(dt)
    ...
  	
    local player = current_room.player
    if not player then return end
    local d = distance(self.x, self.y, player.x, player.y)
    if d < self.r and not player.inside_haste_area then -- Enter event
        player:enterHasteArea()
    elseif d >= self.r and player.inside_haste_area then -- Leave event
    	player:exitHasteArea()
    end
end

We use a variable called inside_haste_area to keep track of whether the player is inside the area or not. This variable is set to true inside enterHasteArea and set to false inside exitHasteArea, meaning that those functions will only be called once when those events happen from the HasteArea object. In the Player class, both functions simply will apply the modifications necessary:

function Player:enterHasteArea()
    self.inside_haste_area = true
    self.pre_haste_aspd_multiplier = self.aspd_multiplier
    self.aspd_multiplier = self.aspd_multiplier/2
end

function Player:exitHasteArea()
    self.inside_haste_area = false
    self.aspd_multiplier = self.pre_haste_aspd_multiplier
    self.pre_haste_aspd_multiplier = nil
end

And so in this way whenever the player enters the area his attack speed will be doubled, and whenever he exits the area it will go back to normal. One big point that's easy to miss here is that it's tempting to put all this logic inside the HasteArea object instead of linking it back to the player via the inside_haste_area variable. The reason why we can't do this is because if we do, then problems will occur whenever the player enters/leaves multiple areas. As it is right now, the fact that the inside_haste_area variable exists means that we will only apply the buff once, even if the player is standing on top of 3 overlapping HasteArea objects.


131. (CONTENT) Implement the spawn_haste_area_on_hp_pickup_chance passive. An InfoText object should be created with the text 'Haste Area!'. Additionally, an onHPPickup function should be added to the Player class.

132. (CONTENT) Implement the spawn_haste_area_on_sp_pickup_chancepassive. An InfoText object should be created with the text 'Haste Area!'.


Chance to Spawn SP on Cycle

The next one we'll go for is spawn_sp_on_cycle_chance. For this one we kinda already know how to do it in its entirety. The "onCycle" part behaves quite similarly to "onResourcePickup", the only difference is that we'll call the onCycle function whenever a new cycle occurs instead of whenever a resource is picked. And the "spawn SP" part is simply creating a new SP resource, which we also already know how to do.

So for the first part, we need to go into the cycle function and call onCycle:

function Player:cycle()
    ...
    self:onCycle()
end

Then we add the spawn_sp_on_cycle_chance variable to the Player:

function Player:new(...)
    ...
  	
    -- Chances
    self.spawn_sp_on_cycle_chance = 0
end

And with that we also automatically add a new chanceList representing the chances of this variable. And because of that we can add the functionality needed to the onCycle function:

function Player:onCycle()
    if self.chances.spawn_sp_on_cycle_chance:next() then
        self.area:addGameObject('SkillPoint')
        self.area:addGameObject('InfoText', self.x, self.y, 
      	{text = 'SP Spawn!', color = skill_point_color})
    end
end

And this should work out as expected:


Chance to Barrage on Kill

The next one is barrage_on_kill_chance. The only thing we don't really know how to do here is the "Barrage" part. Triggering events on kill is similar to the previous one, except instead of whenever a cycle happens, we'll call the player's onKill function whenever an enemy dies.

So first we add the barrage_on_kill_chance variable to the Player:

function Player:new(...)
    ...
  	
    -- Chances
    self.barrage_on_kill_chance = 0
end

Then we create the onKill function and call it whenever an enemy dies. There are two approaches to calling onKill whenever an enemy dies. The first is to just call it from every enemy's die or hit function. The problem with this is that as we add new enemies we'll need to add this same code calling onKill to all of them. The other option is to call onKill whenever a Projectile object collides with an enemy. The problem with this is that some projectiles can collide with enemies but not kill them (because the enemies have more HP or the projectile deals less damage), and so we need to figure out a way to tell if the enemy is actually dead or not. It turns out that figuring that out is pretty easy, so that's what I'm gonna go with:

function Projectile:update(dt)
    ...
  	
    if self.collider:enter('Enemy') then
        ...

        if object then
            object:hit(self.damage)
            self:die()
            if object.hp <= 0 then current_room.player:onKill() end
        end
    end
end

So all we have to do is after we call the enemy's hit function is to simply check if the enemy's HP is 0 or not. If it is it means he's dead and so we can call onKill.

Now for the barrage itself. The way we'll code is that by default, 8 projectiles will be shot within 0.05 seconds of each other, with an angle of between -math.pi/8 and +math.pi/8 of the angle the player is pointing towards. The barrage projectiles will also have the attack that the player has. So if the player has homing projectiles, then all barrage projectiles will also be homing. All that translates to this:

function Player:onKill()
    if self.chances.barrage_on_kill_chance:next() then
        for i = 1, 8 do
            self.timer:after((i-1)*0.05, function()
                local random_angle = random(-math.pi/8, math.pi/8)
                local d = 2.2*self.w
                self.area:addGameObject('Projectile', 
            	self.x + d*math.cos(self.r + random_angle), 
            	self.y + d*math.sin(self.r + random_angle), 
            	{r = self.r + random_angle, attack = self.attack})
            end)
        end
        self.area:addGameObject('InfoText', self.x, self.y, {text = 'Barrage!!!'})
    end
end

Most of this should be pretty straightforward. The only notable thing is that we use after inside a for loop to separate the creation of projectiles by 0.05 seconds between each other. Other than that we simply create the projectile with the given constraints. All that should look like this:


For the next exercises (and every one that comes after them), don't forget to create InfoText objects with the appropriate colors so that the player can tell when something happened.

133. (CONTENT) Implement the spawn_hp_on_cycle_chance passive.

134. (CONTENT) Implement the regain_hp_on_cycle_chance passive. The amount of HP regained is 25.

135. (CONTENT) Implement the regain_full_ammo_on_cycle_chance passive.

136. (CONTENT) Implement the change_attack_on_cycle_chance passive. The new attack is chosen at random.

137. (CONTENT) Implement the spawn_haste_area_on_cycle_chance passive.

138. (CONTENT) Implement the barrage_on_cycle_chance passive.

139. (CONTENT) Implement the launch_homing_projectile_on_cycle_chance passive.

140. (CONTENT) Implement the regain_ammo_on_kill_chance passive. The amount of ammo regained is 20.

141. (CONTENT) Implement the launch_homing_projectile_on_kill_chance passive.

142. (CONTENT) Implement the regain_boost_on_kill_chance passive. The amount of boost regained is 40.

143. (CONTENT) Implement the spawn_boost_on_kill_chance passive.


Gain ASPD Boost on Kill

We already implemented an "ASPD Boost"-like passive before with the HasteArea object. Now we want to implement another where we have a chance to get an attack speed boost whenever we kill an enemy. However, if we try to implement this in the same way that we implement the previous ASPD boost we would soon encounter problems. To recap, this is how we implement the boost in HasteArea:

function HasteArea:update(dt)
    HasteArea.super.update(self, dt)

    local player = current_room.player
    if not player then return end
    local d = distance(self.x, self.y, player.x, player.y)
    if d < self.r and not player.inside_haste_area then player:enterHasteArea()
    elseif d >= self.r and player.inside_haste_area then player:exitHasteArea() end
end

And then enterHasteArea and exitHasteArea look like this:

function Player:enterHasteArea()
    self.inside_haste_area = true
    self.pre_haste_aspd_multiplier = self.aspd_multiplier
    self.aspd_multiplier = self.aspd_multiplier/2
end

function Player:exitHasteArea()
    self.inside_haste_area = false
    self.aspd_multiplier = self.pre_haste_aspd_multiplier
    self.pre_haste_aspd_multiplier = nil
end

If we tried to implement the aspd_boost_on_kill_chance passive in a similar way it would look something like this:

function Player:onKill()
    ...
    if self.chances.aspd_boost_on_kill_chance:next() then
        self.pre_boost_aspd_multiplier = self.aspd_multiplier
    	self.aspd_multiplier = self.aspd_multiplier/2
    	self.timer:after(4, function()
      	    self.aspd_multiplier = self.pre_boost_aspd_multiplier
            self.pre_boost_aspd_multiplier = nil
      	end)
    end
end

Here we simply do what we did for the HasteArea boost. We store the current attack speed multiplier, halve it, and then after a set duration (in this case 4 seconds), we restore it back to its original value. The problem with doing things this way happens whenever we want to stack these boosts together.

Consider the situation where the player has entered a HasteArea and then gets an ASPD boost on kill. The problem here is that if the player exits the HasteArea before the 4 seconds for the boost duration are over then his aspd_multiplier variable will be restored to pre-ASPD boost levels, meaning that leaving the area will erase all other existing attack speed boosts.

And then also consider the situation where the player has an ASPD boost active and then enters a HasteArea. Whenever the boost duration ends the HasteArea effect will also be erased, since the pre_boost_aspd_multiplier will restore aspd_multiplier to a value that doesn't take into account the attack speed boost from the HasteArea. But even more worryingly, whenever the player exits the HasteArea he will now have permanently increased attack speed, since the save attack speed when he entered it was the one that was boosted from the ASPD boost.

So the main way we can fix this is by introducing a few variables:

function Player:new(...)
    ...
  	
    self.base_aspd_multiplier = 1
    self.aspd_multiplier = 1
    self.additional_aspd_multiplier = {}
end

Instead of only having the aspd_multiplier variable, now we'll have base_aspd_multiplier as well as additional_aspd_multiplier. aspd_multiplier will hold the current multiplier affected by all boosts. base_aspd_multiplier will hold the initial multiplier affected only by percentage increases. So if we have 50% increased attack speed from the tree, it will be applied on the constructor (in setStats) to base_aspd_multiplier. Then additional_aspd_multiplier will contain the added values of all boosts. So if we're inside a HasteArea, we would add the appropriate value to this table and then multiply its sum by the base every frame. So our update function for instance would look like this:

function Player:update(dt)
    ...
  	
    self.additional_aspd_multiplier = {}
    if self.inside_haste_area then table.insert(self.additional_aspd_multiplier, -0.5) end
    if self.aspd_boosting then table.insert(self.additional_aspd_multiplier, -0.5) end
    local aspd_sum = 0
    for _, aspd in ipairs(self.additional_aspd_multiplier) do
        aspd_sum = aspd_sum + aspd
    end
    self.aspd_multiplier = self.base_aspd_multiplier/(1 - aspd_sum)
end

In this way, every frame we'd be recalculating the aspd_multiplier variable based on the base as well as the boosts. There are a few multipliers that will make use of functionality very similar to this, so I'll just create a general object for this, since repeating it every time and with different variable names would be tiresome.

The Stat object looks like this:

Stat = Object:extend()

function Stat:new(base)
    self.base = base

    self.additive = 0
    self.additives = {}
    self.value = self.base*(1 + self.additive)
end

function Stat:update(dt)
    for _, additive in ipairs(self.additives) do self.additive = self.additive + additive end

    if self.additive >= 0 then
        self.value = self.base*(1 + self.additive)
    else
        self.value = self.base/(1 - self.additive)
    end

    self.additive = 0
    self.additives = {}
end

function Stat:increase(percentage)
    table.insert(self.additives, percentage*0.01)
end

function Stat:decrease(percentage)
    table.insert(self.additives, -percentage*0.01)
end

And the way we'd use it for our attack speed problem is like this:

function Player:new(...)
    ...
  	
    self.aspd_multiplier = Stat(1)
end

function Player:update(dt)
    ...
  
    if self.inside_haste_area then self.aspd_multiplier:decrease(100) end
    if self.aspd_boosting then self.aspd_multiplier:decrease(100) end
    self.aspd_multiplier:update(dt)
  
    ...
end

We would be able to access the attack speed multiplier at any point after aspd_multiplier:update is called by saying aspd_multiplier.value, and it would return us the correct result based on the base as well as the all possible boosts applied. Because of this we need to change how the aspd_multiplier variable is used:

function Player:update(dt)
    ...
  
    -- Shoot
    self.shoot_timer = self.shoot_timer + dt
    if self.shoot_timer > self.shoot_cooldown*self.aspd_multiplier.value then
        self.shoot_timer = 0
        self:shoot()
    end
end

Here we just change self.shoot_cooldown*self.aspd_multiplier to self.shoot_cooldown*self.aspd_multiplier.value, since things wouldn't work out otherwise. Additionally, we can also change something else here. The way our aspd_multiplier variable works now is contrary to how every other variable in the game works. When we say that we get increased 10% HP, we know that hp_multiplier is 1.1, but when we say that we get increased 10% ASPD, aspd_multiplier is 0.9 instead. We can change this very and make aspd_multiplier behave the same way as other variables by dividing instead of multiplying it to shoot_cooldown:

if self.shoot_timer > self.shoot_cooldown/self.aspd_multiplier.value then

In this way, if we get a 100% increase in ASPD, its value will be 2 and we will be halving the cooldown between shots, which is what we want. Additionally we need to change the way we apply our boosts and instead of calling decrease on them we will call increase:

function Player:update(dt)
    ...
  
    if self.inside_haste_area then self.aspd_multiplier:increase(100) end
    if self.aspd_boosting then self.aspd_multiplier:increase(100) end
    self.aspd_multiplier:update(dt)
end

Another thing to keep in mind is that because aspd_multiplier is a Stat object and not just a number, whenever we implement the tree and import its values to the Player object we'll need to treat them differently. So the treeToPlayer function that I mentioned earlier will have to take this into account as well.

In any case, in this way we can easily implement "Gain ASPD Boost on Kill" correctly:

function Player:new(...)
    ...
  
    -- Chances
    self.gain_aspd_boost_on_kill_chance = 0
end
function Player:onKill()
    ...
  	
    if self.chances.gain_aspd_boost_on_kill_chance:next() then
        self.aspd_boosting = true
        self.timer:after(4, function() self.aspd_boosting = false end)
        self.area:addGameObject('InfoText', self.x, self.y, 
      	{text = 'ASPD Boost!', color = ammo_color})
    end
end

We can also delete the enterHasteArea and exitHasteArea functions, as well as changing how the HasteArea object works slightly:

function HasteArea:update(dt)
    HasteArea.super.update(self, dt)

    local player = current_room.player
    if not player then return end
    local d = distance(self.x, self.y, player.x, player.y)
    if d < self.r then player.inside_haste_area = true
    elseif d >= self.r then player.inside_haste_area = false end
end

Instead of any complicated logic like we had before, we simply set the Player's inside_haste_area attribute to true or false based on if the player is inside the area or not, and then because of the way we implemented the Stat object, the application of the attack speed boost that comes from a HasteArea will be done automatically.


144. (CONTENT) Implement the mvspd_boost_on_cycle_chance passive. A "MVSPD Boost" gives the player 50% increased movement speed for 4 seconds. Also implement the mvspd_multiplier variable and multiply it in the appropriate location.

145. (CONTENT) Implement the pspd_boost_on_cycle_chance passive. A "PSPD Boost" gives projectiles created by the player 100% increased movement speed for 4 seconds. Also implement the pspd_multiplier variable and multiply it in the appropriate location.

146. (CONTENT) Implement the pspd_inhibit_on_cycle_chance passive. A "PSPD Inhibit" gives projectiles created by the player 50% decreased movement speed for 4 seconds.


While Boosting

These next passives we'll implement are the last ones of the "On Event Chance" type. So far all the ones we've focused on are chances of something happening on some event (on kill, on cycle, on resource pickup, ...) and these ones won't be different, since they will be chances for something to happen while boosting.

The first one we'll do is launch_homing_projectile_while_boosting_chance. The way this will work is that there will be a normal chance for the homing projectile to be launched, and this chance will be rolled on an interval of 0.2 seconds whenever we're boosting. This means that if we boost for 1 second, we'll roll this chance 5 times.

A good way of doing this is by defining two new functions: onBoostStart and onBoostEnd and then doing whatever it is we want to do to active the passive when the boost start, and then deactivate it when it ends. To add those two functions we need to change the boost code a little:

function Player:update(dt)
    ...
  
    -- Boost
    ...
    if self.boost_timer > self.boost_cooldown then self.can_boost = true end
    ...
    if input:pressed('up') and self.boost > 1 and self.can_boost then self:onBoostStart() end
    if input:released('up') then self:onBoostEnd() end
    if input:down('up') and self.boost > 1 and self.can_boost then 
        ...
        if self.boost <= 1 then
            self.boosting = false
            self.can_boost = false
            self.boost_timer = 0
            self:onBoostEnd()
        end
    end
    if input:pressed('down') and self.boost > 1 and self.can_boost then self:onBoostStart() end
    if input:released('down') then self:onBoostEnd() end
    if input:down('down') and self.boost > 1 and self.can_boost then 
        ...
        if self.boost <= 1 then
            self.boosting = false
            self.can_boost = false
            self.boost_timer = 0
            self:onBoostEnd()
        end
    end
    ...
end

Here we add input:pressed and input:released, which return true only whenever those events happen, and with that we can be sure that onBoostStart and onBoostEnd will only be called once when those events happen. We also add onBoostEnd to inside the input:down conditional in case the player doesn't release the button but the amount of boost available to him ends and therefore the boost ends as well.

Now for the launch_homing_projectile_while_boosting_chance part:

function Player:new(...)
    ...
  
    -- Chances
    self.launch_homing_projectile_while_boosting_chance = 0
end

function Player:onBoostStart()
    self.timer:every('launch_homing_projectile_while_boosting_chance', 0.2, function()
        if self.chances.launch_homing_projectile_while_boosting_chance:next() then
            local d = 1.2*self.w
            self.area:addGameObject('Projectile', 
          	self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), 
                {r = self.r, attack = 'Homing'})
            self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'})
        end
    end)
end

function Player:onBoostEnd()
    self.timer:cancel('launch_homing_projectile_while_boosting_chance')
end

Here whenever a boost starts we call timer:every to roll a chance for the homing projectile every 0.2 seconds, and then whenever a boost ends we cancel that timer. Here's what that looks like if the chance of this event happening was 100%:


147. (CONTENT) Implement the cycle_speed_multiplier variable. This variable makes the cycle speed faster or slower based on its value. So, for instance, if cycle_speed_multiplier is 2 and our default cycle duration is 5 seconds, then applying it would turn our cycle duration to 2.5 instead.

148. (CONTENT) Implement the increased_cycle_speed_while_boosting passive. This variable should be a boolean that signals if the cycle speed should be increased or not whenever the player is boosting. The boost should be an increase of 200% to cycle speed multiplier.

149. (CONTENT) Implement the invulnerability_while_boosting passive. This variable should be a boolean that signals if the player should be invulnerable whenever he is boosting. Make use of the invincible attribute which already exists and serves the purpose of making the player invincible.


Increased Luck While Boosting

The final "While Boosting" type of passive we'll implement is "Increased Luck While Boosting". Before we can implement it though we need to implement the luck_multiplier stat. Luck is one of the main stats of the game and it works by increasing the chances of favorable events to happen. So, let's say you have 10% chance to launch a homing projectile on kill. If luck_multiplier is 2, then this chance becomes 20% instead.

The way to implement this turns out to be very very simple. All "chance" type passives go through the generateChances function, so we can just implement this there:

function Player:generateChances()
    self.chances = {}
    for k, v in pairs(self) do
        if k:find('_chance') and type(v) == 'number' then
      	    self.chances[k] = chanceList(
            {true, math.ceil(v*self.luck_multiplier)}, 
            {false, 100-math.ceil(v*self.luck_multiplier)})
        end
    end
end

And here we simply multiply v by our luck_multiplier and it should work as expected. With this we can go on to implement the increased_luck_while_boosting passive like this:

function Player:onBoostStart()
    ...
    if self.increased_luck_while_boosting then 
    	self.luck_boosting = true
    	self.luck_multiplier = self.luck_multiplier*2
    	self:generateChances()
    end
end

function Player:onBoostEnd()
    ...
    if self.increased_luck_while_boosting and self.luck_boosting then
    	self.luck_boosting = false
    	self.luck_multiplier = self.luck_multiplier/2
    	self:generateChances()
    end
end

Here we implement it like we initially did for the HasteArea object. The reason we can do this now is because there will not be any other passives that will give the Player a luck boost, which means that we don't have to worry about multiple boosts possibly overriding each other. If we had multiple passives giving boosts to luck, then we'd need to make it a Stat object like we did for the aspd_multiplier.

Also importantly, whenever we change our luck multiplier we also call generateChances again, otherwise our luck boost will not really affect anything. There's a downside to this which is that all lists get reset, and so if some list randomly selected a bunch of unlucky rolls and then it gets reset here, it could select a bunch of unlucky rolls again instead of following the chanceList property where it would be less likely to select more unlucky rolls as time goes on. But this is a very minor problem that I personally don't really worry about.


HP Spawn Chance Multiplier

Now we'll go over hp_spawn_chance_multiplier, which increases the chance that whenever the Director spawns a new resource, that resource will be an HP one. This is a fairly straightforward implementation if we remember how the Director works:

function Player:new(...)
    ...
  	
    -- Multipliers
    self.hp_spawn_chance_multiplier = 1
end
function Director:new(...)
    ...
  
    self.resource_spawn_chances = chanceList({'Boost', 28}, 
    {'HP', 14*current_room.player.hp_spawn_chance_multiplier}, {'SkillPoint', 58})
end

On article 9 we went over the creation of the chances for each resource to be spawned. The resource_spawn_chances chanceList holds those chances, and so all we have to do is make sure that we use hp_spawn_chance_multiplier to increase the chances that the HP resource will be spawned according to the multiplier.

It's also important here to initialize the Director after the Player in the Stage room, since the Director depends on variables the Player has while the Player doesn't depend on the Director at all.


150. (CONTENT) Implement the spawn_sp_chance_multiplier passive.

151. (CONTENT) Implement the spawn_boost_chance_multiplier passive.


Given everything we've implemented so far, these next exercises can be seen as challenges. I haven't gone over most aspects of their implementation, but they're pretty simple compared to everything we've done so far so they should be straightforward.


152. (CONTENT) Implement the drop_double_ammo_chance passive. Whenever an enemy dies there will be a chance that it will create two Ammo objects instead of one.

153. (CONTENT) Implement the attack_twice_chance passive. Whenever the player attacks there will be a chance to call the shoot function twice.

154. (CONTENT) Implement the spawn_double_hp_chance passive. Whenever an HP resource is spawned by the Director there will be a chance that it will create two HP objects instead of one.

155. (CONTENT) Implement the spawn_double_sp_chance passive. Whenever a SkillPoint resource is spawned by the Director there will be a chance that it will create two SkillPoint objects instead of one.

156. (CONTENT) Implement the gain_double_sp_chance passive. Whenever the player collects a SkillPoint resource there will be a chance that he will gain two skill points instead of one.


Enemy Spawn Rate

The enemy_spawn_rate_multiplier will control how fast the Director changes difficulties. By default this happens every 22 seconds, but if enemy_spawn_rate_multiplier is 2 then this will happen every 11 seconds instead. This is another rather straightforward implementation:

function Player:new(...)
    ...
  	
    -- Multipliers
    self.enemy_spawn_rate_multiplier = 1
end
function Director:update(dt)
    ...
  	
    -- Difficulty
    self.round_timer = self.round_timer + dt
    if self.round_timer > self.round_duration/self.stage.player.enemy_spawn_rate_multiplier then
        ...
    end
end

So here we just divide round_duration by enemy_spawn_rate_multiplier to get the target round duration.


157. (CONTENT) Implement the resource_spawn_rate_multiplier passive.

158. (CONTENT) Implement the attack_spawn_rate_multiplier passive.


And here are some more exercises for some more passives. These are mostly multipliers that couldn't fit into any of the classes of passives talked about before but should be easy to implement.

159. (CONTENT) Implement the turn_rate_multiplier passive. This is a passive that increases or decreases the speed with which the Player's ship turns.

160. (CONTENT) Implement the boost_effectiveness_multiplier passive. This is a passive that increases or decreases the effectiveness of boosts. This means that if this variable has the value of 2, a boost will go twice as fast or twice as slow as before.

161. (CONTENT) Implement the projectile_size_multiplier passive. This is a passive that increases or decreases the size of projectiles.

162. (CONTENT) Implement the boost_recharge_rate_multiplier passive. This is a passive that increases or decreases how fast boost is recharged.

163. (CONTENT) Implement the invulnerability_time_multiplier passive. This is a passive that increases or decreases the duration of the player's invulnerability after he's hit.

164. (CONTENT) Implement the ammo_consumption_multiplier passive. This is a passive that increases or decreases the amount of ammo consumed by all attacks.

165. (CONTENT) Implement the size_multiplier passive. This is a passive that increases or decreases the size of the player's ship. Note that that the positions of the trails for all ships, as well as the position of projectiles as they're fired need to be changed accordingly.

166. (CONTENT) Implement the stat_boost_duration_multiplier passive. This is a passive that increases of decreases the duration of temporary buffs given to the player.


Projectile Passives

Now we'll focus on a few projectile passives. These passives will change how our projectiles behave in some fundamental way. These same ideas can also be implemented in the EnemyProjectile object and then we can create enemies that use some of this as well. For instance, there's a passive that makes your projectiles orbit around you instead of just going straight. Later on we'll add an enemy that has tons of projectiles orbiting it as well and the technology behind it is the same for both situations.

90 Degree Change

We'll call this passive projectile_ninety_degree_change and what it will do is that the angle of the projectile will be changed by 90 degrees periodically. The way this looks is like this:

Notice that the projectile roughly moves in the same direction it was moving towards as it was shot, but its angle changes rapidly by 90 degrees each time. This means that the angle change isn't entirely randomly decided and we have to put some thought into it.

The basic way we can go about this is to say that projectile_ninety_degree_change will be a boolean and that the effect will apply whenever it is true. Because we're going to apply this effect in the Projectile class, we have two options in regards to how we'll read from it that the Player's projectile_ninety_degree_change variable is true or not: either pass that in in the opts table whenever we create a new projectile from the shoot function, or read that directly from the player by accessing it through current_room.player. I'll go with the second solution because it's easier and there are no real drawbacks to it, other than having to change current_room.player to something else whenever we move some of this code to EnemyProjectile. The way all this would look is something like this:

function Player:new(...)
    ...
  	
    -- Booleans
    self.projectile_ninety_degree_change = false
end
function Projectile:new(...)
    ...

    if current_room.player.projectile_ninety_degree_change then

    end
end

Now what we have to do inside the conditional in the Projectile constructor is to change the projectile's angle each time by 90 degrees, but also respecting its original direction. What we can do is first change the angle by either 90 degrees or -90 degrees randomly. This would look like this:

function Projectile:new(...)
    ...

    if current_room.player.projectile_ninety_degree_change then
        self.timer:after(0.2, function()
      	    self.ninety_degree_direction = table.random({-1, 1})
            self.r = self.r + self.ninety_degree_direction*math.pi/2
      	end)
    end
end

Now what we need to do is figure out how to turn the projectile in the other direction, and then turn it back in the other, and then again, and so on. It turns out that since this is a periodic thing that will happen forever, we can use timer:every:

function Projectile:new(...)
    ...
  	
    if current_room.player.projectile_ninety_degree_change then
        self.timer:after(0.2, function()
      	    self.ninety_degree_direction = table.random({-1, 1})
            self.r = self.r + self.ninety_degree_direction*math.pi/2
            self.timer:every('ninety_degree_first', 0.25, function()
                self.r = self.r - self.ninety_degree_direction*math.pi/2
                self.timer:after('ninety_degree_second', 0.1, function()
                    self.r = self.r - self.ninety_degree_direction*math.pi/2
                    self.ninety_degree_direction = -1*self.ninety_degree_direction
                end)
            end)
      	end)
    end
end

At first we turn the projectile in the opposite direction that we turned it initially, which means that now it's facing its original angle. Then, after only 0.1 seconds, we turn it again in that same direction so that it's facing the opposite direction to when it first turned. So, if it was fired facing right, what happened is: after 0.2 seconds it turned up, after 0.25 it turned right again, after 0.1 seconds it turned down, and then after 0.25 seconds it will repeat by turning right then up, then right then down, and so on.

Importantly, at the end of each every loop we change the direction it should turn towards, otherwise it wouldn't oscillate between up/down and would keep going up/down instead of straight. Doing all that looks like this:


167. (CONTENT) Implement the projectile_random_degree_change passive, which changes the angle of the projectile randomly instead. Unlike the 90 degrees one, projectiles in this one don't need to retain their original direction.

168. (CONTENT) Implement the angle_change_frequency_multiplier passive. This is a passive that increases or decreases the speed with which angles change in the previous 2 passives. If angle_change_frequency_multiplier is 2, for instance, then instead of angles changing with 0.25 and 0.1 seconds, they will change with 0.125 and 0.05 seconds instead.


Wavy Projectiles

Instead of abruptly changing the angle of our projectile, we can do it softly using the timer:tween function, and in this way we can get a wavy projectile effect that looks like this:

The idea is almost the same as the previous examples but using timer:tween instead:

function Projectile:new(...)
    ...
  	
    if current_room.player.wavy_projectiles then
        local direction = table.random({-1, 1})
        self.timer:tween(0.25, self, {r = self.r + direction*math.pi/8}, 'linear', function()
            self.timer:tween(0.25, self, {r = self.r - direction*math.pi/4}, 'linear')
        end)
        self.timer:every(0.75, function()
            self.timer:tween(0.25, self, {r = self.r + direction*math.pi/4}, 'linear',  function()
                self.timer:tween(0.5, self, {r = self.r - direction*math.pi/4}, 'linear')
            end)
        end)
    end
end

Because of the way timer:every works, in that it doesn't start performing its functions until after the initial duration, we first do one iteration of the loop manually, and then after that the every loop takes over. In the first iteration we also use an initial value of math.pi/8 instead of math.pi/4 because we only want the projectile to tween half of what it usually does, since it starts in the middle position (as it was just shot from the Player) instead of on either edge of the oscillation.


169. (CONTENT) Implement the projectile_waviness_multiplier passive. This is a passive that increases or decreases the target angle that the projectile should reach when tweening. If projectile_waviness_multiplier is 2, for instance, then the arc of its path will be twice as big as normal.


Acceleration and Deceleration

Now we'll go for a few passives that change the speed of the projectile. The first one is "Fast -> Slow" and the second is "Slow -> Fast", meaning, the projectile starts with either fast or slow velocity, and then transitions into either slow or fast velocity. This is what "Fast -> Slow" looks like:

The way we'll implement this is pretty straightforward. For the "Fast -> Slow" one we'll tween the velocity to double its initial value quickly, and then after a while tween it down to half its initial value. And for the other we'll simply do the opposite.

function Projectile:new(...)
    ...
  	
    if current_room.player.fast_slow then
        local initial_v = self.v
        self.timer:tween('fast_slow_first', 0.2, self, {v = 2*initial_v}, 'in-out-cubic', function()
            self.timer:tween('fast_slow_second', 0.3, self, {v = initial_v/2}, 'linear')
        end)
    end

    if current_room.player.slow_fast then
        local initial_v = self.v
        self.timer:tween('slow_fast_first', 0.2, self, {v = initial_v/2}, 'in-out-cubic', function()
            self.timer:tween('slow_fast_second', 0.3, self, {v = 2*initial_v}, 'linear')
        end)
    end
end

170. (CONTENT) Implement the projectile_acceleration_multiplier passive. This is a passive that controls how fast or how slow a projectile accelerates whenever it changes to a higher velocity than its original value.

171. (CONTENT) Implement the projectile_deceleration_multiplier passive. This is a passive that controls how fast or how slow a projectile decelerates whenever it changes to a lower velocity than its original value.


Shield Projectiles

This one is a bit more involved than the others because it has more moving parts to it, but this is what the end result should look like. As you can see, the projectiles orbit around the player and also sort of inherit its movement direction. The way we can achieve this is by using a circle's parametric equation. In general, if we want A to orbit around B with some radius R then we can do something like this:

Ax = Bx + R*math.cos(time)
Ay = By + R*math.sin(time)

Where time is a variable that goes up as times passes. Before we get to implementing this let's set everything else up. shield_projectile_chance will be a chance-type variable instead of a boolean, meaning that every time a new projectile will be created there will be a chance it will orbit the player.

function Player:new(...)
    ...
  	
    -- Chances
    self.shield_projectile_chance = 0
end

function Player:shoot()
    ...
    local shield = self.chances.shield_projectile_chance:next()
  	
    if self.attack == 'Neutral' then
        self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), 
        self.y + 1.5*d*math.sin(self.r), {r = self.r, attack = self.attack, shield = shield})
	...
end

Here we define the shield variable with the roll of if this projectile should be orbitting the player or not, and then we pass that in the opts table of the addGameObject call. Here we have to repeat this step for every attack type we have. Since we'll have to make future changes like this one, we can just do something like this instead now:

function Player:shoot()
    ...
  	
    local mods = {
        shield = self.chances.shield_projectile_chance:next()
    }

    if self.attack == 'Neutral' then
        self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), 
        self.y + 1.5*d*math.sin(self.r), table.merge({r = self.r, attack = self.attack}, mods))
        ...
end

And so in this way, in the future we'll only have to add things to the mods table. The table.merge function hasn't been defined yet, but you can guess what it does based on how we're using it here.

function table.merge(t1, t2)
    local new_table = {}
    for k, v in pairs(t2) do new_table[k] = v end
    for k, v in pairs(t1) do new_table[k] = v end
    return new_table
end

It simply joins two tables together with all their values into a new one and then returns it.

Now we can start with the actual implementation of the shield functionality. At first we want to define a few variables, like the radius, the orbit speed and so on. For now I'll define them like this:

function Projectile:new(...)
    ...
  	
    if self.shield then
        self.orbit_distance = random(32, 64)
        self.orbit_speed = random(-6, 6)
        self.orbit_offset = random(0, 2*math.pi)
    end
end

orbit_distance represents the radius around the player. orbit_speed will be multiplied by time, which means that higher absolute values will make it go faster, while lower ones will make it go slower. Negative values will make the projectile turn in the other direction which adds some randomness to it. orbit_offset is the initial angle offset that each projectile will have. This also adds some randomness to it and prevents all projectiles from being started at roughly the same position. And now that we have all these defined we can apply the circle's parametric equation to the projectile's position:

function Projectile:update(dt)
    ...
  
    -- Shield
    if self.shield then
        local player = current_room.player
        self.collider:setPosition(
      	player.x + self.orbit_distance*math.cos(self.orbit_speed*time + self.orbit_offset),
      	player.y + self.orbit_distance*math.sin(self.orbit_speed*time + self.orbit_offset))
    end
  	
    ...
end

It's important to place this after any other calls we may make to setLinearVelocity otherwise things won't work out. We also shouldn't forget to add the global time variable and increase it by dt every frame. If we do that correctly then it should look like this:

And this gets the job done but it looks wrong. The main thing wrong with it is that the projectile's angles are not taking into account the rotation around the player. One way to fix this is to store the projectile's position last frame and then get the angle of the vector that makes up the subtraction of the current position by the previous position. Code is worth a thousand words so that looks like this:

function Projectile:new(...)
    ...
  	
    self.previous_x, self.previous_y = self.collider:getPosition()
end

function Projectile:update(dt)
    ...
  	
    -- Shield
    if self.shield then
        ...
        local x, y = self.collider:getPosition()
        local dx, dy = x - self.previous_x, y - self.previous_y
        self.r = Vector(dx, dy):angle()
    end

    ...
  
    -- At the very end of the update function
    self.previous_x, self.previous_y = self.collider:getPosition()
end

And in this way we're setting the r variable to contain the angle of the projectile while taking into account its rotation. Because we're using setLinearVelocity and using that angle, it means that when we draw the projectile in Projectile:draw and use Vector(self.collider:getLinearVelocity()):angle()) to get our direction, everything will be set according what we set the r variable to. And so all that looks like this:

And this looks about right. One small problem that you can see in the gif above is that as projectiles are fired, if they turn into shield projectiles they don't do it instantly. There's a 1-2 frame delay where they look like normal projectiles and then they disappear and appear orbiting the player. One way to fix this is to just hide all shield projectiles for 1-2 frames and then unhide them:

function Projectile:new(...)
    ...
  	
    if self.shield then
        ...
    	self.invisible = true
    	self.timer:after(0.05, function() self.invisible = false end)
    end
end

function Projectile:draw()
    if self.invisible then return end
    ...
end

And finally, it would be pretty OP if shield projectiles could just stay there forever until they hit an enemy, so we need to add a projectile duration such that after that duration ends the projectile will be killed:

function Projectile:new(...)
    ...
  	
    if self.shield then
    	...
    	self.timer:after(6, function() self:die() end)
    end
end

And in this way after 6 seconds of existence our shield projectiles will die.


END

I'm going to end it here because the editor I'm using to write this is starting to choke on the size of this article. In the next article we'll continue with the implementation of more passives, as well as adding all player attacks, enemies, and passives related to them. The next article also marks the end of implementation of all content in the game, and the ones coming after that will focus on how to present that content to the player (SkillTree and Console rooms).



One Year Sales Data for BYTEPATH

It's been 1 year since I released BYTEPATH and since Steam now allows it I've decided to share its sales data so that other indiedevs have more data points to work with in their own efforts. I've already written a more in depth article on the game's release so this one will be very short and straight to the point.


Data

This is what the graph for sales looks like:

Each subsequent small bump in the graph after release either corresponds to a sale (all sales were for 50% off) or a random event, like the game getting mentioned in the comments of this video:

The total number of units sold was 5,582 and the gross revenue was $8,833. This is what the breakdown by country and OS looks like (the game was released on Windows and Linux):


This is what wishlist activity looks like:

The number of current outstanding wishes is 4,565 and the lifetime conversation rate was 28.1%. I did nearly no wishlist building before the game was released so most of this activity came after release, and the graph of wishlist activity follows the sales graph pretty closely.


This is what play time looks like:


This is what views, click through rate and store page visits look like:

The leftmost number corresponds to the number of views the game's capsule had on the store. The rightmost number corresponds to the number of views on the game's store page. The middle number corresponds to the percentage of capsule views that turn into store page visits.

Out of all store page views, 80% were generated by Steam and 20% came from external sites. Those 20% can probably mostly be attributed to my own marketing efforts but I didn't add proper analytics to my game's page so I can't know for sure. Either way, my own marketing efforts were mostly writing the tutorial for the game, which was well received and generated as much attention on the game as a tutorial could, and posts that I made when the game was released on reddit, like this one.

Out of the 80% that Steam generated, 50% was generated via other product pages, around 7% through tag pages and around 4% through each home page and search results.

Accessibility and Challenge

Lately there's been a lot of argument over accessibility and challenge in games because of Sekiro's release. Namely, some people think the game should have an easy mode, others don't. So I thought I'd write down my thoughts on this.


Personality

One of the things I think about when thinking through these issues is that often times what makes people enjoy certain types of games comes from their biological needs via their personalities. And these can be roughly separated with the Big Five and mapped into different game types like this (source):

I don't think this mapping of the big five traits into gaming is final or 100% correct, but it's a good starting point to work from from a group of people who have done the work to get here. You can watch the explanation for how they did it here:

For instance, I'm someone who doesn't really like challenging games. If a game has other aspects I enjoy and it just happens to be challenging I'll likely play it, but if it's challenging for the sake of challenge I likely won't play it. Because of this I don't really play any of the souls games and didn't play Sekiro. I really really like what the "power" aspect though, which deals generally with getting more and more powerful over time. So games like Risk of Rain 2 are very good for me. You get to try to lots and lots of different builds because there are so many items and over time your character gets more powerful.

The point is, these personality differences between people are real, they're biological, and people will be driven to play different types of games because of it. Which brings me to...


Challenge

As you can see, challenge is one of the aspects identified by the Quantic Foundry people. Often what happens with AAA games is that developers will try to appeal to the biggest number of personality types as possible because they want more people buying their games. I think everyone understands this intuitively even without knowing about personality traits. But sometimes comes along a company like From Software and they decide that they'll focus on a personality trait that's largely ignored as it means a portion of the population isn't being addressed properly. And so they make the souls franchise which is largely focused on the challenge aspect.

One of the reasons the challenge aspect gets ignored by most big studios is because unlike other aspects, if you really want to cater to the types of people who enjoy challenge you have to exclude everyone who doesn't. This is by definition how the trait works and it's also how it works in real life. Many fields are zero sum, meaning if you win someone else has to lose, and people who like challenge are often driven to those fields. Politics is a good example of a zero sum field, given that there's a fixed number of positions available.

In the case of single player games the competition with other people is indirect rather than direct: you're playing a single player game (so you're not competing directly), but beating the game at all is challenging and anyone who manages it has gone through the same amount of pain and challenge. This is what makes the game interesting and what makes those types of people play it. Adding an easy mode completely devalues the experience for the people who are in it for the challenge (and those people are the main audience of the game), because suddenly it means that beating the game doesn't mean much anymore.

Note that there have been big arguments before in the gaming community when it comes to other traits. I think the Gone Home debate was largely an argument of people who like the story and artistic aspects of games with people who largely don't. And this brings me to...


Journalists/indie devs vs gamers

This is something that happens constantly and it's pretty interesting to watch, but journalists and certain indie devs seem to be always be "against" gamers and vice-versa. This challenge debate is another example of that, the Gone Home debate was also another example. In my view the reason this happens is because both of those groups are just fundamentally different personality wise. To be an indie developer or a journalist you have to be pretty high in the trait called openness to experience, which deals with creativity, interest for ideas and the artistic aspect of things. Whereas to be a gamer you just have to be a human being in the general population, and the general population won't be as high in openness as a group that is professionally selected for that trait.

So what happens is that, being high in openness, journalists/indie devs will enjoy certain aspects of games more than others. They'll enjoy the artistic appeal of games, they'll enjoy the story, they'll enjoy fantasy elements, but they generally won't enjoy things like grinding or challenge, since to enjoy those you have to be higher in conscientiousness, and as a group journalists or indie devs aren't particularly selected for that (and I'd guess that because they're selected for openness often times what happens is that people high in openness will be lower in conscientiousness and vice-versa).

And so this creates a very natural conflict between the interests journalists and some indie devs and the interests of a big portion of the gaming population. And these interest differences will manifest themselves in things like asking for an easy mode to a challenging game. And one of the reasons why these debates tend to get so heated is because they're not just debates about games, they're about people's identity. If you're someone who enjoys challenge a lot that will shape the way you view the world and the way you act in it, it's not just a preference, it literally informs everything around you and how you parse reality. And so when other people attack that you will feel attacked as well. The same argument applies to people who really enjoy the story/artistic element of games. If you're someone like that and you see games that are about that getting attacked you'll also feel extremely defensive. It's a natural reaction and it's a part of being human.


Possible solutions

The main solution to these problems is to simply understand that people have different preferences and that no one is right or wrong. Understand your own preferences and how they differ from other people's and frame your disagreements in this manner. I don't like games like Sekiro because I don't like challenge that much, but I understand how other people might enjoy it and how adding an easy mode would ruin the experience for them, so I won't ask for something like that. Similarly, I don't like games like Gone Home or The Witness, but I can understand why people would enjoy them, so I won't really ask for them to have better gameplay that I would enjoy more like powerups or something. It doesn't make any sense.

Finally, the main argument MANY people who support an easy mode make is, "what difference does it make if there's an easy mode?" Two examples of this (there are many many more):

And as I just explained it, the challenge aspect of the game would be ruined for the people who enjoy that aspect and who are largely ignored by most AAA developers. The reason why so many people don't seem to understand this is because they're not the target audience for FromSoft's games and so they don't feel that gut feeling of WRONG whenever someone mentions that the game should be made easier. Often times people also can't explain this gut feeling, it just feels wrong and they know they're against it with no real way to rationally explain it.

So I'd advise for anyone who gets into these arguments and who wants more accessibility in games to be more empathetic. FromSoft is catering to a very small group of people who really enjoys challenge and those people aren't catered to in a real way by anyone else (how many well made games from AAA studios are there with no easy mode?). By coming in and trying to get their experience ruined you're both attacking a minority of people and you're trying to force your own personality preferences on them. This isn't the correct way to get things done and I think that more empathy will help.

People can have different preferences and games can cater to specific types of people at the exclusion of others. Not every game should cater to everyone because some personality traits by definition require exclusion, and challenge happens to be one of them.

Hidden Gems Don't Exist

Today I got mentioned on this tweet which is talking about my game:

I've been meaning to write about this subject for a while now and this tweet is a good excuse. So in this article I'll expand on why I think that lists like the above are fundamentally flawed and misguided, why a game's quality is not separated from its popularity (if you have a "good" game like mine that is not popular it means the game isn't really that good), why the concept of "hidden gems" is wrong, and why the idea that these hidden gems exist because of poor discoverability on Steam's end is also false.


Good game

The first thing to think about when it comes to this subject is the definition of a hidden gem. In this article, the author says things like:

Every developer I know has a list of obscure indie games they know are great, but just can't get the exposure they deserve.

My twitter friends are constantly trying to personally give visibility for such "hidden gems" and I think that's admirable, and we should keep doing stuff like that.

And I think this is a pretty well agreed upon definition. It's basically a game that is "good" but that isn't popular. So knowing this, we might then move on to the definition of a good game.

This one is a bit more murky but the things I would look at in a game would first be its visual quality. For instance, a game like Owlboy has pretty high visual quality and most people who look at it would be immediately wowed by it, even those who generally dislike pixel art would say things like "damn, this is what pixel art should look like, not that lazy shit other indies are doing".

Another aspect of a game that matters a lot is how it feels to play. Some games might not look super good but they feel amazing to play. The best example of this that I can think of is One Finger Death Punch. OFDP is a game that definitely looks very simple, but the moment you start playing it and pressing buttons it just feels very very good to play. This is the kind of thing that you can convey with words alone, so I definitely recommend buying it and trying it out for yourself.

And then there are games that last a long time. These are games that people can put hundreds or thousands of hours in without getting bored. And finally, there are just games that do those 3 things really well, the best example I can think of right now being Dead Cells. Dead Cells looks super good and the moment you start playing it you immediately notice that pressing a button feels amazing, simply because the animations are super well done and responsive. And then on top of that it's a game that has a lot of replayability and lasts a long time, so it does everything that I care about right.

And these are the things I would look at to define a "good game". But there's one problem with my definition, which is the problem that I don't care about stories in video games. And my 3 main criteria simply don't touch upon story at all. Clearly this is wrong if we're looking at it from a global perspective, because there are tons of games that are story-focused that lots of people love.

This touches upon the bigger issue of defining a good game, which is that it's inherently a subjective pursuit. No one individual or group of individuals can cover the entire spectrum of taste in video games and come up to reasonable conclusions on the quality of all the factors they should care about. It's a fundamentally unsolvable problem if you were to try to solve it in a serious manner.

This is not to say that there's no objectivity when it comes to game quality, quite the contrary. Games that look like Owlboy or Cuphead, for instance, will awe a high percentage of people who look at them. In this sense, those games are objectively good looking. However, when we're talking about hidden gems we're not talking about games that are this obviously good, we're generally talking about games that have very obvious flaws but that despite all those flaws do some things really really well that we think they should have a bigger audience. The problem with this kind of thinking though is that...


The market doesn't owe you anything

The notion that a game can't get the "exposure it deserves" to me is somewhat arrogant. It is essentially a statement that your tastes trump the tastes of other millions of people and their collective decisions to not buy the game that you think deserves more players. Why is your taste more important than the taste of all those millions of people? The answer is that it isn't.

The only possible way to argue that a game deserves more exposure than it got is to assume that the mechanisms through which games are exposed are flawed. And this leads us to an indie developer's favorite scapgegoat: Steam. The argument goes that since Steam "opened the floodgates", tons of games flooded the store and now good games aren't getting the attention they deserve, and so articles and sites like the above get written/made in an attempt to fix the problem.

While it is possible that Steam's algorithms aren't doing the best job possible, people have to understand that Valve has a financial incentive to find good games and surface them to the general population, because if they don't do that they lose money. So it's very likely that Valve checks if their algorithms are working correctly obsessively. In general if you want to understand how Steam works I highly recommend watching the first ten or so minutes of this talk:

With this in mind, it's important to realize that most of the games that "don't get the attention they deserve", more likely than not simply aren't games that convert well among the general population of players, for one reason or another. For instance, tons of Visual Novels fall into the category of hidden gems, so much so that whoever made the site I mentioned at first in the article has a special hidden gem list only for Visual Novels. Which situation is more likely: there are a fixed number of people in the world who like visual novels, and that's why a lot of them are relatively hidden despite their good quality, or Steam's algorithms are wrong? In this situation it's very obviously the former, and I would think that this holds for most other genres as well.


Steam

Lots of indie devs hate on Valve, and I just don't get it. I think that we collectively got very lucky that Valve was the company that ended up "controlling" the PC market, because every action they take is a signal to me that they're interested in the long term health of PC gaming, and by consequence in the long term health of indie gaming (as long as indie developers collectively keep making good games that people want to play).

Valve seems to understand at a fundamental level that their tastes are not better than anyone else's and that the best way they can solve the problem of objectively assessing a game's quality is through the market itself. Through the actions of thousands of people buying or not buying games they are shown, Valve can reliably tell which games are "good" and which games aren't, and then they will show the good games to more people, as you would expect. This is a simple and effective way of running a store and you couldn't really ask for anything different. They've stated time and time again that this is the way they want to run their store, and not through a process of manual/human curation like so many people seem to want:


Knowing all this, I just reject the notion that hidden gems exist at all. Games that are really good but not popular are simply games that fulfill some niche that few people really like, but not something that the general population would want to play. My game, for instance, is such a case. Like the tweet said, it has 99% positive scores (only 1 negative review out of almost 100), but it's also a game that simply has a cap on the number of people interested in playing it. In my Steam page I am very very clear about what the game is and what it isn't:

It's a mix of Path of Exile and Bit Blaster XL, so in theory it has a cap based on the number of people who have played both games, which is certainly less than like 50k people, since they're both very very different games, despite both being popular. And then on top of that, it's a game that stylistically is different from both Bit Blaster XL and Path of Exile, going for a hacker/terminal type of thing. So the audience is capped even more by that. It's just not a game that is going to interest many people, but the people that are indeed interested by it will really like it, probably because I did a good job at what I set out to do.

But none of this means that my game deserves more attention, because it doesn't. It's both a game that has a limit on the number of people who would be interested in it, but it also just doesn't look or play well enough. And the market has responded to this accordingly in my opinion.


Good = popular

With everything I said in mind, it should be more clear as to why I would say at the start of this article that I think that a game's quality is not separated from its popularity. One of the things that follows from the definition of quality that Steam uses, for instance (which is to use the market as the neutral judge of quality), is that game's quality is directly correlated to its popularity. And this is an odd thing to say that doesn't seem right, but once you think about it enough it totally is.

We normally understand a product's quality and popularity as two separate concepts. We can analyze a game's quality by using any of the criteria I mentioned prior in the article, and we can have games that are extremely good at those criteria but that are unpopular. And we can also have games that are extremely bad at those criteria but that are popular. This incongruity can happen for many reasons, but the most obvious one might be marketing. For instance, if the developer of a "bad" game spends tons of money marketing it, the game will likely do better than a game that was objectively better but who's developer was clueless about marketing. And so in this way it's obvious that they are two separate concepts that have little to do with one another.

But if you want to take a neutral approach, then a game's quality is not separated from its marketing efforts, and the game that spent more money on marketing is simply better. This is an idea that indie developers in general don't take into account because it sounds unfair, but it should be obvious. If you design a game that is streamable, for instance, like say a horror game, then you'll have an easier time marketing it to streamers. This horror game, even though it might be worse than many other games on conventional criteria, is way better than all of those on the "streamable" criteria. This is a criteria that most indie developers don't pay attention to, so it's easy to look at that very popular horror game that's played by tons of streamers and say "wow, this is trash but it's so popular, these streamers are fucking stupid, wtf", but that would be an ignorant reaction. And it's ignorant both because the "streamable" criteria is not being taken into account, and because the main idea that dominates the mind of whoever said something like that is that quality and popularity are separated from one another, when they aren't.

I could talk more about this but this article is getting long, but the main notion is that in general, if a game is popular it's good, and if a game isn't popular it's bad. This is a definitional thing where we take it on faith that the market is correct. If you don't agree that the market is correct then you don't have to agree with this, but if you don't agree with the market, in my opinion, you're only setting yourself up for disappointment and unneeded pain. I know that this idea sounds wrong and unfair, but it is what it is. You have to understand that you're a limited human being who doesn't know better than millions of people making choices on what they like or dislike, have some humility. :)


Blaming external factors

This brings me to my last point, which is that in general indie developers have a problem with blaming external factors instead of themselves for their failures. I'm not going to expand much on this because it should be an article on its own, but in general if you hear the words "luck" or "lottery" it should be a red flag that the person you're talking to is infected with the "it's not my fault" disease. It's important to get rid of this mindset and to notice it on yourself, because it a subtle and insidious killer that keeps people from growing as developers and as human beings. Just focus on becoming a better developer and making better games!

tl;dr hidden gems don't exist, the market is truth, Valve is the best company ever, good = popular, don't blame luck for your failures

BYTEPATH #3 - Rooms and Areas

Introduction

In this article we'll cover some structural code needed before moving on to the actual game. We'll explore the idea of Rooms, which are equivalent to what's called a scene in other engines. And then we'll explore the idea of an Area, which is an object management type of construct that can go inside a Room. Like the two previous tutorials, this one will still have no code specific to the game and will focus on higher level architectural decisions.


Room

I took the idea of Rooms from GameMaker's documentation. One thing I like to do when figuring out how to approach a game architecture problem is to see how other people have solved it, and in this case, even though I've never used GameMaker, their idea of a Room and the functions around it gave me some really good ideas.

As the description there says, Rooms are where everything happens in a game. They're the places where all game objects will be created, updated and drawn and you can change from one Room to the other. Those rooms are also normal objects that I'll place inside a rooms folder. This is what one room called Stage would look like:

Stage = Object:extend()

function Stage:new()

end

function Stage:update(dt)

end

function Stage:draw()

end

Simple Rooms

At its simplest form this system only needs one additional variable and one additional function to work:

function love.load()
    current_room = nil
end

function love.update(dt)
    if current_room then current_room:update(dt) end
end

function love.draw()
    if current_room then current_room:draw() end
end

function gotoRoom(room_type, ...)
    current_room = _G[room_type](...)
end

At first in love.load a global current_room variable is defined. The idea is that at all times only one room can be currently active and so that variable will hold a reference to the current active room object. Then in love.update and love.draw, if there is any room currently active it will be updated and drawn. This means that all rooms must have an update and a draw function defined.

The gotoRoom function can be used to change between rooms. It receives a room_type, which is just a string with the name of the class of the room we want to change to. So, for instance, if there's a Stage class defined as a room, it means the 'Stage' string can be passed in. This works based on how the automatic loading of classes was set up in the previous tutorial, which loads all classes as global variables.

In Lua, global variables are held in a global environment table called _G, so this means that they can be accessed like any other variable in a normal table. If the Stage global variable contains the definition of the Stage class, it can be accessed by just saying Stage anywhere on the program, or also by saying _G['Stage'] or _G.Stage. Because we want to be able to load any arbitrary room, it makes sense to receive the room_type string and then access the class definition via the global table.

So in the end, if room_type is the string 'Stage', the line inside the gotoRoom function parses to current_room = Stage(...), which means that a new Stage room is being instantiated. This also means that any time a change to a new room happens, that new room is created from zero and the previous room is deleted. The way this works in Lua is that whenever a table is not being referred to anymore by any variables, the garbage collector will eventually collect it. And so when the instance of the previous room stops being referred to by the current_room variable, eventually it will be collected.

There are obvious limitations to this setup, for instance, often times you don't want rooms to be deleted when you change to a new one, and often times you don't want a new room to be created from scratch every time you change to it. Avoiding this becomes impossible with this setup.

For this game though, this is what I'll use. The game will only have 3 or 4 rooms, and all those rooms don't need continuity between each other, i.e. they can be created from scratch and deleted any time you move from one to the other and it works fine.


Let's go over a small example of how we can map this system onto a real existing game. Let's look at Nuclear Throne:

Watch the first minute or so of this video until the guy dies once to get an idea of what the game is like.

The game loop is pretty simple and, for the purposes of this simple room setup it fits perfectly because no room needs continuity with previous rooms. (you can't go back to a previous map, for instance) The first screen you see is the main menu:

I'd make this a MainMenu room and in it I'd have all the logic needed for this menu to work. So the background, the five options, the effect when you select a new option, the little bolts of lightning on the edges of screen, etc. And then whenever the player would select an option I would call gotoRoom(option_type), which would swap the current room to be the one created for that option. So in this case there would be additional Play, CO-OP, Settings and Stats rooms.

Alternatively, you could have one MainMenu room that takes care of all those additional options, without the need to separate it into multiple rooms. Often times it's a better idea to keep everything in the same room and handle some transitions internally rather than through the external system. It depends on the situation and in this case there's not enough details to tell which is better.

Anyway, the next thing that happens in the video is that the player picks the play option, and that looks like this:

New options appear and you can choose between normal, daily or weekly mode. Those only change the level generation seed as far as I remember, which means that in this case we don't need new rooms for each one of those options (can just pass a different seed as argument in the gotoRoom call). The player chooses the normal option and this screen appears:

I would call this the CharacterSelect room, and like the others, it would have everything needed to make that screen happen, the background, the characters in the background, the effects that happen when you move between selections, the selections themselves and all the logic needed for that to happen. Once the character is chosen the loading screen appears:

Then the game:

When the player finishes the current level this screen popups before the transition to the next one:

Once the player selects a passive from previous screen another loading screen is shown. Then the game again in another level. And then when the player dies this one:

All those are different screens and if I were to follow the logic I followed until now I'd make them all different rooms: LoadingScreen, Game, MutationSelect and DeathScreen. But if you think more about it some of those become redundant.

For instance, there's no reason for there to be a separate LoadingScreen room that is separate from Game. The loading that is happening probably has to do with level generation, which will likely happen inside the Game room, so it makes no sense to separate that to another room because then the loading would have to happen in the LoadingScreen room, and not on the Game room, and then the data created in the first would have to be passed to the second. This is an overcomplication that is unnecessary in my opinion.

Another one is that the death screen is just an overlay on top of the game in the background (which is still running), which means that it probably also happens in the same room as the game. I think in the end the only one that truly could be a separate room is the MutationSelect screen.

This means that, in terms of rooms, the game loop for Nuclear Throne, as explored in the video would go something like: MainMenu -> Play -> CharacterSelect -> Game -> MutationSelect -> Game -> .... Then whenever a death happens, you can either go back to a new MainMenu or retry and restart a new Game. All these transitions would be achieved through the simple gotoRoom function.


Persistent Rooms

For completion's sake, even though this game will not use this setup, I'll go over one that supports some more situations:

function love.load()
    rooms = {}
    current_room = nil
end

function love.update(dt)
    if current_room then current_room:update(dt) end
end

function love.draw()
    if current_room then current_room:draw() end
end

function addRoom(room_type, room_name, ...)
    local room = _G[room_type](room_name, ...)
    rooms[room_name] = room
    return room
end

function gotoRoom(room_type, room_name, ...)
    if current_room and rooms[room_name] then
        if current_room.deactivate then current_room:deactivate() end
        current_room = rooms[room_name]
        if current_room.activate then current_room:activate() end
    else current_room = addRoom(room_type, room_name, ...) end
end

In this case, on top of providing a room_type string, now a room_name value is also passed in. This is because in this case I want rooms to be able to be referred to by some identifier, which means that each room_name must be unique. This room_name can be either a string or a number, it really doesn't matter as long as it's unique.

The way this new setup works is that now there's an addRoom function which simply instantiates a room and stores it inside a table. Then the gotoRoom function, instead of instantiating a new room every time, can now look in that table to see if a room already exists, if it does, then it just retrieves it, otherwise it creates a new one from scratch.

Another difference here is the use of the activate and deactivate functions. Whenever a room already exists and you ask to go to it again by calling gotoRoom, first the current room is deactivated, the current room is changed to the target room, and then that target room is activated. These calls are useful for a number of things like saving data to or loading data from disk, dereferencing variables (so that they can get collected) and so on.

In any case, what this new setup allows for is for rooms to be persistent and to remain in memory even if they aren't active. Because they're always being referenced by the rooms table, whenever current_room changes to another room, the previous one won't be garbage collected and so it can be retrieved in the future.


Let's look at an example that would make good use of this new system, this time with The Binding of Isaac:

Watch the first minute or so of this video. I'm going to skip over the menus and stuff this time and mostly focus on the actual gameplay. It consists of moving from room to room killing enemies and finding items. You can go back to previous rooms and those rooms retain what happened to them when you were there before, so if you killed the enemies and destroyed the rocks of a room, when you go back it will have no enemies and no rocks. This is a perfect fit for this system.

The way I'd setup things would be to have a Room room where all the gameplay of a room happens. And then a general Game room that coordinates things at a higher level. So, for instance, inside the Game room the level generation algorithm would run and from the results of that multiple Room instances would be created with the addRoom call. Each of those instances would have their unique IDs, and when the game starts, gotoRoom would be used to activate one of those. As the player moves around and explores the dungeon further gotoRoom calls would be made and already created Room instances would be activated/deactivated as the player moves about.

One of the things that happens in Isaac is that as you move from one room to the other there's a small transition that looks like this:

I didn't mention this in the Nuclear Throne example either, but that also has a few transitions that happen in between rooms. There are multiple ways to approach these transitions, but in the case of Isaac it means that two rooms need to be drawn at once, so using only one current_room variable doesn't really work. I'm not going to go over how to change the code to fix this, but I thought it'd be worth mentioning that the code I provided is not all there is to it and that I'm simplifying things a bit. Once I get into the actual game and implement transitions I'll cover this is more detail.


Room Exercises

44. Create three rooms: CircleRoom which draws a circle at the center of the screen; RectangleRoom which draws a rectangle at the center of the screen; and PolygonRoom which draws a polygon to the center of the screen. Bind the keys F1, F2 and F3 to change to each room.

45. What is the closest equivalent of a room in the following engines: Unity, GODOT, HaxeFlixel, Construct 2 and Phaser. Go through their documentation and try to find out. Try to also see what methods those objects have and how you can change from one room to another.

46. Pick two single player games and break them down in terms of rooms like I did for Nuclear Throne and Isaac. Try to think through things realistically and really see if something should be a room on its own or not. And try to specify when exactly do addRoom or gotoRoom calls would happen.

47. In a general way, how does the garbage collector in Lua work? (and if you don't know what a garbage collector is then read up on that) How can memory leaks happen in Lua? What are some ways to prevent those from happening or detecting that they are happening?


Areas

Now for the idea of an Area. One of the things that usually has to happen inside a room is the management of various objects. All objects need to be updated and drawn, as well as be added to the room and removed from it when they're dead. Sometimes you also need to query for objects in a certain area (say, when an explosion happens you need to deal damage to all objects around it, this means getting all objects inside a circle and dealing damage to them), as well as applying certain common operations to them like sorting them based on their layer depth so they can be drawn in a certain order. All these functionalities have been the same across multiple rooms and multiple games I've made, so I condensed them into a class called Area:

Area = Object:extend()

function Area:new(room)
    self.room = room
    self.game_objects = {}
end

function Area:update(dt)
    for _, game_object in ipairs(self.game_objects) do game_object:update(dt) end
end

function Area:draw()
    for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end

The idea is that this object will be instantiated inside a room. At first the code above only has a list of potential game objects, and those game objects are being updated and drawn. All game objects in the game will inherit from a single GameObject class that has a few common attributes that all objects in the game will have. That class looks like this:

GameObject = Object:extend()

function GameObject:new(area, x, y, opts)
    local opts = opts or {}
    if opts then for k, v in pairs(opts) do self[k] = v end end

    self.area = area
    self.x, self.y = x, y
    self.id = UUID()
    self.dead = false
    self.timer = Timer()
end

function GameObject:update(dt)
    if self.timer then self.timer:update(dt) end
end

function GameObject:draw()

end

The constructor receives 4 arguments: an area, x, y position and an opts table which contains additional optional arguments. The first thing that's done is to take this additional opts table and assign all its attributes to this object. So, for instance, if we create a GameObject like this game_object = GameObject(area, x, y, {a = 1, b = 2, c = 3}), the line for k, v in pairs(opts) do self[k] = v is essentially copying the a = 1, b = 2 and c = 3 declarations to this newly created instance. By now you should be able to understand how this works, if you don't then read up more on the OOP section in the past article as well as how tables in Lua work.

Next, the reference to the area instance passed in is stored in self.area, and the position in self.x, self.y. Then an ID is defined for this game object. This ID should be unique to each object so that we can identify which object is which without conflict. For the purposes of this game a simple UUID generating function will do. Such a function exists in a library called lume in lume.uuid. We're not going to use this library, only this one function, so it makes more sense to just take that one instead of installing the whole library:

function UUID()
    local fn = function(x)
        local r = math.random(16) - 1
        r = (x == "x") and (r + 1) or (r % 4) + 9
        return ("0123456789abcdef"):sub(r, r)
    end
    return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end

I place this code in a file named utils.lua. This file will contain a bunch of utility functions that don't really fit anywhere. What this function spits out is a string like this '123e4567-e89b-12d3-a456-426655440000' that for all intents and purposes is going to be unique.

One thing to note is that this function uses the math.random function. If you try doing print(UUID()) to see what it generates, you'll find that every time you run the project it's going to generate the same IDs. This problem happens because the seed used is always the same. One way to fix this is to, as the program starts up, randomize the seed based on the time, which can be done like this math.randomseed(os.time()).

However, what I did was to just use love.math.random instead of math.random. If you remember the first article of this series, the first function called in the love.run function is love.math.randomSeed(os.time()), which does exactly the same job of randomizing the seed, but for LÖVE's random generator instead. Because I'm using LÖVE, whenever I need some random functionality I'm going to use its functions instead of Lua's as a general rule. Once you make that change in the UUID function you'll see that it starts generating different IDs.

Back to the game object, the dead variable is defined. The idea is that whenever dead becomes true the game object will be removed from the game. Then an instance of the Timer class is assigned to each game object as well. I've found that timing functions are used on almost every object, so it just makes sense to have it as a default for all of them. Finally, the timer is updated on the update function.

Given all this, the Area class should be changed as follows:

Area = Object:extend()

function Area:new(room)
    self.room = room
    self.game_objects = {}
end

function Area:update(dt)
    for i = #self.game_objects, 1, -1 do
        local game_object = self.game_objects[i]
        game_object:update(dt)
        if game_object.dead then table.remove(self.game_objects, i) end
    end
end

function Area:draw()
    for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end

The update function now takes into account the dead variable and acts accordingly. First, the game object is update normally, then a check to see if it's dead happens. If it is, then it's simply removed from the game_objects list. One important thing here is that the loop is happening backwards, from the end of the list to the start. This is because if you remove elements from a Lua table while moving forward in it it will end up skipping some elements, as this discussion shows.

Finally, one last thing that should be added is an addGameObject function, which will add a new game object to the Area:

function Area:addGameObject(game_object_type, x, y, opts)
    local opts = opts or {}
    local game_object = _G[game_object_type](self, x or 0, y or 0, opts)
    table.insert(self.game_objects, game_object)
    return game_object
end

It would be called like this area:addGameObject('ClassName', 0, 0, {optional_argument = 1}). The game_object_type variable will work like the strings in the gotoRoom function work, meaning they're names for the class of the object to be created. _G[game_object_type], in the example above, would parse to the ClassName global variable, which would contain the definition for the ClassName class. In any case, an instance of the target class is created, added to the game_objects list and then returned. Now this instance will be updated and drawn every frame.

And that how this class will work for now. This class is one that will be changed a lot as the game is built but this should cover the basic behavior it should have (adding, removing, updating and drawing objects).


Area Exercises

48. Create a Stage room that has an Area in it. Then create a Circle object that inherits from GameObject and add an instance of that object to the Stage room at a random position every 2 seconds. The Circle instance should kill itself after a random amount of time between 2 and 4 seconds.

49. Create a Stage room that has no Area in it. Create a Circle object that does not inherit from GameObject and add an instance of that object to the Stage room at a random position every 2 seconds. The Circle instance should kill itself after a random amount of time between 2 and 4 seconds.

50. The solution to exercise 1 introduced the random function. Augment that function so that it can take only one value instead of two and it should generate a random real number between 0 and the value on that case (when only one argument is received). Also augment the function so that min and max values can be reversed, meaning that the first value can be higher than the second.

51. What is the purpose of the local opts = opts or {} in the addGameObject function?



BYTEPATH #8 - Enemies

Introduction

In this article we'll go over the creation of a few enemies as well as the EnemyProjectile class, which is a projectile that some enemies can shoot to hurt the player. This article will be a bit shorter than others since we won't focus on creating all enemies now, only the basic behavior that will be shared among most of them.


Enemies

Enemies in this game will work in a similar way to how the resources we created in the last article worked, in the sense that they will be spawned at either left or right of the screen at a random y position and then they will slowly move inward. The code used to make that work will be exactly the same as the one used in each resource we implemented.

We'll get started with the first enemy which is called Rock. It looks like this:

The constructor code for this object will be very similar to the Boost one, but with a few small differences:

function Rock:new(area, x, y, opts)
    Rock.super.new(self, area, x, y, opts)

    local direction = table.random({-1, 1})
    self.x = gw/2 + direction*(gw/2 + 48)
    self.y = random(16, gh - 16)

    self.w, self.h = 8, 8
    self.collider = self.area.world:newPolygonCollider(createIrregularPolygon(8))
    self.collider:setPosition(self.x, self.y)
    self.collider:setObject(self)
    self.collider:setCollisionClass('Enemy')
    self.collider:setFixedRotation(false)
    self.v = -direction*random(20, 40)
    self.collider:setLinearVelocity(self.v, 0)
    self.collider:applyAngularImpulse(random(-100, 100))
end

Here instead of the object using a RectangleCollider, it will use a PolygonCollider. We create the vertices of this polygon with the function createIrregularPolygon which will be defined in utils.lua. This function should return a list of vertices that make up an irregular polygon. What I mean by an irregular polygon is one that is kinda like a circle but where each vertex might be a bit closer or further away from the center, and where the angles between each vertex may be a little random as well.

To start with the definition of that function we can say that it will receive two arguments: size and point_amount. The first will refer to the radius of the circle and the second will refer to the number of points that will compose the polygon:

function createIrregularPolygon(size, point_amount)
    local point_amount = point_amount or 8
end

Here we also say that if point_amount isn't defined it will default to 8.

The next thing we can do is start defining all the points. This can be done by going from 1 to point_amount and in each iteration defining the next vertex based on an angle interval. So, for instance, to define the position of the second point we can say that its angle will be somewhere around 2*angle_interval, where angle_interval is the value 2*math.pi/point_amount. So in this case it would be around 90 degrees. This probably makes more sense in code, so:

function createIrregularPolygon(size, point_amount)
    local point_amount = point_amount or 8
    local points = {}
    for i = 1, point_amount do
        local angle_interval = 2*math.pi/point_amount
        local distance = size + random(-size/4, size/4)
        local angle = (i-1)*angle_interval + random(-angle_interval/4, angle_interval/4)
        table.insert(points, distance*math.cos(angle))
        table.insert(points, distance*math.sin(angle))
    end
    return points
end

And so here we define angle_interval like previously explained, but we also define distance as being around the radius of the circle, but with a random offset between -size/4 and +size/4. This means that each vertex won't be exactly on the edge of the circle but somewhere around it. We also randomize the angle interval a bit to create the same effect. Finally, we add both x and y components to a list of points which is then returned. Note that the polygon is created in local space (assuming that the center is 0, 0), which means that we have to then use setPosition to place the object in its proper place.

Another difference in the constructor of this object is the use of the Enemy collision class. Like all other collision classes, this one should be defined before it can be used:

function Stage:new()
    ...
    self.area.world:addCollisionClass('Enemy')
    ...
end

In general, new collision classes should be added for object types that will have different collision behaviors between each other. For instance, enemies will physically ignore the player but will not physically ignore projectiles. Because no other object type in the game follows this behavior, it means we need to add a new collision class to do it. If the Projectile collision class only ignored the player instead of also ignoring other projectiles, then enemies would be able to have their collision class be Projectile as well.

The last thing about the Rock object is how its drawn. Since it's just a polygon we can simply draw its points using love.graphics.polygon:

function Rock:draw()
    love.graphics.setColor(hp_color)
    local points = {self.collider:getWorldPoints(self.collider.shapes.main:getPoints())}
    love.graphics.polygon('line', points)
    love.graphics.setColor(default_color)
end

We get its points first by using PolygonShape:getPoints. These points are returned in local coordinates, but we want global ones, so we have to use Body:getWorldPoints to convert from local to global coordinates. Once that's done we can just draw the polygon and it will behave like expected. Note that because we're getting points from the collider directly and the collider is a polygon that is rotating around, we don't need to use pushRotate to rotate the object like we did for the Boost object since the points we're getting are already accounting for the objects rotation.

If you do all this it should look like this:


Enemies Exercises

110. Perform the following tasks:

  • Add an attribute named hp to the Rock class that initially is set to 100
  • Add a function named hit to the Rock class. This function should do the following:
    • It should receives a damage argument, and in case it isn't then it should default to 100
    • damage will be subtracted from hp and if hp hits 0 or lower then the Rock object will die
    • If hp doesn't hit 0 or lower then an attribute named hit_flash will be set to true and then set to false 0.2 seconds later. In the draw function of the Rock object, whenever hit_flash is set to true the color of the object will be set to default_color instead of hp_color.

111. Create a new class named EnemyDeathEffect. This effect gets created whenever an enemy dies and it behaves exactly like the ProjectileDeathEffect object, except that it is bigger according to the size of the Rock object. This effect should be created whenever the Rock object's hp attribute hits 0 or lower.

112. Implement the collision event between an object of the Projectile collision class and an object of the Enemy collision class. In this case, implement it in the Projectile's class update function. Whenever the projectile hits an object of the Enemy class, it should call the enemy's hit function with the amount of damage this projectile deals (by default, projectiles will have an attribute named damage that is initially set to 100). Whenever a hit happens the projectile should also call its own die function.

113. Add a function named hit to the Player class. This function should do the following things:

  • It should receive a damage argument, and in case it isn't defined it should default to 10
  • This function should not do anything whenever the invincible attribute is true
  • Between 4 and 8 ExplodeParticle objects should be spawned
  • The addHP (or removeHP function if you decided to add this one) should take the damage attribute and use it to remove HP from the Player. Inside the addHP (or removeHP) function there should be a way to deal with the case where hp hits or goes below 0 and the player dies.

Additionally, the following conditional operations should hold:

  • If the damage received is equal to or above 30, then the invincible attribute should be set to true and 2 seconds later it should be set to false. On top of that, the camera should shake with intensity 6 for 0.2 seconds, the screen should flash for 3 frames, and the game should be slowed to 0.25 for 0.5 seconds. Finally, an invisible attribute should alternate between true and false every 0.04 for the duration that invincible is set to true, and additionally the Player's draw function shouldn't draw anything whenever invisible is set to true.
  • If the damage received is below 30, then the camera should shake with intensity 6 for 0.1 seconds, the screen should flash for 2 frames, and the game should be slowed to 0.75 for 0.25 seconds.

This hit function should be called whenever the Player collides with an Enemy. The player should be hit for 30 damage on enemy collision.


After finishing these 4 exercises you should have completed everything needed for the interactions between Player, Projectile and Rock enemy to work like they should in the game. These interactions will hold true and be similar for other enemies as well. And it all should look like this:


EnemyProjectile

So, now we can focus on another part of making enemies which is creating enemies that can shoot projectiles. A few enemies will be able to do that and so we need to create an object, like the Projectile one, but that is used by enemies instead. For this we'll create the EnemyProjectile object.

This object can be created at first by just copypasting the code for the Projectile one and changing it slightly. Both these objects will share a lot of the same code. We could somehow abstract them out into a general projectile-like object that has the common behavior, but that's really not necessary since these are the only two types of projectiles the game will have. After the copypasting is done the things we have to change are these:

function EnemyProjectile:new(...)
    ...
    self.collider:setCollisionClass('EnemyProjectile')
end

The collision class of an EnemyProjectile should also be EnemyProjectile. We want EnemyProjectiles objects to ignore other EnemyProjectiles, Projectiles and the Player. So we must add the collision class such that it fits that purpose:

function Stage:new()
    ...
    self.area.world:addCollisionClass('EnemyProjectile', 
    {ignores = {'EnemyProjectile', 'Projectile', 'Enemy'}})
end

The other main thing we have to change is the damage. A normal projectile shot by the player deals 100 damage, but a projectile shot by an enemy should deal 10 damage:

function EnemyProjectile:new(...)
    ...
    self.damage = 10
end

Another thing is that we want projectiles shot by enemies to collide with the Player but not with other enemies. So we can take the collision code that the Projectile object used and just turn it around on the Player instead:

function EnemyProjectile:update(dt)
    ...
    if self.collider:enter('Player') then
        local collision_data = self.collider:getEnterCollisionData('Player')
	...
    end

Finally, we want this object to look completely red instead of half-red and half-white, so that the player can tell projectiles shot from an enemy to projectiles shot by himself:

function EnemyProjectile:draw()
    love.graphics.setColor(hp_color)
    ...
    love.graphics.setColor(default_color)
end

With all these small changes we have successfully create the EnemyProjectile object. Now we need to create an enemy that will use it!


Shooter

This is what the Shooter enemy looks like:

As you can see, there's a little effect and then after a projectile is fired. The projectile looks just like the player one, except it's all red.

We can start making this enemy by copypasting the code from the Rock object. This enemy (and all enemies) will share the same property that they code from either left or right of then screen and then move inwards slowly, and since the Rock object already has that code taken care of we can start from there. Once that's done, we have to change a few things:

function Shooter:new(...)
    ...
    self.w, self.h = 12, 6
    self.collider = self.area.world:newPolygonCollider(
    {self.w, 0, -self.w/2, self.h, -self.w, 0, -self.w/2, -self.h})
end

The width, height and vertices of the Shooter enemy are different from the Rock. With the rock we just created an irregular polygon, but here we need the enemy to have a well defined and pointy shape so that the player can instinctively tell where it will come from. The setup here for the vertices is similar to how we did it for designing ships, so if you want you can change the way this enemy looks and make it look cooler.

function Shooter:new(...)
    ...
    self.collider:setFixedRotation(false)
    self.collider:setAngle(direction == 1 and 0 or math.pi)
    self.collider:setFixedRotation(true)
end

The other thing we need to change is that unlike the rock, setting the object's velocity is not enough. We must also set its angle so that physics collider points in the right direction. To do this we first need to disable its fixed rotation (otherwise setting the angle won't work), change the angle, and then set its fixed rotation back to true. The rotation is set to fixed again because we don't want the collider to spin around if something hits it, we want it to remain pointing to the direction it's moving towards.

The line direction == 1 and math.pi or 0 is basically how you do a ternary operator in Lua. In other languages this would look like (direction == 1) ? math.pi : 0. The exercises back in article 2 and 4 I think went over this in detail, but essentially what will happen is that if direction is 1 (coming from the right and pointing to the left) then the first conditional will parse to true, which leaves us with true and math.pi or 0. Because of precedence between and and or, true and math.pi will go first, which leaves us with math.pi or 0, which will return math.pi, since or returns the first element whenever both are true. On the other hand, if direction is -1, then the first conditional will parse to false and we'll have false and math.pi or 0, which will parse to false or 0, which will parse to 0, since or returns the second element whenever the first is false.

With all this, we can spawn Shooter objects and they should look like this:

Now we need to create the pre-attack effect. Usually in most games whenever an enemy is about to attack something happens that tells the player that enemy is about to attack. Most of the time it's an animation, but it could also be an effect. In our case we'll use a simple "charging up" effect, where a bunch of particles are continually sucked into the point where the projectile will come from until the release happens.

At a high level this is how we'll do it:

function Player:new(...)
    ...
  	
    self.timer:every(random(3, 5), function()
        -- spawn PreAttackEffect object with duration of 1 second
        self.timer:after(1, function()
            -- spawn EnemyProjectile
        end)
    end)
end

So this means that with an interval of between 3 and 5 seconds each Shooter enemy will shoot a new projectile. This will happen after the PreAttackEffect effect is up for 1 second.

The basic way effects like these that have to do with particles work is that, like with the trails, some type of particle will be spawned every frame or every other frame and that will make the effect work. In this case, we will spawn particles called TargetParticle. These particles will move towards a point we define as the target and then die after a duration or when they reach the point.

function TargetParticle:new(area, x, y, opts)
    TargetParticle.super.new(self, area, x, y, opts)

    self.r = opts.r or random(2, 3)
    self.timer:tween(opts.d or random(0.1, 0.3), self, 
    {r = 0, x = self.target_x, y = self.target_y}, 'out-cubic', function() self.dead = true end)
end

function TargetParticle:draw()
    love.graphics.setColor(self.color)
    draft:rhombus(self.x, self.y, 2*self.r, 2*self.r, 'fill')
    love.graphics.setColor(default_color)
end

Here each particle will be tweened towards target_x, target_y over a d duration (or a random value between 0.1 and 0.3 seconds), and when that position is reached then the particle will die. The particle is also drawn as a rhombus (like one of the effects we made earlier), but it could be drawn as a circle or rectangle since it's small enough and gets smaller over the tween duration.

The way we create these objects in PreAttackEffect looks like this:

function PreAttackEffect:new(...)
    ...
    self.timer:every(0.02, function()
        self.area:addGameObject('TargetParticle', 
        self.x + random(-20, 20), self.y + random(-20, 20), 
        {target_x = self.x, target_y = self.y, color = self.color})
    end)
end

So here we spawn one particle every 0.02 seconds (almost every frame) in a random location around its position, and then we set the target_x, target_y attributes to the position of the effect itself (which will be at the tip of the ship).

In Shooter, we create PreAttackEffect like this:

function Shooter:new(...)
    ...
    self.timer:every(random(3, 5), function()
        self.area:addGameObject('PreAttackEffect', 
        self.x + 1.4*self.w*math.cos(self.collider:getAngle()), 
        self.y + 1.4*self.w*math.sin(self.collider:getAngle()), 
        {shooter = self, color = hp_color, duration = 1})
        self.timer:after(1, function()
         
        end)
    end)
end

The initial position we set should be at the tip of the Shooter object, and so we can use the general math.cos and math.sin pattern we've been using so far to achieve that and account for both possible angles (0 and math.pi). We also pass a duration attribute, which controls how long the PreAttackEffect object will stay alive for. Back there we can do this:

function PreAttackEffect:new(...)
    ...
    self.timer:after(self.duration - self.duration/4, function() self.dead = true end)
end

The reason we don't use duration by itself here is because this object is what I call in my head a controller object. For instance, it doesn't have anything in its draw function, so we never actually see it in the game. What we see are the TargetParticle objects that it commands to spawn. Those objects have a random duration of between 0.1 and 0.3 seconds each, which means if we want the last particles to end right as the projectile is being shot, then this object has to die between 0.1 and 0.3 seconds earlier than its 1 second duration. As a general case thing I decided to make this 0.75 (duration - duration/4), but it could be another number that is closer to 0.9 instead.

In any case, if you run everything now it should look like this:

And this works well enough. But if you pay attention you'll notice that the target position of the particles (the position of the PreAttackEffect object) is staying still instead of following the Shooter. We can fix this in the same way we fixed the ShootEffect object for the player. We already have the shooter attribute pointing to the Shooter object that created the PreAttackEffect object, so we can just update PreAttackEffect's position based on the position of this shooter parent object:

function PreAttackEffect:update(dt)
    ...
    if self.shooter and not self.shooter.dead then
        self.x = self.shooter.x + 1.4*self.shooter.w*math.cos(self.shooter.collider:getAngle())
    	self.y = self.shooter.y + 1.4*self.shooter.w*math.sin(self.shooter.collider:getAngle())
    end
end

And so here every frame we're updating this objects position to be at the tip of the Shooter object that created it. If you run this it would look like this:

One important thing about the update code about is the not self.shooter.dead part. One thing that can happen when we reference objects within each other like this is that one object dies while another still holds a reference to it. For instance, the PreAttackEffect object lasts 0.75 seconds, but between its creation and its demise, the Shooter object that created it can be killed by the player, and if that happens problems can occur.

In this case the problem is that we have to access the Shooter's collider attribute, which gets destroyed whenever the Shooter object dies. And if that object is destroyed we can't really do anything with it because it doesn't exist anymore, so when we try to getAngle it that will crash our game. We could work out a general system that solves this problem but I don't really think that's necessary. For now we should just be careful whenever we reference objects like this to make sure that we don't access objects that might be dead.

Now for the final part, which is the one where we create the EnemyProjectile object. For now we'll handle this relatively simply by just spawning it like we would spawn any other object, but with some specific attributes:

function Shooter:new(...)
    ...
    self.timer:every(random(3, 5), function()
        ...
        self.timer:after(1, function()
            self.area:addGameObject('EnemyProjectile', 
            self.x + 1.4*self.w*math.cos(self.collider:getAngle()), 
            self.y + 1.4*self.w*math.sin(self.collider:getAngle()), 
            {r = math.atan2(current_room.player.y - self.y, current_room.player.x - self.x), 
             v = random(80, 100), s = 3.5})
        end)
    end)
end

Here we create the projectile at the same position that we created the PreAttackEffect, and then we set its velocity to a random value between 80 and 100, and then its size to be slightly larger than the default value. The most important part is that we set its angle (r attribute) to point towards the player. In general, whenever you want something to get the angle of something from source to target, you should do:

angle = math.atan2(target.y - source.y, target.x - source.x)

And that's what we're doing here. After the object is spawned it will point itself towards the player and move there. It should look like this:

If you compare this to the initial gif on this section this looks a bit different. The projectiles there have a period where they slowly turn towards the player rather than coming out directly towards him. This uses the same piece of code as the homing passive that we will add eventually, so I'm gonna leave that for later.

One thing that will happen for the EnemyProjectile object is that eventually it will be filled with lots of functionality so that it can serve the purposes of lots of different enemies. All this functionality, however, will first be implemented in the Projectile object because it will serve as a passive to the player. So, for instance, there's a passive that makes projectiles circle around the player. Once we implement that, we can copypaste that code to the EnemyProjectile object and then implement an enemy that makes use of that idea and has projectiles that circle it instead. A number of enemies will be created in this way so those will be left as an exercise for when we implement passives for the player.

For now, we'll stay with those two enemies (Rock and Shooter) and with the EnemyProjectile object as it is and move on to other things, but we'll come back to create more enemies in the future as we add more functionality to the game.


EnemyProjectile/Shooter Exercises

114. Implement a collision event between Projectile and EnemyProjectile. In the EnemyProjectile class, make it so that whenever it hits an object of the Projectile class, both object's die function will be called and they will both be destroyed.

115. Is the way the direction attribute is named confusing in the Shooter class? If so, what could it be named instead? If not, how is it not?



BYTEPATH Postmortem

It's been about a month and a half since I released BYTEPATH and I think it's been long enough that writing a postmortem makes sense. I've already written a more technical and programming oriented postmortem-ish article here so go check it if you're interested in that. This article will focus more on anything that isn't programming related.


Context

This game came about because I had failed for about 5 years on 3 other projects prior to this one, and I decided that I had enough and that I'd just finish and release a game no matter what. I picked a relatively simple gameplay loop to make by copying Bit Blaster XL's. Then I tried to pick something else that would keep people playing the game for longer than if it was just like Bit Blaster XL, and the best way I know of doing that is just adding lots of variety to it, so I decided to copy the idea behind Path of Exile's passive skill tree. This meant that while the gameplay was simple, the surrounding systems had enough complexity that I could grow and learn something from a technical perspective, which is also good since generally I don't want to be working on projects that I learn nothing from.

On top of making the game I also wrote a tutorial on how to make it from scratch. I wrote this tutorial both because I wanted to see if I actually enjoyed writing tutorials in a thorough manner, and to also check if the "tutorial market" was a viable way of making money on top of just making games. I think it's well known that there are many young people who are interested in making games and it would follow that those people would be willing to spend money on material aimed at teaching them how to make a game. And so it's worth it to check if there are enough of them to make writing said material worth it.


Expectations

The game itself was made entirely with programmer art and a few shaders here and there, so my expectations for how it would do were low. My goal going into this project was to just make the $100 I paid to get on Steam back, which meant that at $2 a copy, I'd have to sell 500 copies, since you need to make at least $1000 to make the entry fee back. I think for someone's first game and for a game with no proper art that's a good goal.

As for the tutorial I was set on selling each copy (which contained the game's source code, answers to exercises and a Steam key to the game) at $25. My expectations were that I wouldn't really sell that many copies, like 10 at most, so making the price a lot higher like that made sense. The reason for this is that while the tutorial is very thorough and educational, the number of people who are interested in LÖVE and Lua would be pretty low, and the number of people in that pool that would be OK with paying $25 for source code would be even smaller.

As for marketing efforts, my plan was to just post the game and the tutorial on a few specific subreddits and on Hacker News. My posts have been "at the top" of a bunch of subreddits and of HN before and so I know that the number of views that these sites generate can get pretty crazy. And my thought process was that if I could just repeat that again for this game it would give it a really good visibility boost. I knew the tutorial itself would get to the top of /r/programming, /r/gamedev and HN without many problems, because it's just a lot of content and a lot of good content, and generally it's hard for people to ignore and not upvote stuff like that. As for the game itself I wasn't sure if I would make it to the top of any of my target subreddits because the game just didn't look good enough.


Financial Results

The combined amount of money made on both the game and the tutorial, after all cuts and taxes, was about $3k. I live in Brazil, so after currency conversion this amounts to ~R$10k. This was well beyond my initial expectations so in this sense the game was a huge success. If we were to look at this from a "can I do this for a living" perspective then it wasn't a success though. The game itself took about 4 months to make, and the tutorial took about the same amount of time. These 8 months were spread unevenly along 1 year where in between I had breaks where I worked on other projects or didn't work on anything. If we take the amount of time it took for this project to be anywhere between 8 and 12 months then I made either slightly below local minimum wage or slightly above it, which isn't enough to do it for a living.

However, I think it's unreasonble to expect your first game to be a massive success or anything like that, and it's also unreasonable to expect that of a game with no art. At the end of the day this game just wasn't good enough and the market responded appropriately to its quality. One of the big mistakes I see other indie developers making is not being realistic about their experience and the quality of their games, and if you can't do that then you're setting yourself up for disappointment.

As for the tutorial, I didn't enjoy writing it as much as I thought I would, and it took the same amount of time to make as the actual game. So even though the tutorial sales also exceed my expectations by a lot, in the future I'm not going to write more tutorials like this. They just take too much time and effort for not enough gain. If I were using a more popular engine like Unity then the pool of people who would be interested would be higher and maybe it would be worth it, but since I'm not then that's that.


Marketing Results

The most important lessons I learned from making this game were technical ones, but the second most important were marketing related. A lot of the "success" that I was able to have was due to my marketing efforts so I think it's a good idea to expand on those.


Design with marketing purpose #1

The main piece of the game's design that I settled on that would also serve as its main marketing tool was the huge skill tree. I still remember when I first saw Path of Exile's skill tree and the feeling it gave me and it seems that most people when looking at it for the first time have that same feeling:

And so my idea was to make my own version of that. The results of this weren't as good as I'd hoped for because my skill tree didn't look good, I think. While some people were impressed by it and got the game solely because of this, my overall conversion rate on the Steam store was pretty low I think, which means that having a picture of the skill tree as the first picture that was displayed when people hovered over the game didn't work that well.


Design with marketing purpose #2

The second way in which I designed the game with marketing in mind was by making it a console/terminal/hacking type of game. Because the game used programmer art I had very little options in terms of how I would present the game thematically, and so this one just fit like a glove. There are also a number of people who like games that contain terminals in them, and so this matched it perfectly. Additionally, I also made sure to port the game to Linux, to further increase my chances of capturing the types of people who are interested in terminal games in general.

The Linux idea turned out to be not so great because they amounted to like 5% of my sales. Not a lot, really. But there were a number of people who bought the game and mentioned that they bought it alone for the console/terminal part of it, and not because of the skill tree. Which means that as an additional marketing tool it sort of worked.


Tutorial marketing

I managed to get my tutorial on the top of Hacker News for half a day with this thread, on the top of /r/programming for a day with this thread and on the top of /r/gamedev with this thread. I posted these about a week before the game was going to be released because I thought it would be a good idea to generate some visibility for the game before people have the opportunity to buy it in the first place. This decision was OK I think but I wonder how things would have gone if I released the tutorial and the game on the same day. In any case, these threads generated about 25k views on my GitHub blog over the next few days when they were released:

And this also generated a similar looking graph on itch.io in terms of tutorial sales:

Overall the response to the tutorial in terms of attention it got was expected, the number of purchases surprised me though, since I thought they would be lower. One of the strategies I used to increase the chances that the thread I created about this on HN would go to the top was to post it at low traffic times. I think on HN I posted the thread on a Saturday night, and so that meant that just a small amount of upvotes quickly made the thread rise up to the top and then it stayed on the front page for a while. On reddit's smaller subreddits doing this isn't necessary, especially on /r/programming and /r/gamedev, since those subs are small enough that good content naturally rises to the top no matter the time. But on bigger subreddits a strategy like this might also help.


Game marketing

About a week after posting the tutorial I released the game and my main way of marketing the game would be through reddit as well. The primary subreddit that I wanted to target was /r/pathofexile. My game is inspired by it, it's a very big subreddit full of people who are interested in that game, so it just makes the most sense to do it there primarily. One of the obvious problems with this is that I'm posting about a game that isn't PoE on the PoE subreddit. I had no way of knowing if the moderators would be cool with this, or even if the developers of the game would be cool with this, since they're very active on the subreddit and browse it all the time.

However, I did my best to increase the chances of it being OK by timing it correctly, I think. One of the things about PoE is that every 3-4 months they release a new league to the game. The next league was going to come out on March 2nd, and what happens is that as the league goes people gradually stop playing and then 1-2 weeks before the next league starts they start getting hyped for it again. My idea was to release the game on Feburary 23rd, 1 week before the new league, to maximize on the number of people who are getting hyped about the new league but not actually playing PoE, but to also not intrude too much on the next league's actual release date, since the devs like to do a lot of hyping up themselves on the subreddit by releasing a lot of previews of what's coming (and they generally like to do this 1-2 weeks before the league releases).

I posted the game on the subreddit on this thread and I used the same strategy that I mentioned above for HN. I posted it on a Friday night, at an hour of very low traffic. Another advantage of posting at this time is that the developers of the game are from New Zealand, which meant that they were probably well awake and checking the subreddit. This meant that if they weren't OK with this kind of shilling on their subreddit they would be able to do something about it quickly if they wanted to.

After posting it immediately got a bunch of upvotes and shot up to the front of the subreddit. One of the developers of the game commented on it rather quickly and then the main developer also did. So at the end of the day they were OK with it! :D However, after about 1 hour of the thread being up it got removed by the moderators because they didn't allow for products to be linked on their subreddit like that. I messaged them and after a few minutes of internal discussion they reinstated the thread! And after that it stayed on the front page of the subreddit for about 1 day and generated around 40k views total.

I posted the game on a few other smaller subreddits as well and I tried posting it on HN but it didn't really get any traction there. At the end of the day the game was put on the radar and got a lot of attention because of the /r/pathofexile thread, so thanks to the developers and to the moderators for being OK with it ^^


Further marketing

The only other marketing I did was to write a programming/technical postmortem on the game about 3 days after its release. I have many opinions that would be considered controversial by most people when it comes to a lot of subjects, so here the only notable thing I did was to write an article that had many of those controversial opinions because that would generate discussion around it and hopefully spread the article more. This somewhat worked well. I posted the article on /r/programming and /r/gamedev as you can see it generated a lot of lively discussion. In terms of numbers this article managed to get about the same amount of views as when I first posted the tutorial, which was very good:

This article took me less than a day to write and for the amount of time it took it was a very good amount of visibility. Overall, all these marketing efforts amounted to about 40% of the visits on my game's Steam page, which is higher than I would have expected. Steam itself generated TONS of views on my game, but because I didn't do a good job at converting those people it didn't generate as many visits as one would expect.

And other than that, I didn't contact any streamers, youtubers or journalists, because I didn't believe this game would get any traction with any of those people. Again, it's a game that doesn't look good at all and it would have been mostly a waste of time to try.


Improvements for the Future

Despite a lot of my efforts doing well, there are a lot of things that I didn't do, either because I had no time, or because I just didn't think this game deserved the amount of effort it would take. I still think those things are important though so I might as well list them out too.


Trailer

BYTEPATH's trailer turned out OK I guess but nothing special:

And it turned out this way because I'm not too experienced with making trailers yet but also I didn't have the tools I wanted available in hand. However, I deeply believe that the trailer is the most important marketing asset you have for your game, and that it actually should take precedence over making the game. One of the things that I wanna do in the future is that once I'm done with prototyping some game and I decide that I'm going to make it for real, I will first make a trailer for it, without the game actually being done at all.

This serves the main purpose of figuring out how to make the game marketable. You can have a really cool game prototype that plays well, but it means nothing if it isn't a marketable idea. Forcing and releasing a trailer before committing to making the full game will serve as a very good test of its marketability: it will either fade into obscurity or get some attention. If it fades into obscurity then you know that you need to change the way you present it or change games altogether, and if it gets some attention then you know you're on the right path, and you also get some community building going on.


Game design with marketing in mind

This goes in accordance with the previous point, but the game should be thought up with how marketable it is in mind from the very beginning. For BYTEPATH this took the shape of the skill tree and of the console/terminal look, but it also took the form of having a target "place" where I would post the game on release too, like /r/pathofexile. I think this is also something really important that people don't mention or think about often.

My next game, Frogfaller, has this problem. When I was first thinking it up I wasn't taking this into account that much, so now I have a design in my hands that I just don't know how to market yet. I can't think of a single place on the Internet where this game is a perfect fit for, like /r/pathofexile was for BYTEPATH, and I think that this is very bad because it just makes marketing it harder. On the other hand, the artist I'm working with on it makes super good looking art so that also makes it easier to market it, but ideally you want both good art and a good and well defined target audience that can be reached easily.

For instance, the game I'm planning to make after Frogfaller will be a clone of Recettear. It will have the anime style and similar gameplay, and I can think of like 100 places on the Internet where if I said something like "I'm making an anime game like Recettear" and I posted a good looking screenshot with proper anime art and all that, it would be very very well received. So again, it's simply a matter of designing a game with a well defined group of people in mind. If I can't think up of a few places where I'll post the game when it releases then I would probably rethink the game entirely.


Streamable game

I've been watching a lot of Twitch streams lately as well and streamers in general can be a very good source of sales. But for streamers to play a game it has to be "streamable". I haven't managed to logically pin down what makes a game streamable yet entirely, but if you give me a list of 10 games and have me watch 30 minutes of gameplay of each, I'll be able to tell you which ones are streamable and which ones are not.

In general people have a sense that roguelites for instance are streamable, because if you die you can just start another run and play what essentially amounts to a different game. But I think there's a more general idea behind it that has to do with unexpected things happening. Horror games, for instance, seem to be very streamable for this reason. A game like Path of Exile on the other hand doesn't have that going for it at all. A good recent example is Getting Over It, which has a gameplay loop that just consistently generates "streamable moments" in the form of progression loss:

As well as general taunting and scares:

I feel like you want a game that generates moments that brings the streamer and chat together and that those moments more easily happen when something unexpected happens. But they can also happen if you make a game with Twitch chat integration, for instance. In those cases the chat can vote of what happens in the game and they can decide to fuck the streamer over or help him. Another way of looking at this is that you want to make the streamer's job (entertaining thousands of people) easier, so you need to do a lot of the heavy lifting in your game and place the streamer in positions where he can easily just react to something that happened in the game.


Art

Finally, with making a marketable and streamable game in general comes the idea of making the game look good. This is another area where I think lots of people don't focus on enough I think. However before I go on I should say that I'm not an artist so what I'm about to say next might be wrong or misguided, but it's what I currently believe to be true.

A lot of artists that I follow seem to have the problem that they can't "compose a screen" properly. I don't know if there's an actual art term for "composing a screen", but it basically means putting the pieces of the game together on the screen in a way that looks good. There are many many many artists who I see can draw individual assets really well, but they just can't put those assets together in a way that looks pleasing. And this is something that sinks A LOT of indie games that would otherwise do well, in my opinion. It's by far one of the most important things I look for in an artist if I'm going to make something with one. Here's one example of a game that fails at this in my opinion:

Tangledeep is an example where the assets look good but for one reason or another (perhaps noisy background assets) it just doesn't come together as a good looking screen. Obviously a lot of this is subjective but it's something that I pay attention to a lot and that I think more people should pay attention to as well. I'm sure that artists talk about this and they have proper terms for this idea and it's actually something that people study (hopefully), so it's not like people can't get better at it.


Marketing in general

I talked a lot about marketing in this post, and in general I think it should be my goal to design a game that markets itself so that marketing it is easier, but that I should also focus my marketing efforts on places that I understand. The sites I use right now that can be used for marketing purposes are in order of use frequency: twitch, 4chan, reddit and YouTube. Along with that there are a bunch of Discord servers I frequent that can be used for marketing as well.

With that in mind, I think it's reasonable to focus my marketing efforts on those 5 places that I frequently use and that I understand well, while avoiding the ones that I don't use at all. For instance, I don't read and have never read any gaming journalism site. While those sites can generate a lot of views for me, it just seems like a waste of time to cold e-mail hundreds of journalists who will end up ignoring any of my games when I don't even use their service.

It makes much more sense to market on a service that I actually enjoy using because will be advantages that come with understanding how it works that will make my job easier. For instance, with Twitch generally you can just donate a small amount of money to a streamer and a robot will read your message out loud and the streamer will respond to it. If a streamer is ever looking for a game to play live, suggesting your game is something that you might do that could be very very effective. The worst thing that could happen is that you would waste like $5. The same goes for reddit, I use reddit all the time, which means that I understand how the rules of the site work and that I have a very old account that is unlikely to be confused for a random spammer.

Marketing in the places that you frequently use is something that can get you tons of advantages, and avoiding the places that you don't use can also save you time and effort that would have been better spent elsewhere. On top of that, "shaping" yourself to use the most popular sites and services is a useful skill to have. If you're someone who hates streaming you might want to not hate it so much and eventually even like it, because it's useful as an indie to be in touch with the gaming community as much as possible, since it will make your job a lot easier.

BYTEPATH #6 - Player Basics

Introduction

In this section we'll focus on adding more functionality to the Player class. First we'll focus on the player's attack and the Projectile object. After that we'll focus on two of the main stats that the player will have: Boost and Cycle/Tick. And finally we'll start on the first piece of content that will be added to the game, which is different Player ships. From this section onward we'll also only focus on gameplay related stuff, while the previous 5 were mostly setup for everything.


Player Attack

The way the player will attack in this game is that each n seconds an attack will be triggered and executed automatically. In the end there will be 16 types of attacks, but pretty much all of them have to do with shooting projectiles in the direction that the player is facing. For instance, this one shoots homing projectiles:

While this one shoots projectiles at a faster rate but at somewhat random angles:

Attacks and projectiles will have all sorts of different properties and be affected by different things, but the core of it is always the same.

To achieve this we first need to make it so that the player attacks every n seconds. n is a number that will vary based on the attack, but the default one will be 0.24. Using the timer library that was explained in a previous section we can do this easily:

function Player:new()
    ...
    self.timer:every(0.24, function()
        self:shoot()
    end)
end

With this we'll be calling a function called shoot every 0.24 seconds and inside that function we'll place the code that will actually create the projectile object.

So, now we can define what will happen in the shoot function. At first, for every shot fired we'll have a small effect to signify that a shot was fired. A good rule of thumb I have is that whenever an entity is created or deleted from the game, an accompanying effect should appear, as it masks the fact that an entity just appeared/disappeared out of nowhere on the screen and generally makes things feel better.

To create this new effect first we need to create a new game object called ShootEffect (you should know how to do this by now). This effect will simply be a square that lasts for a very small amount of time around the position where the projectile will be created from. The easiest way to get that going is something like this:

function Player:shoot()
    self.area:addGameObject('ShootEffect', self.x + 1.2*self.w*math.cos(self.r), 
    self.y + 1.2*self.w*math.sin(self.r))
end
function ShootEffect:new(...)
    ...
    self.w = 8
    self.timer:tween(0.1, self, {w = 0}, 'in-out-cubic', function() self.dead = true end)
end

function ShootEffect:draw()
    love.graphics.setColor(default_color)
    love.graphics.rectangle('fill', self.x - self.w/2, self.y - self.w/2, self.w, self.w)
end

And that looks like this:

The effect code is rather straight forward. It's just a square of width 8 that lasts for 0.1 seconds, and this width is tweened down to 0 along that duration. One problem with the way things are now is that the effect's position is static and doesn't follow the player. It seems like a small detail because the duration of the effect is small, but try changing that to 0.5 seconds or something longer and you'll see what I mean.

One way to fix this is to pass the Player object as a reference to the ShootEffect object, and so in this way the ShootEffect object can have its position synced to the Player object:

function Player:shoot()
    local d = 1.2*self.w

    self.area:addGameObject('ShootEffect', self.x + d*math.cos(self.r), 
    self.y + d*math.sin(self.r), {player = self, d = d})
end
function ShootEffect:update(dt)
    ShootEffect.super.update(self, dt)
    if self.player then 
    	self.x = self.player.x + self.d*math.cos(self.player.r) 
    	self.y = self.player.y + self.d*math.sin(self.player.r) 
  	end
end

function ShootEffect:draw()
    pushRotate(self.x, self.y, self.player.r + math.pi/4)
    love.graphics.setColor(default_color)
    love.graphics.rectangle('fill', self.x - self.w/2, self.y - self.w/2, self.w, self.w)
    love.graphics.pop()
end

The player attribute of the ShootEffect object is set to self in the player's shoot function via the opts table. This means that a reference to the Player object can be accessed via self.player in the ShootEffect object. Generally this is the way we'll pass references of objects from one another, because usually objects get created from within another objects function, which means that passing self achieves what we want. Additionally, we set the d attribute to the distance the effect should appear at from the center of the Player object. This is also done through the opts table.

Then in ShootEffect's update function we set its position to the player's. It's important to always check if the reference that will be accessed is actually set (if self.player then) because if it isn't than an error will happen. And often times, as we build more, it will be the case that entities will die while being referenced somewhere else and we'll try to access some of its values, but because it died, those values aren't set anymore and then an error is thrown. It's important to keep this in mind when referencing entities within each other like this.

Finally, the last detail is that I make it so that the square is synced with the player's angle, and then I also rotate that by 45 degrees to make it look cooler. The function used to achieve that was pushRotate and it looks like this:

function pushRotate(x, y, r)
    love.graphics.push()
    love.graphics.translate(x, y)
    love.graphics.rotate(r or 0)
    love.graphics.translate(-x, -y)
end

This is a simple function that pushes a transformation to the transformation stack. Essentially it will rotate everything by r around point x, y until we call love.graphics.pop. So in this example we have a square and we rotate around its center by the player's angle plus 45 degrees (pi/4 radians). For completion's sake, the other version of this function which also contains scaling looks like this:

function pushRotateScale(x, y, r, sx, sy)
    love.graphics.push()
    love.graphics.translate(x, y)
    love.graphics.rotate(r or 0)
    love.graphics.scale(sx or 1, sy or sx or 1)
    love.graphics.translate(-x, -y)
end

These functions are pretty useful and will be used throughout the game so make sure you play around with them and understand them well!


Player Attack Exercises

80. Right now, we simply use an initial timer call in the player's constructor telling the shoot function to be called every 0.24 seconds. Assume an attribute self.attack_speed exists in the Player which changes to a random value between 1 and 2 every 5 seconds:

function Player:new(...)
    ...

    self.attack_speed = 1
    self.timer:every(5, function() self.attack_speed = random(1, 2) end)

    self.timer:every(0.24, function() self:shoot() end)

How would you change the player object so that instead of shooting every 0.24 seconds, it shoots every 0.24/self.attack_speed seconds? Note that simply changing the value in the every call that calls the shoot function will not work.

81. In the last article we went over garbage collection and how forgotten references can be dangerous and cause leaks. In this article I explained how we will reference objects within one another using the Player and ShootEffect objects as examples. In this instance where the ShootEffect is a short-lived object that contains a reference to the Player inside it, do we need to care about dereferencing the Player reference so that it can be collected eventually or is it not necessary? In a more general way, when do we need to care about dereferencing objects that reference each other like this?

82. Using pushRotate, rotate the player around its center by 180 degrees. It should look like this:

83. Using pushRotate, rotate the line that points in the player's moving direction around its center by 90 degrees. It should look like this:

84. Using pushRotate, rotate the line that points in the player's moving direction around the player's center by 90 degrees. It should look like this:

85. Using pushRotate, rotate the ShootEffect object around the player's center by 90 degrees (on top of already rotating it by the player's direction). It should look like this:


Player Projectile

Now that we have the shooting effect done we can move on to the actual projectile. The projectile will have a movement mechanism that is very similar to the player's in that it's a physics object that has an angle and then we'll set its velocity according to that angle. So to start with, the call inside the shoot function:

function Player:shoot()
    ...
    self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), 
    self.y + 1.5*d*math.sin(self.r), {r = self.r})
end

And this should have nothing unexpected. We use the same d variable that was defined earlier to set the Projectile's initial position, and then pass the player's angle as the r attribute. Note that unlike the ShootEffect object, the Projectile won't need anything more than the angle of the player when it was created, and so we don't need to pass the player in as a reference.

Now for the Projectile's constructor. The Projectile object will also have a circle collider (like the Player), a velocity and a direction its moving along:

function Projectile:new(area, x, y, opts)
    Projectile.super.new(self, area, x, y, opts)

    self.s = opts.s or 2.5
    self.v = opts.v or 200

    self.collider = self.area.world:newCircleCollider(self.x, self.y, self.s)
    self.collider:setObject(self)
    self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r))
end

The s attribute represents the radius of the collider, it isn't r because that one already is used for the movement angle. In general I'll use variables w, h, r or s to represent object sizes. The first two when the object is a rectangle, and the other two when it's a circle. In cases where the r variable is already being used for a direction (like in this one), then s will be used for the radius. Those attributes are also mostly for visual purposes, since most of the time those objects already have the collider doing all collision related work.

Another thing we do here, which I think I already explained in another article, is the opts.attribute or default_value construct. Because of the way or works in Lua, we can use this construct as a fast way of saying this:

if opts.attribute then
    self.attribute = opts.attribute
else 
    self.attribute = default_value 
end

We're checking to see if the attribute exists, and then setting some variable to that attribute, and if it doesn't then we set it to a default value. In the case of self.s, it will be set to opts.s if it was defined, otherwise it will be set to 2.5. The same applies to self.v. Finally, we set the projectile's velocity by using setLinearVelocity with the initial velocity of the projectile and the angle passed in from the Player. This uses the same idea that the Player uses for movement so that should be already understood.

If we now update and draw the projectile like:

function Projectile:update(dt)
    Projectile.super.update(self, dt)
    self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r))
end

function Projectile:draw()
    love.graphics.setColor(default_color)
    love.graphics.circle('line', self.x, self.y, self.s)
end

And that should look like this:


Player Projectile Exercises

86. From the player's shoot function, change the size/radius of the created projectiles to 5 and their velocity to 150.

87. Change the shoot function to spawn 3 projectiles instead of 1, while 2 of those projectiles are spawned with angles pointing to the player's angle +-30 degrees. It should look like this:

88. Change the shoot function to spawn 3 projectiles instead of 1, with the spawning position of each side projectile being offset from the center one by 8 pixels. It should look like this:

89. Change the initial projectile speed to 100 and make it accelerate up to 400 over 0.5 seconds after its creation.


Player & Projectile Death

Now that the Player can move around and attack in a basic way, we can start worrying about some additional rules of the game. One of those rules is that if the Player hits the edge of the play area, he will die. The same should be the case for Projectiles, since right now they are being spawned but they never really die, and at some point there will be so many of them alive that the game will slow down considerably.

So let's start with the Projectile object:

function Projectile:update(dt)
    ...

    if self.x < 0 then self:die() end
    if self.y < 0 then self:die() end
    if self.x > gw then self:die() end
    if self.y > gh then self:die() end
end

We know that the center of the play area is located at gw/2, gh/2, which means that the top-left corner is at 0, 0 and the bottom-right corner is at gw, gh. And so all we have to do is add a few conditionals to the update function of a projectile checking to see if its position is beyond any of those edges, and if it is, we call the die function.

The same logic applies for the Player object:

function Player:update(dt)
    ...

    if self.x < 0 then self:die() end
    if self.y < 0 then self:die() end
    if self.x > gw then self:die() end
    if self.y > gh then self:die() end
end

Now for the die function. This function is very simple and essentially what it will do it set the dead attribute to true for the entity and then spawn some visual effects. For the projectile the effect spawned will be called ProjectileDeathEffect, and like the ShootEffect, it'll be a square that lasts for a small amount of time and then disappears, although with a few differences. The main difference is that ProjectileDeathEffect will flash for a while before turning to its normal color and then disappearing. This gives a subtle but nice popping effect that looks good in my opinion. So the constructor could look like this:

function ProjectileDeathEffect:new(area, x, y, opts)
    ProjectileDeathEffect.super.new(self, area, x, y, opts)

    self.first = true
    self.timer:after(0.1, function()
        self.first = false
        self.second = true
        self.timer:after(0.15, function()
            self.second = false
            self.dead = true
        end)
    end)
end

We defined two attributes, first and second, which will denote in which stage the effect is in. If in the first stage, then its color will be white, while in the second its color will be what its color should be. After the second stage is done then the effect will die, which is done by setting dead to true. This all happens in a span of 0.25 seconds (0.1 + 0.15) so it's a very short lived and quick effect. Now for how the effect should be drawn, which is very similar to how ShootEffect was drawn:

function ProjectileDeathEffect:draw()
    if self.first then love.graphics.setColor(default_color)
    elseif self.second then love.graphics.setColor(self.color) end
    love.graphics.rectangle('fill', self.x - self.w/2, self.y - self.w/2, self.w, self.w)
end

Here we simply set the color according to the stage, as I explained, and then we draw a rectangle of that color. To create this effect, we do it from the die function in the Projectile object:

function Projectile:die()
    self.dead = true
    self.area:addGameObject('ProjectileDeathEffect', self.x, self.y, 
    {color = hp_color, w = 3*self.s})
end

One of the things I failed to mention before is that the game will have a finite amount of colors. I'm not an artist and I don't wanna spend much time thinking about colors, so I just picked a few of them that go well together and used them everywhere. Those colors are defined in globals.lua and look like this:

default_color = {222, 222, 222}
background_color = {16, 16, 16}
ammo_color = {123, 200, 164}
boost_color = {76, 195, 217}
hp_color = {241, 103, 69}
skill_point_color = {255, 198, 93}

For the projectile death effect I'm using hp_color (red) to show what the effect looks like, but the proper way to do this in the future will be to use the color of the projectile object. Different attack types will have different colors and so the death effect will similarly have different colors based on the attack. In any case, the way the effect looks like is this:


Now for the Player death effect. The first thing we can do is mirror the Projectile die function and set dead to true when the Player reaches the edges of the screen. After that is done we can do some visual effects for it. The main visual effect for the Player death will be a bunch of particles that appear called ExplodeParticle, kinda like an explosion but not really. In general the particles will be lines that move towards a random angle from their initial position and slowly decrease in length. A way to get this working would be something like this:

function ExplodeParticle:new(area, x, y, opts)
    ExplodeParticle.super.new(self, area, x, y, opts)

    self.color = opts.color or default_color
    self.r = random(0, 2*math.pi)
    self.s = opts.s or random(2, 3)
    self.v = opts.v or random(75, 150)
    self.line_width = 2
    self.timer:tween(opts.d or random(0.3, 0.5), self, {s = 0, v = 0, line_width = 0}, 
    'linear', function() self.dead = true end)
end

Here we define a few attributes, most of them are self explanatory. The additional thing we do is that over a span of between 0.3 and 0.5 seconds, we tween the size, velocity and line width of the particle to 0, and after that tween is done, the particle dies. The movement code for particle is similar to the Projectile, as well as the Player, so I'm going to skip it. It simply follows the angle using its velocity.

And finally the particle is drawn as a line:

function ExplodeParticle:draw()
    pushRotate(self.x, self.y, self.r)
    love.graphics.setLineWidth(self.line_width)
    love.graphics.setColor(self.color)
    love.graphics.line(self.x - self.s, self.y, self.x + self.s, self.y)
    love.graphics.setColor(255, 255, 255)
    love.graphics.setLineWidth(1)
    love.graphics.pop()
end

As a general rule, whenever you have to draw something that is going to be rotated (in this case by the angle of direction of the particle), draw is as if it were at angle 0 (pointing to the right). So, in this case, we have to draw the line from left to right, with the center being the position of rotation. So s is actually half the size of the line, instead of its full size. We also use love.graphics.setLineWidth so that the line is thicker at the start and then becomes skinnier as time goes on.

The way these particles are created is rather simple. Just create a random number of them on the die function:

function Player:die()
    self.dead = true 
    for i = 1, love.math.random(8, 12) do 
    	self.area:addGameObject('ExplodeParticle', self.x, self.y) 
  	end
end

One last thing you can do is to bind a key to trigger the Player's die function, since the effect won't be able to be seen properly at the edge of the screen:

function Player:new(...)
    ...

    input:bind('f4', function() self:die() end)
end

And all that looks like this:

This doesn't look very dramatic though. One way of really making something seem dramatic is by slowing time down a little. This is something a lot of people don't notice, but if you pay attention lots of games slow time down slightly whenever you get hit or whenever you die. A good example is Downwell, this video shows its gameplay and I marked the time when a hit happens so you can pay attention and see it for yourself.

Doing this ourselves is rather easy. First we can define a global variable called slow_amount in love.load and set it to 1 initially. This variable will be used to multiply the delta that we send to all our update functions. So whenever we want to slow time down by 50%, we set slow_amount to 0.5, for instance. Doing this multiplication can look like this:

function love.update(dt)
    timer:update(dt*slow_amount)
    camera:update(dt*slow_amount)
    if current_room then current_room:update(dt*slow_amount) end
end

And then we need to define a function that will trigger this work. Generally we want the time slow to go back to normal after a small amount of time. So it makes sense that this function should have a duration attached to it, on top of how much the slow should be:

function slow(amount, duration)
    slow_amount = amount
    timer:tween('slow', duration, _G, {slow_amount = 1}, 'in-out-cubic')
end

And so calling slow(0.5, 1) means that the game will be slowed to 50% speed initially and then over 1 second it will go back to full speed. One important thing to note here that the 'slow' string is used in the tween function. As explained in an earlier article, this means that when the slow function is called when the tween of another slow function call is still operating, that other tween will be cancelled and the new tween will continue from there, preventing two tweens from operating on the same variable at the same time.

If we call slow(0.15, 1) when the player dies it looks like this:

Another thing we can do is add a screen shake to this. The camera module already has a :shake function to it, and so we can add the following:

function Player:die()
    ...
    camera:shake(6, 60, 0.4)
    ...
end

And finally, another thing we can do is make the screen flash for a few frames. This is something else that lots of games do that you don't really notice, but it helps sell an effect really well. This is a rather simple effect: whenever we call flash(n), the screen will flash with the background color for n frames. One way we can do this is by defining a flash_frames global variable in love.load that starts as nil. Whenever flash_frames is nil it means that the effect isn't active, and whenever it's not nil it means it's active. The flash function looks like this:

function flash(frames)
    flash_frames = frames
end

And then we can set this up in the love.draw function:

function love.draw()
    if current_room then current_room:draw() end

    if flash_frames then 
        flash_frames = flash_frames - 1
        if flash_frames == -1 then flash_frames = nil end
    end
    if flash_frames then
        love.graphics.setColor(background_color)
        love.graphics.rectangle('fill', 0, 0, sx*gw, sy*gh)
        love.graphics.setColor(255, 255, 255)
    end
end

First, we decrease flash_frames by 1 every frame, and then if it reaches -1 we set it to nil because the effect is over. And then whenever the effect is not over, we simply draw a big rectangle covering the whole screen that is colored as background_color. Adding this to the die function like this:

function Player:die()
    self.dead = true 
    flash(4)
    camera:shake(6, 60, 0.4)
    slow(0.15, 1)

    for i = 1, love.math.random(8, 12) do 
    	self.area:addGameObject('ExplodeParticle', self.x, self.y) 
  	end
end

Gets us this:

Very subtle and barely noticeable, but it's small details like these that make things feel more impactful and nicer.


Player/Projectile Death Exercises

90. Without using the first and second attribute and only using a new current_color attribute, what is another way of achieving the changing colors of the ProjectileDeathEffect object?

91. Change the flash function to accept a duration in seconds instead of frames. Which one is better or is it just a matter of preference? Could the timer module use frames instead of seconds for its durations?


Player Tick

Now we'll move on to another crucial part of the Player which is its cycle mechanism. The way the game works is that in the passive skill tree there will be a bunch of skills you can buy that will have a chance to be triggered on each cycle. And a cycle is just a counter that is triggered every n seconds. We need to set this up in a basic way. And to do that we'll just make it so that the tick function is called every 5 seconds:

function Player:new(...)
    ...

    self.timer:every(5, function() self:tick() end)
end

In the tick function, for now the only thing we'll do is add a little visual effect called TickEffect any time a tick happens. This effect is similar to the refresh effect in Downwell (see Downwell video I mentioned earlier in this article), in that it's a big rectangle over the Player that goes up a little. It looks like this:

The first thing to notice is that it's a big rectangle that covers the player and gets smaller over time. But also that, like the ShootEffect, it follows the player. Which means that we know we'll need to pass the Player object as a reference to the TickEffect object:

function Player:tick()
    self.area:addGameObject('TickEffect', self.x, self.y, {parent = self})
end
function TickEffect:update(dt)
    ...

    if self.parent then self.x, self.y = self.parent.x, self.parent.y end
end

Another thing we can see is that it's a rectangle that gets smaller over time, but only in height. An easy way to do that is like this:

function TickEffect:new(area, x, y, opts)
    TickEffect.super.new(self, area, x, y, opts)

    self.w, self.h = 48, 32
    self.timer:tween(0.13, self, {h = 0}, 'in-out-cubic', function() self.dead = true end)
end

If you try this though, you'll see that the rectangle isn't going up like it should and it's just getting smaller around the middle of the player. One day to fix this is by introducing an y_offset attribute that gets bigger over time and that is subtracted from the y position of the TickEffect object:

function TickEffect:new(...)
    ...

    self.y_offset = 0
    self.timer:tween(0.13, self, {h = 0, y_offset = 32}, 'in-out-cubic', 
    function() self.dead = true end)
end

function TickEffect:update(dt)
    ...

    if self.parent then self.x, self.y = self.parent.x, self.parent.y - self.y_offset end
end

And in this way we can get the desired effect. For now this is all that the tick function will do. Later as we add stats and passives it will have more stuff attached to it.


Player Boost

Another important piece of gameplay is the boost. Whenever the user presses up, the player should start moving faster. And whenever the user presses down, the player should start moving slower. This boost mechanic is a core part of the gameplay and like the tick, we'll focus on the basics of it now and later add more to it.

First, lets get the button pressing to work. One of the attributes we have in the player is max_v. This sets the maximum velocity with which the player can move. What we want to do whenever up/down is pressed is change this value so that it becomes higher/lower. The problem with doing this is that after the button is done being pressed we need to go back to the normal value. And so we need another variable to hold the base value and one to hold the current value.

Whenever there's a stat (like velocity) that needs to be changed in game by modifiers, this (needing a base value and a current one) is a very common pattern. Later on as we add more stats and passives into the game we'll go into this with more detail. But for now we'll add an attribute called base_max_v, which will contain the initial/base value of the maximum velocity, and the normal max_v attribute will hold the current maximum velocity, affected by all sorts of modifiers (like the boost).

function Player:new(...)
    ...

    self.base_max_v = 100
    self.max_v = self.base_max_v
end

function Player:update(dt)
    ...

    self.max_v = self.base_max_v
    if input:down('up') then self.max_v = 1.5*self.base_max_v end
    if input:down('down') then self.max_v = 0.5*self.base_max_v end
end

With this, every frame we're setting max_v to base_max_v and then we're checking to see if the up or down buttons are pressed and changing max_v appropriately. It's important to notice that this means that the call to setLinearVelocity that uses max_v has to happen after this, otherwise it will all fall apart horribly!

Now that we have the basic boost functionality working, we can add some visuals. The way we'll do this is by adding trails to the player object. This is what they'll look like:

The creation of trails in general follow a pattern. And the way I do it is to create a new object every frame or so and then tween that object down over a certain duration. As the frames pass and you create object after object, they'll all be drawn near each other and the ones that were created earlier will start getting smaller while the ones just created will still be bigger, and the fact that they're all created from the bottom part of the player and the player is moving around, means that we'll get the desired trail effect.

To do this we can create a new object called TrailParticle, which will essentially just be a circle with a certain radius that gets tweened down along some duration:

function TrailParticle:new(area, x, y, opts)
    TrailParticle.super.new(self, area, x, y, opts)

    self.r = opts.r or random(4, 6)
    self.timer:tween(opts.d or random(0.3, 0.5), self, {r = 0}, 'linear', 
    function() self.dead = true end)
end

Different tween modes like 'in-out-cubic' instead of 'linear', for instance, will make the trail have a different shape. I used the linear one because it looks the best to me, but your preference might vary. The draw function for this is just drawing a circle with the appropriate color and with the radius using the r attribute.

On the Player object's end, we can create new TrailParticles like this:

function Player:new(...)
    ...

    self.trail_color = skill_point_color 
    self.timer:every(0.01, function()
        self.area:addGameObject('TrailParticle', 
        self.x - self.w*math.cos(self.r), self.y - self.h*math.sin(self.r), 
        {parent = self, r = random(2, 4), d = random(0.15, 0.25), color = self.trail_color}) 
    end)

And so every 0.01 seconds (this is every frame, essentially), we spawn a new TrailParticle object behind the player, with a random radius between 2 and 4, random duration between 0.15 and 0.25 seconds, and color being skill_point_color, which is yellow.

One additional thing we can do is changing the color of the particles to blue whenever up or down is being pressed. To do this we must add some logic to the boost code, namely, we need to be able to tell when a boost is happening, and to do this we'll add a boosting attribute. Via this attribute we'll be able to know when a boost is happening and then change the color being referenced in trail_color accordingly:

function Player:update(dt)
    ...

    self.max_v = self.base_max_v
    self.boosting = false
    if input:down('up') then 
        self.boosting = true
        self.max_v = 1.5*self.base_max_v 
    end
    if input:down('down') then 
        self.boosting = true
        self.max_v = 0.5*self.base_max_v 
    end
    self.trail_color = skill_point_color 
    if self.boosting then self.trail_color = boost_color end
end

And so with this we get what we wanted by changing trail_color to boost_color (blue) whenever the player is being boosted.


Player Ship Visuals

Now for the last thing this article will cover: ships! The game will have various different ship types that the player can be, each with different stats, passives and visuals. Right now we'll focus only the visual part and we'll add 1 ship, and as an exercise you'll have to add 7 more.

One thing that I should mention now that will hold true for the entire tutorial is something regarding content. Whenever there's content to be added to the game, like various ships, or various passives, or various options in a menu, or building the skill tree visually, etc, you'll have to do most of that work yourself. In the tutorial I'll cover how to do it once, but when that's covered and it's only a matter of manually and mindlessly adding more of the same, it will be left as an exercise.

This is both because covering literally everything with all details would take a very long time and make the tutorial super big, and also because you need to learn if you actually like doing the manual work of adding content into the game. A big part of game development is just adding content and not doing anything "new", and depending on who you are personality wise you may not like the fact that there's a bunch of work to do that's just dumb work that might be not that interesting. In those cases you need to learn this sooner rather than later, because it's better to then focus on making games that don't require a lot of manual work to be done, for instance. This game is totally not that though. The skill tree will have about 800 nodes and all those have to be set manually (and you will have to do that yourself if you want to have a tree that big), so it's a good way to learn if you enjoy this type of work or not.

In any case, let's get started on one ship. This is what it looks like:

As you can see, it has 3 parts to it, one main body and two wings. The way we'll draw this is as a collection of simple polygons, and so we have to define 3 different polygons. We'll define the polygon's positions as if it is turned to the right (0 angle, as I explained previously). It will be something like this:

function Player:new(...)
    ...

    self.ship = 'Fighter'
    self.polygons = {}

    if self.ship == 'Fighter' then
        self.polygons[1] = {
            ...
        }

        self.polygons[2] = {
            ...
        }

        self.polygons[3] = {
            ...
        }
    end
end

And so inside each polygon table, we'll define the vertices of the polygon. To draw these polygons we'll have to do some work. First, we need to rotate the polygons around the player's center:

function Player:draw()
    pushRotate(self.x, self.y, self.r)
    love.graphics.setColor(default_color)
    -- draw polygons here
    love.graphics.pop()
end

After this, we need to go over each polygon:

function Player:draw()
    pushRotate(self.x, self.y, self.r)
    love.graphics.setColor(default_color)
    for _, polygon in ipairs(self.polygons) do
        -- draw each polygon here
    end
    love.graphics.pop()
end

And then we draw each polygon:

function Player:draw()
    pushRotate(self.x, self.y, self.r)
    love.graphics.setColor(default_color)
    for _, polygon in ipairs(self.polygons) do
        local points = fn.map(polygon, function(k, v) 
        	if k % 2 == 1 then 
          		return self.x + v + random(-1, 1) 
        	else 
          		return self.y + v + random(-1, 1) 
        	end 
      	end)
        love.graphics.polygon('line', points)
    end
    love.graphics.pop()
end

The first thing we do is getting all the points ordered properly. Each polygon will be defined in local terms, meaning, a distance from the center of that is assumed to be 0, 0. This means that each polygon does now not know at which position it is in the game world yet.

The fn.map function goes over each element in a table and applies a function to it. In this case the function is checking to see if the index of the element is odd or even, and if its odd then it means it's for the x component, and if its even then it means it's for the y component. And so in each of those cases we simply add the x or y position of the player to the vertex, as a well as a random number between -1 and 1, so that the ship looks a bit wobbly and cooler. Then, finally, love.graphics.polygon is called to draw all those points.

Now, here's what the definition of each polygon looks like:

self.polygons[1] = {
    self.w, 0, -- 1
    self.w/2, -self.w/2, -- 2
    -self.w/2, -self.w/2, -- 3
    -self.w, 0, -- 4
    -self.w/2, self.w/2, -- 5
    self.w/2, self.w/2, -- 6
}

self.polygons[2] = {
    self.w/2, -self.w/2, -- 7
    0, -self.w, -- 8
    -self.w - self.w/2, -self.w, -- 9
    -3*self.w/4, -self.w/4, -- 10
    -self.w/2, -self.w/2, -- 11
}

self.polygons[3] = {
    self.w/2, self.w/2, -- 12
    -self.w/2, self.w/2, -- 13
    -3*self.w/4, self.w/4, -- 14
    -self.w - self.w/2, self.w, -- 15
    0, self.w, -- 16
}

The first one is the main body, the second is the top wing and the third is the bottom wing. All vertices are defined in an anti-clockwise manner, and the first point of a line is always the x component, while the second is the y component. Here I'll show a drawing that maps each vertex to the numbers outlined to the side of each point pair above:

And as you can see, the first point is way to the right and vertically aligned with the center, so its self.w, 0. The next is a bit to the left and above the first, so its self.w/2, -self.w/2, and so on.

Finally, another thing we can do after adding this is making the trails match the ship. For this one, as you can see in the gif I linked before, it has two trails coming out of the back instead of just one:

function Player:new(...)
    ...

    self.timer:every(0.01, function()
        if self.ship == 'Fighter' then
            self.area:addGameObject('TrailParticle', 
            self.x - 0.9*self.w*math.cos(self.r) + 0.2*self.w*math.cos(self.r - math.pi/2), 
            self.y - 0.9*self.w*math.sin(self.r) + 0.2*self.w*math.sin(self.r - math.pi/2), 
            {parent = self, r = random(2, 4), d = random(0.15, 0.25), color = self.trail_color}) 
            self.area:addGameObject('TrailParticle', 
            self.x - 0.9*self.w*math.cos(self.r) + 0.2*self.w*math.cos(self.r + math.pi/2), 
            self.y - 0.9*self.w*math.sin(self.r) + 0.2*self.w*math.sin(self.r + math.pi/2), 
            {parent = self, r = random(2, 4), d = random(0.15, 0.25), color = self.trail_color}) 
        end
    end)
end

And here we use the technique of going from point to point based on an angle to get to our target. The target points we want are behind the player (0.9*self.w behind), but each offset by a small amount (0.2*self.w) along the opposite axis to the player's direction.

And all this looks like this:


Ship Visuals Exercises

As a small note, the (CONTENT) tag will mark exercises that are the content of the game itself. Exercises marked like this will have no answers and you're supposed to do them 100% yourself! From now on, more and more of the exercises will be like this, since we're starting to get into the game itself and a huge part of it is just manually adding content to it.

92. (CONTENT) Add 7 more ship types. To add a new ship type, simply add another conditional elseif self.ship == 'ShipName' then to both the polygon definition and the trail definition. Here's what the ships I made look like (but obviously feel free to be 100% creative and do your own designs):



Behavior Trees #1

2014-08-16 21:05

This post explains behavior trees as I understood them. There are very few behavior tree related tutorials out there that have both code examples as well as concise explanations. The best ones I could find are here and here, and while they're really good, when going through them I surely had many doubts that the lack of other decent tutorials didn't help answering. With that in mind, this article provides another source that people can use as a reference as they learn about behavior trees. Part 1 (this one) will deal with explaining what behavior trees are and how you can implement them, while Part 2 will use them to build a simple yet complete enemy behavior. Most of the code used here uses this as a reference with some slight changes. And I learned about behavior trees initially from this article! Give it a quick read first, since I think it does a better job at explaining the basics than I do here.


Introduction

Behavior Trees are used to describe the behavior of entities in a game in a hopefully intuitive manner. They do this by using the following definitions:

  • A tree is composed of nodes (like the normal tree data structure would);
  • A node can run some operation and then return the status of that operation to its parent;
  • Three statuseses are usually used: success, failure and running.

With these three simple rules a lot can be done. For instance:

That tree is added to an entity like this:

...
function Entity:new(...)
    ...
    self.behavior_tree = Root(self, moveToPoint())
end
  
function Entity:update(dt)
    self.behavior_tree:update(dt)
end
...

And so the moveToPoint behavior, which finds a point for the entity to move to and then moves it there, will be run every frame. moveToPoint returns success whenever the entity gets close enough to the target point, running when it's moving there and failure when it's stuck around some area for more than a few seconds. In this way, this single behavior can take place along multiple frames (whenever it's returning running) and also deals with failure nicely.

This ability is what makes behavior trees an attractive choice for handling AI. Failure and actions over multiple frames are kind of what makes AI coding somewhat of a pain and usually degenerates into a bunch of ifs or huge switches or some other not that clean structure. Offloading all that complexity to behavior trees (which are basically doing all those ifs in their structure) and making it so that actions are coded in a safe environment with an extremely well defined interface (success, failure, running) makes everything easier. On top of that it's possible that the composability of actions and subtrees makes it easier to reuse code, although I haven't used BTs for too long to see if that's true or not for myself, but it totally seems to be so far.


Behavior

Before anything else we're going to do the base node/behavior class. It's pretty simple and looks like this:

Behavior = mg.Class:extend()
  
function Behavior:new()
    self.status = 'invalid'
end
  
function Behavior:update(dt, context)
    if self.status ~= 'running' then self:start(context) end
    self.status = self:run(dt, context)
    if self.status ~= 'running' then self:finish(self.status, context) end
    return self.status 
end

The constructor initializes this node's status to some invalid value and the update function runs the start, run and finish functions and returns its status. start and finish are run only if the current status isn't running, which makes sense, since anything else implies the node hasn't been started or has finished already. You'll note that there's a context variable being passed around. This will be the shared space for which all nodes in the tree can write to and read from. It's a way nodes have of communicating with each other and is just a table created at the top most node, the Root.


Root

The root node is the top most node of all behavior trees and is simply there because we need to store tree-wide data somewhere, such as the context.

Root = Behavior:extend()
  
function Root:new(object, behavior)
    Root.super.new(self)
    self.behavior = behavior
    self.object = object
    self.context = {object = self.object}
end
  
function Root:update(dt)
    return Root.super.update(self, dt)
end
  
function Root:run(dt)
    return self.behavior:update(dt, self.context)
end
  
function Root:start()
    self.context = {object = self.object}
end
  
function Root:finish(status)
    
end

A root node inherits from the previous Behavior class. For its constructor, since each behavior tree is gonna be attached to an entity, it takes that entity as well as a single behavior as arguments. In the update function it calls Behavior's update function, which calls Root's start, run and finish functions depending on its status and the returned value from run. Root's run function, where the heart of its behavior lies, simply calls the attached behavior's update function, which translates to it going down and starting to explore the tree. That behavior/node, unless it's a leaf node like moveToPoint was, will also somehow call its behavior's update functions and so on. This means that each frame the whole tree is being explored, starting from the root node and going down according to the logic of each subsequent one. Finally, root's start function starts the context and adds the object this tree is attached to to the object field. Nodes down the tree will then be able to access the object by saying context.object.


Action

An action is a leaf node that will perform some... action. This action can be either a conditional question that changes the context with some information, for instance, or just a normal action like moveToPoint.

Action = Behavior:extend()
  
function Action:new(name)
    Action.super.new(self)
    self.name = name or 'Action'
end
  
function Action:update(dt, context)
    return Action.super.update(self, dt, context)
end

To be honest this class probably didn't even need to exist, but like the Root node it inherits from Behavior. The only meaningful change is that it takes in a name so that it can be identified (when debugging for instance). To create a new Action we need to inherit from it like this:

newAction = Action:extend()
  
function newAction:new()
    newAction.super.new(self, 'newAction')
end
  
function newAction:update(dt, context)
    return newAction.super.update(self, dt, context)
end
  
function newAction:run(dt, context)
    -- newAction's behavior goes here
    -- should return 'success', 'running' or 'failure'
end
  
function newAction:start(context)
    -- any setup goes here
end
  
function newAction:finish(status, context)
    -- any cleanup goes here
end

The constructor and the update functions call Action's constructor and update functions, similarly to Root, but then the run, start and finish functions are where the functionality of the new action will go.


Sequence

A sequence is a composite node (it can have multiple children) that performs each of its children in sequence. If one child fails then the whole sequence fails, but if one child succeeds then the sequence moves on to the next. If all children succeed then the sequence succeeds. If a child is still running then the sequence returns running as well and on the next frame it will come back to that running node.

Sequence = Behavior:extend()
  
function Sequence:new(name, behaviors)
    Sequence.super.new(self)
    self.name = name
    self.behaviors = behaviors
    self.current_behavior = 1
end
  
function Sequence:update(dt, context)
    return Sequence.super.update(self, dt, context) 
end
  
function Sequence:run(dt, context)
    while true do
        local status = self.behaviors[self.current_behavior]:update(dt, context)
        if status ~= 'success' then return status end
        self.current_behavior = self.current_behavior + 1
        if self.current_behavior == ##self.behaviors + 1 then return 'success' end
    end
end
  
function Sequence:start(context)
    self.current_behavior = 1
end
  
function Sequence:finish(status, context)
  
end

A sequence takes in a name and multiple behaviors. The update function is as usual and the start function sets the current behavior to be the first. The run function then does the work of going through all behaviors if they return success, or returning running or failure if any behavior returns that. Note that if a node returns running, on the next frame it will return to that node because current_behavior will be set there. If on every frame current_behavior was set to one (1), for instance, the sequence would only ever succeed if all nodes succeeded on this frame, but it would also not fail when running was returned, it would just lose the ability to do the sequence over multiple frames.


Selector

A selector is another composite node that performs each of its children in sequence, however, unlike a Sequence, it succeeds if any one of its children succeeds, and fails only if all children fail. Whenever a single node fails then it moves on to the next and tries it, if that fails, it goes on to the next until one succeeds or returns running. Similarly to a Sequence, running will also make the Selector come back to the running node on the next frame.

Selector = Behavior:extend()
  
function Selector:new(name, behaviors)
    Selector.super.new(self)
    self.name = name
    self.behaviors = behaviors 
    self.current_behavior = 1
end
  
function Selector:update(dt, context)
    return Selector.super.update(self, dt, context)
end
  
function Selector:run(dt, context)
    while true do
        local status = self.behaviors[self.current_behavior]:update(dt, context)
        if status ~= 'failure' then return status end
        self.current_behavior = self.current_behavior + 1
        if self.current_behavior == ##self.behaviors + 1 then return 'failure' end
    end
end
  
function Selector:start(context)
    self.current_behavior = 1
end
  
function Selector:finish(status, context)
  
end

The logic is pretty much the same as a Sequence, just reversed.


Decorator

A decorator is a node that can only have one child and that performs some logic to change the result returned from that single child.

Decorator = Behavior:extend()
  
function Decorator:new(name, behavior)
    Decorator.super.new(self)
    self.name = name
    self.behavior = behavior
end
  
function Decorator:update(dt, context)
    return Decorator.super.update(self, dt, context)
end

The class for it, like an Action, is just a stub that is supposed to be inherited from when you create an actual decorator. It takes in a name and the to be attached behavior. A common decorator is the Inverter, which takes the value from the child and inverts it:

Inverter = Decorator:extend()
  
function Inverter:new(behavior)
    Inverter.super.new(self, 'Inverter', behavior)
end
  
function Inverter:update(dt, context)
    return Inverter.super.update(self, dt, context)
end
  
function Inverter:run(dt, context)
    local status = self.behavior:update(dt, context)
    if status == 'running' then return 'running'
    elseif status == 'success' then return 'failure'
    elseif status == 'failure' then return 'success' end
end
  
function Inverter:start(context)
  
end
  
function Inverter:finish(status, context)
  
end

The constructor and update functions are as always, and the run function, which has the meat of the inversion behavior, runs the attached behavior's update function, takes its returned value and then returns failure if it succeeded, success if it failed, and running if its still running. Another useful decorator is RepeatUntilFail, which has a run function that looks like this:

RepeatUntilFail = Decorator:extend()
  
function RepeatUntilFail:new(behavior)
    RepeatUntilFail.super.new(self, 'RepeatUntilFail', behavior)
end
  
function RepeatUntilFail:update(dt, context)
    return RepeatUntilFail.super.update(self, dt, context)
end
  
function RepeatUntilFail:run(dt, context)
    local status = self.behavior:update(dt, context)
    if status ~= 'failure' then return 'running'
    else return 'success' end
end
  
function RepeatUntilFail:start(context)
  
end
  
function RepeatUntilFail:finish(status, context)
  
end

Another one is a timer, that runs the child node after a certain amount of time has passed:

Timer = Decorator:extend()
  
function Timer:new(name, duration, behavior)
    Timer.super.new(self, name, behavior)
    self.duration = duration
    self.ready_to_run = false
end
  
function Timer:update(dt, context)
    return Timer.super.update(self, dt, context)
end
  
function Timer:run(dt, context)
    if self.ready_to_run then
        local status = self.behavior:update(dt, context)
        return status
    else return 'running' end
end
  
function Timer:start(context)
    self.ready_to_run = false
    mg.timer:after(self.duration, function() self.ready_to_run = true end)
end
  
function Timer:finish(status, context)
    self.ready_to_run = false
end

Here mg.timer is the timer module, and :after simply makes it so that the function passed gets called n seconds after this point. So this decorator will be ready_to_run after duration seconds have passed from when it first started, and when that happens it will run and return the returned value from its child.


Example

Now let's build a real tree and attach it to a real entity. The idea is to make the entity move around randomly by using two actions: findWanderPoint, which finds a point for the entity to move to and adds it to the context, and moveToPoint, which moves the entity to that point. findWanderPoint needs to receive a point and a radius where it can look around for a new point and so it would look like this:

findWanderPoint = Action:extend()
  
function findWanderPoint:new(x, y, radius)
    findWanderPoint.super.new(self, 'findWanderPoint')
  
    self.x = x
    self.y = y
    self.radius = radius
end
  
function findWanderPoint:update(dt, context)
    return findWanderPoint.super.update(self, dt, context)
end
  
function findWanderPoint:run(dt, context)
    -- Find a point in a circle centered in x, y with radius radius
    local angle = mg.utils.math.random(0, 2*math.pi)
    local distance = mg.utils.math.random(0, self.radius)
    -- Add that point to the context so that it can be accessed 
    -- later by the moveToPoint action
    context.move_to_point_target = mg.Vector(self.x + distance*math.cos(angle), 
                                             self.y + distance*math.sin(angle))
    return 'success'
end
  
function findWanderPoint:start(context)
  
end
  
function findWanderPoint:finish(status, context)
  
end

And moveToPoint like this:

moveToPoint = Action:extend()
  
function moveToPoint:new()
    moveToPoint.super.new(self, 'moveToPoint')
  
    self.last_position = mg.Vector(0, 0)
    self.stuck = false
end
  
function moveToPoint:update(dt, context)
    return moveToPoint.super.update(self, dt, context)
end
  
function moveToPoint:run(dt, context)
    -- Fail if stuck
    if self.stuck then return 'failure' end
    -- Check distance to target point, if roughly there then succeed,
    -- otherwise return running, which means the entity is moving there.
    -- In the off chance move_to_point_target isn't set then fail.
    if context.move_to_point_target then
        local distance = context.move_to_point_target:dist(
                         mg.Vector(context.object.body:getPosition()))
        if distance < 10 then return 'success'
        elseif distance >= 10 then return 'running' end
    else return 'failure' end
end
  
function moveToPoint:start(context)
    -- Steering behavior takes care of moving the object towards the target point
    context.object:arriveOn(context.move_to_point_target)
    self.last_position = mg.Vector(context.object.body:getPosition())
    -- Every 4 seconds check to see if the distance to the position of 4 seconds ago
    -- is still roughly the same. If it is then set self.stuck to true.
    context.object.timer:every(4, function()
        local current_position = mg.Vector(context.object.body:getPosition())
        local distance = current_position:dist(self.last_position)
        if distance < 5 then self.stuck = true end
        self.last_position = current_position
    end)
end
  
function moveToPoint:finish(status, context)
    -- Turn steering behavior off and set the node back to it's initial state,
    -- as well as removing variables that aren't going to be 
    -- used anymore from the context
    context.object:arriveOff()
    context.move_to_point_target = nil
    self.stuck = false
end

And the entity getting this behavior would create the tree like this:

    ...
    -- in said entity's constructor
    self.behavior_tree = Root(self,
        Sequence('wander', {
            findWanderPoint(self.x, self.y, 100),
            moveToPoint(),
        })
    )
    ...

To bind both actions we're using a sequence! If for some reason findWanderPoint fails (it can't do since it always returns 'success', but supposing it could), then moveToPoint won't do anything. Another thing to note is that both actions are now tied together by their use of context.move_to_point_target. This means that any future action that preceeds moveToPoint needs to set context.move_to_point_target to some 2D vector, otherwise it won't work. These types of dependencies are necessary, even if they may not be ideal. A solution for this is described here by using stacks, but IMO that seems like adding too much low level logic to the tree. But if you find it better then who am I to judge I've only been using this technique for like a week so yea

BYTEPATH #12 - More Passives

Blast

We'll start by implementing all attacks left. The first one is the Blast attack and it looks like this:

Multiples projectiles are fired like a shotgun with varying velocities and then they quickly disappear. All the colors are the ones negative_colors table and each projectile deals less damage than normal. This is what the attack table looks like:

attacks['Blast'] = {cooldown = 0.64, ammo = 6, abbreviation = 'W', color = default_color}

And this is what the projectile creation process looks like:

function Player:shoot()
    ...
  	
    elseif self.attack == 'Blast' then
        self.ammo = self.ammo - attacks[self.attack].ammo
        for i = 1, 12 do
            local random_angle = random(-math.pi/6, math.pi/6)
            self.area:addGameObject('Projectile', 
      	        self.x + 1.5*d*math.cos(self.r + random_angle), 
      	        self.y + 1.5*d*math.sin(self.r + random_angle), 
            table.merge({r = self.r + random_angle, attack = self.attack, 
          	v = random(500, 600)}, mods))
        end
        camera:shake(4, 60, 0.4)
    end

    ...
end

So here we just create 12 projectiles with a random angle between -30 and +30 degrees from the direction the player is moving towards. We also randomize the velocity between 500 and 600 (its normal value is 200), meaning the projectile is about 3 times as fast as normal.

Doing this alone though won't get the behavior we want, since we also want for the projectiles to disappear rather quickly. The way to do this is like this:

function Projectile:new(...)
    ...
  	
    if self.attack == 'Blast' then
        self.damage = 75
        self.color = table.random(negative_colors)
        self.timer:tween(random(0.4, 0.6), self, {v = 0}, 'linear', function() self:die() end)
    end
  	
    ...
end

Three things are happening here. The first is that we're setting the damage to a value lower than 100. This means that to kill the normal enemy which has 100 HP we'll need two projectiles instead of one. This makes sense given that this attack shoots 12 of them at once. The second thing we're doing is setting the color of this projectile to a random one from the negative_colors table. This is also something we wanted to do and this is the proper place to do it. Finally, we're also saying that after a random amount between 0.4 and 0.6 seconds this projectile will die, which will give us the effect we wanted. We're also slowing the projectile's velocity down to 0 instead of just killing it, since that looks a little better.

And all this gets us all the behavior we wanted and on the surface this looks done. However, now that we've added tons of passives in the previous article we need to be careful and make sure that everything we add afterwards plays well with those passives. For instance, the last thing we did in the previous article was add the shield projectile effect. The problem with the Blast attack is that it doesn't play well with the shield projectile effect at all, since Blast projectiles will die after 0.4-0.6 seconds and this makes for a very poor shield projectile.

One of the ways we can fix this is by singling out the offending passive (in this case the shield one) and applying different logic for each situation. In the situation where shield is true for a projectile, then the projectile will last 6 seconds no matter what. And in all other situations then the duration set by whatever attack will take hold. This would look like this:

function Projectile:new(...)
    ...
  	
    if self.attack == 'Blast' then
        self.damage = 75
        self.color = table.random(negative_colors)
    	if not self.shield then
            self.timer:tween(random(0.4, 0.6), self, {v = 0}, 'linear', function() 
                self:die() 
            end)
      	end
    end
  
    if self.shield then
        ...
        self.timer:after(6, function() self:die() end)
    end
  	
    ...
end

This seems like a hacky solution, and you can easily imagine that it would get more complicated if more and more passives are added and we have to add more and more conditions delineating what happens if this or if not that. But from my experience doing this is by far the easiest and less error prone way of doing things. The alternative is trying to solve this problem in some kind of general way and generally those have unintended consequences. Maybe there's a better general solution for this problem specifically that I can't think of, but given that it hasn't occurred to me yet then the next best thing is to do the simplest thing, which is just a bunch of conditionals saying what should or should not happen. In any case, now for every attack we add that changes a projectile's duration, we'll have to preface it with if not self.shield.


172. (CONTENT) Implement the projectile_duration_multiplier passive. Remember to use it on any duration-based behavior on the Projectile class.


Spin

The next attack we'll implement is the Spin one. It looks like this:

These projectiles just have their angle constantly changed by a fixed amount instead of going straight. The way we can achieve this is by adding a rv variable, which represents the angle's rate of change, and then every frame add this amount to r:

function Projectile:new(...)
    ...
  	
    self.rv = table.random({random(-2*math.pi, -math.pi), random(math.pi, 2*math.pi)})
end

function Projectile:update(dt)
    ...
  	
    if self.attack == 'Spin' then
    	self.r = self.r + self.rv*dt
    end
  	
    ...
end

The reason we pick between -2*math.pi and -math.pi OR between math.pi and 2*math.pi is because we don't want any of the values that are lower than math.pi or higher than 2*math.pi in absolute terms. Low absolute values means that the circle the projectile makes is bigger, while higher absolute values means that the circle is smaller. We want a nice limit on the circle's size both ways so this makes sense. It should also be clear that the difference between negative or positive values is in the direction the circle spin towards.

We can also add a duration to Spin projectiles since we don't want them to stay around forever:

function Projectile:new(...)
    ...
  	
    if self.attack == 'Spin' then
        self.timer:after(random(2.4, 3.2), function() self:die() end)
    end
end

This is what the shoot function would look like:

function Player:shoot()
    ...
  
    elseif self.attack == 'Spin' then
        self.ammo = self.ammo - attacks[self.attack].ammo
        self.area:addGameObject('Projectile', 
    	self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), 
    	table.merge({r = self.r, attack = self.attack}, mods))
    end  	
end

And this is what the attack table looks like:

attacks['Spin'] = {cooldown = 0.32, ammo = 2, abbreviation = 'Sp', color = hp_color}

And all this should get us the behavior we want. However, there's an additional thing to do which is the trail on the projectile. Unlike the Homing projectile which uses a trail like the one we used for the Player ships, this one follows the shape and color of the projectile but also slowly becomes invisible until it disappears completely. We can implement this in the same way we did for the other trail object, but taking into account those differences:

ProjectileTrail = GameObject:extend()

function ProjectileTrail:new(area, x, y, opts)
    ProjectileTrail.super.new(self, area, x, y, opts)

    self.alpha = 128
    self.timer:tween(random(0.1, 0.3), self, {alpha = 0}, 'in-out-cubic', function() 
    	self.dead = true 
    end)
end

function ProjectileTrail:update(dt)
    ProjectileTrail.super.update(self, dt)
end

function ProjectileTrail:draw()
    pushRotate(self.x, self.y, self.r) 
    local r, g, b = unpack(self.color)
    love.graphics.setColor(r, g, b, self.alpha)
    love.graphics.setLineWidth(2)
    love.graphics.line(self.x - 2*self.s, self.y, self.x + 2*self.s, self.y)
    love.graphics.setLineWidth(1)
    love.graphics.setColor(255, 255, 255, 255)
    love.graphics.pop()
end

function ProjectileTrail:destroy()
    ProjectileTrail.super.destroy(self)
end

And so this looks pretty standard, the only notable thing being that we have an alpha variable which we tween to 0 so that the projectile slowly disappears over a random duration of between 0.1 and 0.3 seconds, and then we draw the trail just like we would draw a projectile. Importantly, we use the r, s and color variables from the parent projectile, which means that when creating it we need to pass all that down:

function Projectile:new(...)
    ...
  
    if self.attack == 'Spin' then
        self.rv = table.random({random(-2*math.pi, -math.pi), random(math.pi, 2*math.pi)})
        self.timer:after(random(2.4, 3.2), function() self:die() end)
        self.timer:every(0.05, function()
            self.area:addGameObject('ProjectileTrail', self.x, self.y, 
            {r = Vector(self.collider:getLinearVelocity()):angle(), 
            color = self.color, s = self.s})
        end)
    end

    ...
end

And this should get us the results we want.


173. (CONTENT) Implement the Flame attack. This is what the attack table looks like:

attacks['Flame'] = {cooldown = 0.048, ammo = 0.4, abbreviation = 'F', color = skill_point_color}

And this is what the attack looks like:

The projectiles should remain alive for a random duration between 0.6 and 1 second, and like the Blast projectiles, their velocity should be tweened to 0 over that duration. The projectiles also make use of the ProjectileTrail object in the way that the Spin projectiles do. Flame projectiles also deal decreased damage at 50 each.


Bounce

Bounce projectiles will bounce off of walls instead of being destroyed by them. By default a Bounce projectile can bounce 4 times before being destroyed whenever it hits a wall again. The way we can define this is by setting it through the opts table in the shoot function:

function Player:shoot()
    ...
  	
    elseif self.attack == 'Bounce' then
        self.ammo = self.ammo - attacks[self.attack].ammo
        self.area:addGameObject('Projectile', 
    	self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), 
    	table.merge({r = self.r, attack = self.attack, bounce = 4}, mods))
    end
end

And so the bounce variable will contain the number of bounces left for this projectile. We can use this so that whenever we hit a wall we decrease this by 1:

function Projectile:update(dt)
    ...
  
    -- Collision
    if self.bounce and self.bounce > 0 then
        if self.x < 0 then
            self.r = math.pi - self.r
            self.bounce = self.bounce - 1
        end
        if self.y < 0 then
            self.r = 2*math.pi - self.r
            self.bounce = self.bounce - 1
        end
        if self.x > gw then
            self.r = math.pi - self.r
            self.bounce = self.bounce - 1
        end
        if self.y > gh then
            self.r = 2*math.pi - self.r
            self.bounce = self.bounce - 1
        end
    else
        if self.x < 0 then self:die() end
        if self.y < 0 then self:die() end
        if self.x > gw then self:die() end
        if self.y > gh then self:die() end
    end
  
    ...
end

Here on top of decreasing the number of bounces left, we also change the projectile's direction according to which wall it hit. Perhaps there's a general and better way to do this, but I could only think of this solution, which involves taking into account each wall separately and then doing the math necessary to reflect/mirror the projectile's angle properly. Note that when bounce is 0, the first conditional will fail and then we'll go down to the normal path that we had before, which ends up killing the projectile.

It's also important to place all this collision code before we call setLinearVelocity, otherwise our bounces won't work at all since we'll turn the projectile with a one frame delay and then simply reversing its angle won't make it come back. To be safe we could also use setPosition to forcefully set the position of the projectile to the border of the screen on top of turning its angle, but I didn't find that to be necessary.

Moving on, the colors of a bounce projectile are random like the Spread one, except going through the default_colors table. This means we need to take care of it separately in the Projectile:draw function:

function Projectile:draw()
    ...
    if self.attack == 'Bounce' then love.graphics.setColor(table.random(default_colors)) end
    ...
end

And the attack table looks like this:

attacks['Bounce'] = {cooldown = 0.32, ammo = 4, abbreviation = 'Bn', color = default_color}

And all that should look like this:


174. (CONTENT) Implement the 2Split attack. This is what it looks like:

It looks exactly like the Homing projectile, except that it uses ammo_color instead.

Whenever it hits an enemy the projectile will split into two (two new projectiles are created) at +-45 degrees from the direction the projectile was moving towards. If the projectile hits a wall then 2 projectiles will be created either against the reflected angle of the wall (so if it hits the up wall 2 projectiles will be created pointing to math.pi/4 and 3*math.pi/4) or against the reflected angle of the projectile, this is entirely your choice. This is what the attacks table looks like:

attacks['2Split'] = {cooldown = 0.32, ammo = 3, abbreviation = '2S', color = ammo_color}

175. (CONTENT) Implement the 4Split attack. This is what it looks like:

It behaves exactly like the 2Split attack, except that it creates 4 projectiles instead of 2. The projectiles point at all 45 degrees angles from the center, meaning angles math.pi/4, 3*math.pi/4, -math.pi/4 and -3*math.pi/4. This is what the attack table looks like:

attacks['4Split'] = {cooldown = 0.4, ammo = 4, abbreviation = '4S', color = boost_color}

Lightning

This is what the Lightning attack looks like:

Whenever the player reaches a certain distance from an enemy, a lightning bolt will be created, dealing damage to the enemy. Most of the work here lies in creating the lightning bolt, so we'll focus on that first. We'll do it by creating an object called LightningLine, which will be the visual representation of our lightning bolt:

LightningLine = GameObject:extend()

function LightningLine:new(area, x, y, opts)
    LightningLine.super.new(self, area, x, y, opts)
  	
    ...
    self:generate()
end

function LightningLine:update(dt)
    LightningLine.super.update(self, dt)
end

-- Generates lines and populates the self.lines table with them
function LightningLine:generate()
    
end

function LightningLine:draw()

end

function LightningLine:destroy()
    LightningLine.super.destroy(self)
end

I'll focus on the draw function and leave the entire generation of the lightning lines for you! This tutorial explains the generation method really really well and it would be redundant for me to repeat all that here. I'll assume that you have all the lines that compose the lightning bolt in a table called self.lines, and that each line is a table which contains the keys x1, y1, x2, y2. With that in mind we can draw the lightning bolt in a basic way like this:

function LightningLine:draw()
    for i, line in ipairs(self.lines) do 
        love.graphics.line(line.x1, line.y1, line.x2, line.y2) 
    end
end

However, this looks too simple. So what we'll do is draw all those lines first with boost_color and with line width set to 2.5, and then on top of that we'll draw the same lines again but with default_color and line width set to 1.5. This should make the lightning bolt a bit thicker and also look somewhat more like lightning.

function LightningLine:draw()
    for i, line in ipairs(self.lines) do 
        local r, g, b = unpack(boost_color)
        love.graphics.setColor(r, g, b, self.alpha)
        love.graphics.setLineWidth(2.5)
        love.graphics.line(line.x1, line.y1, line.x2, line.y2) 

        local r, g, b = unpack(default_color)
        love.graphics.setColor(r, g, b, self.alpha)
        love.graphics.setLineWidth(1.5)
        love.graphics.line(line.x1, line.y1, line.x2, line.y2) 
    end
    love.graphics.setLineWidth(1)
    love.graphics.setColor(255, 255, 255, 255)
end

I also use an alpha attribute here that starts at 255 and is tweened down to 0 over the duration of the line, which is about 0.15 seconds.

Now for when to actually create this LightningLine object. The way we want this attack to work is that whenever the player gets close enough to an enemy within his immediate line of sight, the attack will be triggered and we will damage that enemy. So first let's gets all enemies close to the player. We can do this in the same way we did for the homing projectile, which had to pick a target within a certain radius. The radius we want, however, can't be centered on the player because we don't want the player to be able to hit enemies behind him, so we'll offset the center of this circle a little forward in the direction the player is moving towards and then go from there.

function Player:shoot()
    ...
  
    elseif self.attack == 'Lightning' then
        local x1, y1 = self.x + d*math.cos(self.r), self.y + d*math.sin(self.r)
        local cx, cy = x1 + 24*math.cos(self.r), y1 + 24*math.sin(self.r)
        ...
end

Here we're defining x1, y1, which is the position we generally shoot projectiles from (right in front of the tip of the ship), and then we're also defining cx, cy, which is the center of the radius we'll use to find a nearby enemy. We offset this circle by 24 units, which is a big enough number to prevent us from picking enemies that are behind the player.

The next thing we can do is just copypaste the code we used in the Projectile object when we wanted homing projectiles to find their target, but change it to fit our needs here by changing the circle position for our own cx, cy circle center:

function Player:shoot()
    ...
  	
    elseif self.attack == 'Lightning' then
        ...
  
        -- Find closest enemy
        local nearby_enemies = self.area:getAllGameObjectsThat(function(e)
            for _, enemy in ipairs(enemies) do
                if e:is(_G[enemy]) and (distance(e.x, e.y, cx, cy) < 64) then
                    return true
                end
            end
        end)
  	...
end

After this we'll have a list of enemies within a 64 units radius of a circle 24 units in front of the player. What we can do here is either pick an enemy at random or find the closest one. I'll go with the latter and so to do that we need to sort the table based on each enemy's distance to the circle:

function Player:shoot()
    ...
  	
    elseif self.attack == 'Lightning' then
        ...
  
        table.sort(nearby_enemies, function(a, b) 
      	    return distance(a.x, a.y, cx, cy) < distance(b.x, b.y, cx, cy) 
    	end)
        local closest_enemy = nearby_enemies[1]
  	...
end

table.sort can be used here to achieve that goal. Then it's a matter of taking the first element of the sorted table and attacking it:

function Player:shoot()
    ...
  	
    elseif self.attack == 'Lightning' then
        ...
  
        -- Attack closest enemy
        if closest_enemy then
            self.ammo = self.ammo - attacks[self.attack].ammo
            closest_enemy:hit()
            local x2, y2 = closest_enemy.x, closest_enemy.y
            self.area:addGameObject('LightningLine', 0, 0, {x1 = x1, y1 = y1, x2 = x2, y2 = y2})
            for i = 1, love.math.random(4, 8) do 
      	        self.area:addGameObject('ExplodeParticle', x1, y1, 
                {color = table.random({default_color, boost_color})}) 
    	    end
            for i = 1, love.math.random(4, 8) do 
      	        self.area:addGameObject('ExplodeParticle', x2, y2, 
                {color = table.random({default_color, boost_color})}) 
    	    end
        end
    end
end

First we make sure that closest_enemy isn't nil, because if it is then we shouldn't do anything, and most of the time it will be nil since no enemies will be around. If it isn't, then we remove ammo as we did for all other attacks, and call the hit function on that enemy object so that it takes damage. After that we spawn the LightningLine object with the x1, y1, x2, y2 variables representing the position right in front of the ship from where the bolt will come from and the center of the enemy. Finally, we spawn a bunch of ExplodeParticles to add something to the attack.

One last thing we need to set before this can all work is the attacks table:

attacks['Lightning'] = {cooldown = 0.2, ammo = 8, abbreviation = 'Li', color = default_color}

And all that should look like this:


176. (CONTENT) Implement the Explode attack. This is what it looks like:

An explosion is created and it destroys all enemies in a certain radius around it. The projectile itself looks like the homing projectile except with hp_color and a bit bigger. The attack table looks like this:

attacks['Explode'] = {cooldown = 0.6, ammo = 4, abbreviation = 'E', color = hp_color}

177. (CONTENT) Implement the Laser attack. This is what it looks like:

A huge line is created and it destroys all enemies that cross it. This can be programmed literally as a line or as a rotated rectangle for collision detection purposes. If you choose to go with a line then it's better to use 3 or 5 lines instead that are a bit separated from each other, otherwise the player can sometimes miss enemies in a way that doesn't feel fair.

The effect of the attack itself is different from everything else but shouldn't be too much trouble. One huge white line in the middle that tweens its width down over time, and two red lines to the sides that start closer to the white lines but then spread out and disappear as the effect ends. The shooting effect is a bigger version of the original ShootEffect object and its also colored red. The attack table looks like this:

attacks['Laser'] = {cooldown = 0.8, ammo = 6, abbreviation = 'La', color = hp_color}

178. (CONTENT) Implement the additional_lightning_bolt passive. If this is set to true then the player will be able to attack with two lightning bolts at once. Programming wise this means that instead of looking for the only the closest enemy, we will look for two closest enemies and attack both if they exist. You may also want to separate each attack by a small duration like 0.5 seconds between each, since this makes it feel better.

179. (CONTENT) Implement the increased_lightning_angle passive. This passive increases the angle with which the lightning attack can be triggered, meaning that it will also hit enemies to the sides and sometimes behind the player. In programming terms this means that if increased_lightning_angle is true, then we won't offset the lightning circle by 24 units like we did and we will use the center of the player as the center position for our calculations instead.

180. (CONTENT) Implement the area_multiplier passive. This is a passive that increases the area of all area based attacks and effects. The most recent examples would be the lightning circle of the Lightning attack as well as the explosion area of the Explode attack. But it would also apply to explosions in general and anything that is area based (where a circle is used to get information or apply effects).

181. (CONTENT) Implement the laser_width_multiplier passive. This is a passive that increases or decreases the width of the Laser attack.

182. (CONTENT) Implement the additional_bounce_projectiles passive. This is a passive that increases the amount of bounces a Bounce projectile has. By default, Bounce attack projectiles can bounce 4 times. If additional_bounce_projectiles is 4, then Bounce attack projectiles should be able to bounce a total of 8 times.

183. (CONTENT) Implement the fixed_spin_attack_direction passive. This is a boolean passive that makes it so that all Spin attack projectiles spin in a fixed direction, meaning, all of them will either spin only left or only right.

184. (CONTENT) Implement the split_projectiles_split_chance passive. This is a projectile that adds a chance for projectiles that were split from a 2Split or 4Split attack to also be able to split. For instance, if this chance ever became 100, then split projectiles would split recursively indefinitely (however we'll never allow this to happen on the tree).

185. (CONTENT) Implement the [attack]_spawn_chance_multiplier passives, where [attack] is the name of each attack. These passives will increase the chance that a particular attack will be spawned. Right now whenever we spawn an Attack resource, the attack is picked randomly. However, now we want them to be picked from a chanceList that initially has equal chances for all attacks, but will get changed by the [attack]_spawn_chance_multiplier passives.

186. (CONTENT) Implement the start_with_[attack] passives, where [attack] is the name of each attack. These passives will make it so that the player starts with that attack. So for instance, if start_with_bounce is true, then the player will start every round with the Bounce attack. If multiple start_with_[attack] passives are true, then one must be picked at random.


Additional Homing Projectiles

The additional_homing_projectiles passive will add additional projectiles to "Launch Homing Projectile"-type passives. In general the way we're launching homing projectiles looks like this:

function Player:onAmmoPickup()
    if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then
        local d = 1.2*self.w
        self.area:addGameObject('Projectile', 
      	self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), 
      	{r = self.r, attack = 'Homing'})
        self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'})
    end
end

additional_homing_projectiles is a number that will tell us how many extra projectiles should be used. So to make this work we can just do something like this:

function Player:onAmmoPickup()
    if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then
        local d = 1.2*self.w
    	for i = 1, 1+self.additional_homing_projectiles do
            self.area:addGameObject('Projectile', 
      	    self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), 
      	    {r = self.r, attack = 'Homing'})
      	end
        self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'})
    end
end

And then all we have left to do is apply this to every instance where a launch_homing_projectile passive of some kind appears.


187. (CONTENT) Implement the additional_barrage_projectiles passive.

188. (CONTENT) Implement the barrage_nova passive. This is a boolean that when set to true will make it so that barrage projectiles fire in a circle rather than in the general direction the player is looking towards. This is what it looks like:


Mine Projectile

A mine projectile is a projectile that stays around the location it was created at and eventually explodes. This is what it looks like:

As you can see, it just rotates like a Spin attack projectile but at a much faster rate. To implement this all we'll do is say that whenever the mine attribute is true for a projectile, it will behave like Spin projectiles do but with an increased spin velocity.

function Projectile:new(...)
    ...
    if self.mine then
        self.rv = table.random({random(-12*math.pi, -10*math.pi), 
        random(10*math.pi, 12*math.pi)})
        self.timer:after(random(8, 12), function()
            -- Explosion
        end)
    end
    ...
end

function Projectile:update(dt)
    ... 	
    -- Spin or Mine
    if self.attack == 'Spin' or self.mine then self.r = self.r + self.rv*dt end
    ...
end

Here instead of confining our spin velocities to between absolute math.pi and 2*math.pi, we do it for absolute 10*math.pi and 12*math.pi. This results in the projectile spinning much faster and covering a smaller area, which is perfect for the kind of behavior we want.

Additionally, after a random duration of between 8 and 12 seconds the projectile will explode. This explosion should be handled in the same way that explosions were handled for the Explode projectile. In my case I created an Explosion object, but there are many different ways of doing it and as long as it works it's OK. I'll leave that as an exercise since the Explode attack was also an exercise.


189. (CONTENT) Implement the drop_mines_chance passive, which adds a chance for the player to drop a mine projectile behind him every 0.5 seconds. Programming wise this works via a timer that will run every 0.5 seconds and on each of those runs we'll roll our drop_mines_chance:next() function.

190. (CONTENT) Implement the projectiles_explode_on_expiration passive, which makes it so that whenever projectiles die because their duration ended, they will also explode. This should only apply to when their duration ends. If a projectile comes into contact with an enemy or a wall it shouldn't explode as a result of this passive being set to true.

191. (CONTENT) Implement the self_explode_on_cycle_chance passive. This passive adds a chance for the player to create explosions around himself on each cycle. This is what it looks like:

The explosions used are the same ones as for the Explode attack. The number, placement and size of the explosions created will be left for you to decide based on what you feel is best.

192. (CONTENT) Implement the projectiles_explosions passive. If this is set to true then all explosions that happen from a projectile created by the player will create multiple projectiles instead in similar fashion to how the barrage_nova passive works. The number of projectiles created initially is 5 and this number is affected by the additional_barrage_projectiles passive.


Energy Shield

When the energy_shield passive is set to true, the player's HP will become energy shield instead (called ES from now). ES works differently than HP in the following ways:

  • The player will take double damage
  • The player's ES will recharge after a certain duration without taking damage
  • The player will have halved invulnerability time

We can implement all of this mostly in the hit function:

function Player:new(...)
    ...
    -- ES
    self.energy_shield_recharge_cooldown = 2
    self.energy_shield_recharge_amount = 1

    -- Booleans
    self.energy_shield = true
    ...
end

function Player:hit(damage)
    ...
  	
    if self.energy_shield then
        damage = damage*2
        self.timer:after('es_cooldown', self.energy_shield_recharge_cooldown, function()
            self.timer:every('es_amount', 0.25, function()
                self:addHP(self.energy_shield_recharge_amount)
            end)
        end)
    end
  
    ...
end

We're defining that our cooldown for when ES starts recharging after a hit is 2 seconds, and that the recharge rate is 4 ES per second (1 per 0.25 seconds in the every call). We're also placing this conditional at the top of the hit function and doubling the damage variable, which will be used further down to actually deal damage to the player.

The only thing left now is making sure that we halve the invulnerability time. We can either do this on the hit function or on the setStats function. I'll go with the latter since we haven't messed with that function in a while.

function Player:setStats()
    ...
  
    if self.energy_shield then
        self.invulnerability_time_multiplier = self.invulnerability_time_multiplier/2
    end
end

Since setStats is called at the end of the constructor and after the treeToPlayer function is called (meaning that it's called after we've loaded in all passives from the tree), we can be sure that energy_shield being true or not is representative of the passives the player picked up in the tree, and we can also be sure that we're only halving the invulnerability timer after all increases/decreases to this multiplier have been applied from the tree. This last assurance isn't really necessary for this passive, since the order here doesn't really matter, but for other passives it might and that would be a case where applying changes in setStats makes sense. Generally if a chance to a stat comes from a boolean and it's a change that is permanent throughout the game, then placing it in setStats makes the most sense.


193. (CONTENT) Change the HP UI so that whenever energy_shield is set to true is looks like this instead:

194. (CONTENT) Implement the energy_shield_recharge_amount_multiplier passive, which increases or decreases the amount of ES recharged per second.

195. (CONTENT) Implement the energy_shield_recharge_cooldown_multiplier passive, which increases or decreases the cooldown duration before ES starts recharging after a hit.


Added Chance to All 'On Kill' Events

If added_chance_to_all_on_kill_events is 5, for instance, then all 'On Kill' passives will have their chances increased by 5%. This means that if originally the player got passives that accumulated his launch_homing_projectile_on_kill_chance to 8, then instead of having 8% final probability he will have 13% instead. This is a pretty OP passive but implementation wise it's also interesting to look at.

We can implement this by changing the way the generateChances function generates its chanceLists. Since that function goes through all passives that end with _chance, it stands to reason that we can also parse out all passives that have _on_kill on them, which means that once we do that our only job is to add added_chance_to_all_on_kill_events to the appropriate place in the chanceList generation.

So first let separate normal passives from ones that have on_kill on them:

function Player:generateChances()
    self.chances = {}
    for k, v in pairs(self) do
        if k:find('_chance') and type(v) == 'number' then
            if k:find('_on_kill') and v > 0 then

            else
                self.chances[k] = chanceList({true, math.ceil(v)}, {false, 100-math.ceil(v)})
            end
      	end
    end
end

We use the same method as we did to find passives with _chance in them, except changing that for _on_kill. Additionally, we also need to make sure that this passive has an above 0% probability of generating its event. We don't want our new passive to add its chance to all 'On Kill' events even when the player invested no points whatsoever in that event, so we only do it for events where the player already has some chance invested in it.

Now what we can do is simply create the chanceList, but instead of using v by itself we'll use v+added_chance_to_all_on_kill_events:

function Player:generateChances()
    self.chances = {}
    for k, v in pairs(self) do
        if k:find('_chance') and type(v) == 'number' then
            if k:find('_on_kill') and v > 0 then
                self.chances[k] = chanceList(
                {true, math.ceil(v+self.added_chance_to_all_on_kill_events)}, 
                {false, 100-math.ceil(v+self.added_chance_to_all_on_kill_events)})
            else
                self.chances[k] = chanceList({true, math.ceil(v)}, {false, 100-math.ceil(v)})
            end
      	end
    end
end

Increases in Ammo Added as ASPD

This one is a conversion of part of one stat to another. In this case we're taking all increases to the Ammo resource and adding them as extra attack speed. The precise way we'll do it is following this formula:

local ammo_increases = self.max_ammo - 100
local ammo_to_aspd = 30
aspd_multiplier:increase((ammo_to_aspd/100)*ammo_increases)

This means that if we have, let's say, 130 max ammo, and our ammo_to_aspd conversion is 30%, then we'll end up increasing our attack speed by 0.3*30 = 9%. If we have 250 max ammo and the same conversion percentage, then we'll end up with 1.5*30 = 45%.

To get this working we can define the attribute first:

function Player:new(...)
    ...
  
    -- Conversions
    self.ammo_to_aspd = 0
end

And then we can apply the conversion to the aspd_multiplier variable. Because this variable is a Stat, we need to do it in the update function. If this variable was a normal variable we'd do it in the setStats function instead.

function Player:update(dt)
    ...
    -- Conversions
    if self.ammo_to_aspd > 0 then 
        self.aspd_multiplier:increase((self.ammo_to_aspd/100)*(self.max_ammo - 100)) 
    end

    self.aspd_multiplier:update(dt)
    ...
end

And this should work out how we expect it to.


Final Passives

There are only about 20 more passives left to implement and most of them are somewhat trivial and so will be left as exercises. They don't really have any close relation to most of the passives we went through so far, so even though they may be trivial you can think of them as challenges to see if you really have a grasp on the codebase and on what's actually going on.


196. (CONTENT) Implement the change_attack_periodically passive, which changes the player's attack every 10 seconds. The new attack is chosen randomly.

197. (CONTENT) Implement the gain_sp_on_death passive, which gives the player 20SP whenever he dies.

198. (CONTENT) Implement the convert_hp_to_sp_if_hp_full passive, which gives the player 3SP whenever he gathers an HP resource and his HP is already full.

199. (CONTENT) Implement the mvspd_to_aspd passive, which adds the increases in movement speed to attack speed. The increases should be added using the same formula used for ammo_to_aspd, meaning that if the player has 30% increases to MVSPD and mvspd_to_aspd is 30 (meaning 30% conversion), then his ASPD should be increased by 9%.

200. (CONTENT) Implement the mvspd_to_hp passive, which adds the decreases in movement speed to the player's HP. As an example, if the player has 30% decreases to MVSPD and mvspd_to_hp is 30 (meaning 30% conversion), then the added HP should be 21.

201. (CONTENT) Implement the mvspd_to_pspd passive, which adds the increases in movement speed to a projectile's speed. This one works in the same way as the mvspd_to_aspd one.

202. (CONTENT) Implement the no_boost passive, which makes the player have no boost (max_boost = 0).

203. (CONTENT) Implement the half_ammo passive, which makes the player have halved ammo.

204. (CONTENT) Implement the half_hp passive, which makes the player have halved HP.

205. (CONTENT) Implement the deals_damage_while_invulnerable passive, which makes the player deal damage on contact with enemies whenever he is invulnerable (when the invincible attribute is set to true when the player gets hit, for example).

206. (CONTENT) Implement the refill_ammo_if_hp_full passive, which refills the player's ammo completely if the player picks up an HP resource and his HP is already full.

207. (CONTENT) Implement the refill_boost_if_hp_full passive, which refills the player's boost completely if the player picks up an HP resource and his HP is already full.

208. (CONTENT) Implement the only_spawn_boost passive, which makes it so that the only resources that can be spawned are Boost ones.

209. (CONTENT) Implement the only_spawn_attack passive, which makes it so that no resources are spawned on their set cooldown, but only Attacks are. This means that attacks are spawned both on the resource cooldown as well as on their own attack cooldown (so every 16 seconds as well as every 30 seconds).

210. (CONTENT) Implement the no_ammo_drop passive, which makes it so that enemies never drop ammo.

211. (CONTENT) Implement the infinite_ammo passive, which makes it so that no attack the player uses consumes ammo.


And with that we've gone over all passives. In total we went over around 150 different passives, which will be extended to about 900 or so nodes in the skill tree, since many of those passives are just stat upgrades and stat upgrades can be spread over the tree of concentrated in one place.

But before we get to tree (which we'll go over in the next article), now that we have pretty much all this implemented we can build on top of it and implement all enemies and all player ships as well. You are free to deviate from the examples I'll give on each of those exercises and create your own enemies/ships.


Enemies

212. (CONTENT) Implement the BigRock enemy. This enemy behaves just like the Rock one, except it's bigger and splits into 4 Rock objects when killed. It has 300 HP by default.

213. (CONTENT) Implement the Waver enemy. This enemy behaves like a wavy projectile and occasionally shoots projectiles out of its front and back (like the Back attack). It has 70 HP by default.

214. (CONTENT) Implement the Seeker enemy. This enemy behaves like the Ammo object and moves slowly towards the player. At a fixed interval this enemy will also release mines that behave in the same way as mine projectiles. It has 200 HP by default.

215. (CONTENT) Implement the Orbitter enemy. This enemy behaves like the Rock or BigRock enemies, but has a field of projectiles surrounding him. Those projectiles behave just like shield projectiles we implemented in the last article. If the Orbitter dies before his projectiles, the leftover projectiles will start homing towards the player for a short duration. It has 450 HP by default.


Ships

We already went over visuals for all the ships in article 5 or 6 I think and if I remember correctly those were exercises as well. So I'll assume you have those already made and hidden somewhere and that they have names and all that. The next exercises will assume the ones I created, but since it's mostly usage of passives we implemented in the last and this article, you can create your own ships based on what you feel is best. These are just the ones I personally came up with.

216. (CONTENT) Implement the Crusader ship:

Its stats are as follows:

  • Boost = 80
  • Boost Effectiveness Multiplier = 2
  • Movement Speed Multiplier = 0.6
  • Turn Rate Multiplier = 0.5
  • Attack Speed Multiplier = 0.66
  • Projectile Speed Multiplier = 1.5
  • HP = 150
  • Size Multiplier = 1.5

217. (CONTENT) Implement the Rogue ship:

Its stats are as follows:

  • Boost = 120
  • Boost Recharge Multiplier = 1.5
  • Movement Speed Multiplier = 1.3
  • Ammo = 120
  • Attack Speed Multiplier = 1.25
  • HP = 80
  • Invulnerability Multiplier = 0.5
  • Size Multiplier = 0.9

218. (CONTENT) Implement the Bit Hunter ship:

Its stats are as follows:

  • Movement Speed Multiplier = 0.9
  • Turn Rate Multiplier = 0.9
  • Ammo = 80
  • Attack Speed Multiplier = 0.8
  • Projectile Speed Multiplier = 0.9
  • Invulnerability Multiplier = 1.5
  • Size Multiplier = 1.1
  • Luck Multiplier = 1.5
  • Resource Spawn Rate Multiplier = 1.5
  • Enemy Spawn Rate Multiplier = 1.5
  • Cycle Speed Multiplier = 1.25

219. (CONTENT) Implement the Sentinel ship:

Its stats are as follows:

  • Energy Shield = true

220. (CONTENT) Implement the Striker ship:

Its stats are as follows:

  • Ammo = 120
  • Attack Speed Multiplier = 2
  • Projectile Speed Multiplier = 1.25
  • HP = 50
  • Additional Barrage Projectiles = 8
  • Barrage on Kill Chance = 10%
  • Barrage on Cycle Chance = 10%
  • Barrage Nova = true

221. (CONTENT) Implement the Nuclear ship:

Its stats are as follows:

  • Boost = 80
  • Turn Rate Multiplier = 0.8
  • Ammo = 80
  • Attack Speed Multiplier = 0.85
  • HP = 80
  • Invulnerability Multiplier = 2
  • Luck Multiplier = 1.5
  • Resource Spawn Rate Multiplier = 1.5
  • Enemy Spawn Rate Multiplier = 1.5
  • Cycle Speed Multiplier = 1.5
  • Self Explode on Cycle Chance = 10%

222. (CONTENT) Implement the Cycler ship:

Its stats are as follows:

  • Cycle Speed Multiplier = 2

223. (CONTENT) Implement the Wisp ship:

Its stats are as follows:

  • Boost = 50
  • Movement Speed Multiplier = 0.5
  • Turn Rate Multiplier = 0.5
  • Attack Speed Multiplier = 0.66
  • Projectile Speed Multiplier = 0.5
  • HP = 50
  • Size Multiplier = 0.75
  • Resource Spawn Rate Multiplier = 1.5
  • Enemy Spawn Rate Multiplier = 1.5
  • Shield Projectile Chance = 100%
  • Projectile Duration Multiplier = 1.5

END

And now we've finished implementing all content in the game. These last two articles were filled with exercises that are mostly about manually adding all this content. To some people that might be extremely boring, so it's a good gauge of if you like implementing this kind of content or not. A lot of game development is just straight up stuff like this, so if you really really don't like it it's better to learn about it sooner rather than later.

The next article will focus on the skill tree, which is how we're going to present all these passives to the player. We'll focus on building everything necessary to make the skill tree work, but the building of the tree itself (like placing and linking nodes together) will be entirely up to you. This is another one of those things where we're just manually adding content to the game and not doing anything too complicated.



BYTEPATH #1 - Game Loop

@SSYGEA

Introduction

This tutorial series will cover the creation of a complete game with Lua and LÖVE. It's aimed at programmers who have some experience but are just starting out with game development, or game developers who already have some experience with other languages or frameworks but want to figure out Lua or LÖVE better.

The game that will be created is called 「BYTEPATH」 and it's a mix of Bit Blaster XL and Path of Exile's Passive Skill Tree. It's simple enough that it can be covered in a number of articles without extending for too long, but with enough content that a beginner would feel uncomfortable with the code and end up giving up before finishing.

It's also at a level of complexity that most game development tutorials don't cover. Most of the problems beginners have when starting out with game development has to do with scope. The usual advice is to start small and work your way up, and while that might be a good idea, if the types of projects you're interested in cannot be made any smaller then there are very few resources out there that attempt to guide you through the problems that come up.

In my case, I've always been interested in making games with lots and lots of items/passives/skills and so when I was starting out it was really hard to figure out a good way to structure my code so that I wouldn't get lost. Hopefully these tutorials can help someone with that.


Requirements

Before you start there are some programming knowledge requirements:

  • The basics of programming, like variables, loops, conditionals, basic data structures and so on;

  • The basics of OOP, like knowing what classes, instances, attributes and methods are;

  • And the very basics of Lua, this quick tutorial should be good enough.

Essentially this is not for people who are just getting started with programming in general. Also, this tutorial series will have exercises. If you've ever been in the situation where you finish a tutorial and you don't know what to do next it's probably because it had no exercises, so if you don't want that to happen here then I recommend at least trying to do them.


START

To start off you need to install LÖVE on your system and then figure out how to run LÖVE projects. You can follow the steps from here for that.

Once that's done you should create a main.lua file in your project folder with the following contents:

function love.load()

end

function love.update(dt)

end

function love.draw()

end

If you run this you should see a window popup and it should show a black screen. In the code above, once your LÖVE project is run the love.load function is run once at the start of the program and love.update and love.draw are run every frame. So, for instance, if you wanted to load an image and draw it, you'd do something like this:

function love.load()
    image = love.graphics.newImage('image.png')
end

function love.update(dt)

end

function love.draw()
    love.graphics.draw(image, 0, 0)
end

love.graphics.newImage loads the image texture to the image variable and then every frame it's drawn at position 0, 0. To see that love.draw actually draws the image on every frame, try this:

love.graphics.draw(image, love.math.random(0, 800), love.math.random(0, 600))

The default size of the window is 800x600, so what this should do is randomly draw the image around the screen really fast:

Note that between every frame the screen is cleared, otherwise the image you're drawing randomly would slowly fill the entire screen as it is drawn in random positions. This happens because LÖVE provides a default game loop for its projects that clears the screen at the end of every frame. I'll go over this game loop and how you can change it now.


Game Loop

The default game loop LÖVE uses can be found in the love.run page, and it looks like this:

function love.run()
    if love.math then
        love.math.setRandomSeed(os.time())
    end

    if love.load then love.load(arg) end

    -- We don't want the first frame's dt to include time taken by love.load.
    if love.timer then love.timer.step() end

    local dt = 0

    -- Main loop time.
    while true do
        -- Process events.
        if love.event then
            love.event.pump()
            for name, a,b,c,d,e,f in love.event.poll() do
                if name == "quit" then
                    if not love.quit or not love.quit() then
                        return a
                    end
                end
                love.handlers[name](a,b,c,d,e,f)
            end
        end

        -- Update dt, as we'll be passing it to update
        if love.timer then
            love.timer.step()
            dt = love.timer.getDelta()
        end

        -- Call update and draw
        if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled

        if love.graphics and love.graphics.isActive() then
            love.graphics.clear(love.graphics.getBackgroundColor())
            love.graphics.origin()
            if love.draw then love.draw() end
            love.graphics.present()
        end

        if love.timer then love.timer.sleep(0.001) end
    end
end

When the program starts love.run is run and then from there everything happens. The function is fairly well commented and you can find out what each function does on the LÖVE wiki. But I'll go over the basics:

if love.math then
    love.math.setRandomSeed(os.time())
end

In the first line we're checking to see if love.math is not nil. In Lua all values are true, except for false and nil, so the if love.math condition will be true if love.math is defined as anything at all. In the case of LÖVE these variables are set to be enabled or not in the conf.lua file. You don't need to worry about this file for now, but I'm just mentioning it because it's in that file that you can enable or disable individual systems like love.math, and so that's why there's a check to see if it's enabled or not before anything is done with one of its functions.

In general, if a variable is not defined in Lua and you refer to it in any way, it will return a nil value. So if you ask if random_variable then this will be false unless you defined it before, like random_variable = 1.

In any case, if the love.math module is enabled (which it is by default) then its seed is set based on the current time. See love.math.setRandomSeed and os.time. After doing this, the love.load function is called:

if love.load then love.load(arg) end

arg are the command line arguments passed to the LÖVE executable when it runs the project. And as you can see, the reason why love.load only runs once is because it's only called once, while the update and draw functions are called multiple times inside a loop (and each iteration of that loop corresponds to a frame).

-- We don't want the first frame's dt to include time taken by love.load.
if love.timer then love.timer.step() end

local dt = 0

After calling love.load and after that function does all its work, we verify that love.timer is defined and call love.timer.step, which measures the time taken between the two last frames. As the comment explains, love.load might take a long time to process (because it might load all sorts of things like images and sounds) and that time shouldn't be the first thing returned by love.timer.getDelta on the first frame of the game.

dt is also initialized to 0 here. Variables in Lua are global by default, so by saying local dt it's being defined only to the local scope of the current block, which in this case is the love.run function. See more on blocks here.

-- Main loop time.
while true do
    -- Process events.
    if love.event then
        love.event.pump()
        for name, a,b,c,d,e,f in love.event.poll() do
            if name == "quit" then
                if not love.quit or not love.quit() then
                    return a
                end
            end
            love.handlers[name](a,b,c,d,e,f)
        end
    end
end

This is where the main loop starts. The first thing that is done on each frame is the processing of events. love.event.pump pushes events to the event queue and according to its description those events are generated by the user in some way, so think key presses, mouse clicks, window resizes, window focus lost/gained and stuff like that. The loop using love.event.poll goes over the event queue and handles each event. love.handlers is a table of functions that calls the relevant callbacks. So, for instance, love.handlers.quit will call the love.quit function if it exists.

One of the things about LÖVE is that you can define callbacks in the main.lua file that will get called when an event happens. A full list of all callbacks is available here. I'll go over callbacks in more detail later, but this is how all that happens. The a, b, c, d, e, f arguments you can see passed to love.handlers[name] are all the possible arguments that can be used by the relevant functions. For instance, love.keypressed receives as arguments the key pressed, its scancode and if the key press event is a repeat. So in the case of love.keypressed the a, b, c values would be defined as something while d, e, f would be nil.

-- Update dt, as we'll be passing it to update
if love.timer then
    love.timer.step()
    dt = love.timer.getDelta()
end

-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled

love.timer.step measures the time between the two last frames and changes the value returned by love.timer.getDelta. So in this case dt will contain the time taken for the last frame to run. This is useful because then this value is passed to the love.update function, and from there it can be used in the game to define things with constant speeds, despite frame rate changes.

if love.graphics and love.graphics.isActive() then
    love.graphics.clear(love.graphics.getBackgroundColor())
    love.graphics.origin()
    if love.draw then love.draw() end
    love.graphics.present()
end

After calling love.update, love.draw is called. But before that we verify that the love.graphics module exists and that we can draw to the screen via love.graphics.isActive. The screen is cleared to the defined background color (initially black) via love.graphics.clear, transformations are reset via love.graphics.origin, love.draw is finally called and then love.graphics.present is used to push everything drawn in love.draw to the screen. And then finally:

if love.timer then love.timer.sleep(0.001) end

I never understood why love.timer.sleep needs to be here at the end of the frame, but the explanation given by a LÖVE developer here seems reasonable enough.

And with that the love.run function ends. Everything that happens inside the while true loop is referred to as a frame, which means that love.update and love.draw are called once per frame. The entire game is basically repeating the contents of that loop really fast (like at 60 frames per second), so get used to that idea. I remember when I was starting it took me a while to get an instinctive handle on how this worked for some reason.

There's a helpful discussion on this function on the LÖVE forums if you want to read more about it.

Anyway, if you don't want to you don't need to understand all of this at the start, but it's helpful to be somewhat comfortable with editing how your game loop works and to figure out how you want it to work exactly. There's an excellent article that goes over different game loop techniques and does a good job of explaining each. You can find it here.


Game Loop Exercises

1. Implement the Fixed Delta Time loop from the Fix Your Timestep article by changing love.run.

2. Implement the Variable Delta Time loop from the Fix Your Timestep article by changing love.run.

3. Implement the Semi-Fixed Timestep loop from the Fix Your Timestep article by changing love.run.

4. Implement the Free the Physics loop from the Fix Your Timestep article by changing love.run.


Game Loop Exercises HELP!

I'm going to go over the solution for the first two exercises. In general I'm not going to provide answers to exercises because it would take me a lot of time to do so and because the process you take to get to the answers is the most important part of it all, and often that will involve some googling. But for these first parts I'll go over a few of the solutions to help a little.


For the first question, we want to implement the Fixed Delta Time loop. As the article states, this one uses a fixed delta of 1/60s. In the default love.run function the delta changes based on the value returned by love.timer.getDelta, and that changes based on the time taken between the two last frames. So the default implementation is definitely not a fixed delta of 1/60s. To achieve that, we need to first remove the sections that change the delta in any way. So this piece of code will be commented out:

if love.timer then
    love.timer.step()
    dt = love.timer.getDelta()
end

And then we need to define our dt variable to the value we want it to have, which is 1/60:

local dt = 1/60

And then the final love.run function would look like this:

function love.run()
	if love.math then
		love.math.setRandomSeed(os.time())
	end

	if love.load then love.load(arg) end

	-- We don't want the first frame's dt to include time taken by love.load.
	-- if love.timer then love.timer.step() end

	local dt = 1/60

	-- Main loop time.
	while true do
		-- Process events.
		if love.event then
			love.event.pump()
			for name, a,b,c,d,e,f in love.event.poll() do
				if name == "quit" then
					if not love.quit or not love.quit() then
						return a
					end
				end
				love.handlers[name](a,b,c,d,e,f)
			end
		end

    --[[
		-- Update dt, as we'll be passing it to update
		if love.timer then
			love.timer.step()
			dt = love.timer.getDelta()
		end
    ]]--

		-- Call update and draw
		if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled

		if love.graphics and love.graphics.isActive() then
			love.graphics.clear(love.graphics.getBackgroundColor())
			love.graphics.origin()
			if love.draw then love.draw() end
			love.graphics.present()
		end

		if love.timer then love.timer.sleep(0.001) end
	end
end

So we commented out sections that updated dt or that called love.timer.step. (-- is used to comment out a line and --[[]]-- is used to comment out blocks of code in Lua) We defined dt as 1/60 and the rest of the function remains exactly the same. The problems with this approach are explained in the article, but that's how you'd do it if you wanted to for some reason.


The second question asks for the implementation of the Variable Delta Time loop. The way this is described is as follows:

Just measure how long the previous frame takes, then feed that value back in as the delta time for the next frame.

If we go back to the description of the love.timer.step and love.timer.getDelta functions, that's exactly what they do. So, as it turns out, there seems to be no differences between the default love.run implementation and Variable Delta Time.


END

And with that this part of the tutorial is over. By now you should be able to run basic LÖVE projects and have some understanding of how its game loop works. For the next part I'll introduce some useful libraries that will be used throughout the entire project.

Kimetsu no Yaiba and Agreeableness

17/08/19 - Kimetsu no Yaiba and Agreeableness

There's a new show this season called Kimetsu no Yaiba about a boy who becomes a demon slayer. This is a show that I've been thinking a lot about lately because only recently (at around episode 17) I realized something important about it. There will be spoilers going forward, so stop reading if you want to watch it yourself.

The main confusion I had throughout the 17 episodes before my realization was that this seemed like a normal shounen show (where the main characters gets getting more powerful with time and overcoming his obstacles) but it had a bunch of small differences to it that seemed really off and somehow prevented me from enjoying it. I kept watching mostly because of the animation quality (very high) and because I was curious about what would happen, but I was really having a hard time liking it.

The first difference to note is that in most shounen shows the main character is missing a father figure primarily but sets out on a quest set by that father figure directly or indirectly. In Hunter x Hunter Gon sets out to become a hunter because his dad was one and he wanted to meet him and understand why he left. In Attack on Titan Eren receives the key to the basement from his father and sets out on a quest to go back to that basement. It's a common trope for this to be an element of good stories and it's no different here.

In Kimetsu no Yaiba, Tanjirou is also missing a father figure, but he has quite a big family, and his father's death has happened far enough before the show starts that Tanjirou, as the oldest male in the family, has already appropriately taken the role of providing and protecting his younger siblings and his mother as much as he can. A demon attacks and kills his family, turns his only surviving sister into a demon, and so he sets out on a quest to figure out how he can turn his only family back left into a human by becoming a demon slayer. This motivation seems similar to the ones mentioned before but it's slightly different because it's about protection rather than going on a pure adventure instead.

Tanjirou as a character is also an unusually kind person. Most shounen characters are rather self-centered and disagreeable, meaning that they'll do things mostly their own way, but Tanjirou is constantly shown to be able to diffuse situations with kindness and understanding rather than through conflict (physical or otherwise).

Along the way, Tanjirou meets two additional characters that are also demon slayers and start traveling with him. The first is Zenitsu, a really neurotic character who's basically afraid of everything. When I first watched this character be introduced I really wondered why whoever wrote this story would do this. It isn't funny nor interesting to watch a character be constantly afraid of everything. And the way the he is portrayed on the show is really really over the top too, so it's even more unappealing.

The other character introduced is Inosuke, an uncivilized boy who likely grew up away from civilization, who is constantly looking for fights and is very angry. Inosuke is more normal than Zenitsu in terms of where this kind of character fits in a story, but I also thought it was an unnatural addition. However, extremely angry and uncontrollable characters like these aren't that rare, for instance, Bakugo in Boku no Hero Academia is a good example of this that works really well. Inosuke also happens to be fairly attractive (despite constantly wearing a boar's head as a mask), and this is also important to note.

The addition of these two characters was really odd, and made a show that was already odd in various ways feel even more alien to me. I just wasn't connecting with the things happening nor with the characters. But then I finally understood why:


Agreeableneess

My main realization was that this show was written by someone and for someone who is considerably higher in agreeableness than me. Most shounen shows are written without a heavy focus on kindness, protection and compassion in the way that this show is, and that's the main reason why the show felt off to me. I'm someone who's fairly low in agreeableness so my idea is that I'll feel this difference in focus even more than most people.

The first thing to note is that the character is higher in agreeableness by nature. He's kind, compassionate, looking out for his family and friends way more than the average shounen protagonist (or even the average person), and he's constantly self-sacrificing for others. You could describe most shounen characters in this way, but it's the small details and the whole composition that matters. For instance, every character Tanjirou meets is used to enable the author to show off and/or support his kind nature: he saves a family of kids from a demon, handling and protecting the kids in a very soft and calming way; he meets an old demon lady and her younger demon friend who's constantly trying to pick fights with him, but he constantly defuses those attempts without getting aggressive; he meets a powerful enemy who wants to have a true family, which allows the author to further explore the importance and depth of Tanjirou's own bond with his sister; he meets a character driven by grief and sorrow and manages to sooth him and give him hope for the future, and so on... The show just flows differently and focuses more on these agreeable aspects rather than other things (despite being a shounen show and having a fair bit of physical combat).

The second thing to note is that the permanent supporting cast of characters (Zenitsu and Inosuke) perfectly match a high agreeableness main character. One of the things about high agreeableness is that people who are like this are highly focused on helping others, but the fundamental ways in which this is shown in stories generally can take a few shapes.

One shape is protection. This show hits that with the main character constantly carrying his sister around and trying to save her, his backstory of failing to protect his family (the greatest tragedy for someone driven by the protection instinct), and also his willingness to protect his new found friends. Another shape it can take is the shape of "taming". One of the central female myths, if you could call it that, is one where a woman finds a brute, uncivilized, uncontrollable, but extremely attractive/handsome male, and tames him so that he can become a proper and civilized member of society. This show achieves that through Inosuke, the extremely attractive but uncivilized character that tags along Tanjirou.

This also isn't unique to this show. The other show that did something like this most successfully is Boku no Hero Academia with Bakugo, an also extremely angry and attractive character who the main character slowly civilizes through his kindness. It's also important to note that I mentioned that this is the "central female myth", and this is important because women will identify with this trope way more than men. So even though Tanjirou/Deku are male characters, the female element in them (which is their overall kindness, as women are generally more agreeable than men) is able to make this trope a believable reality in those stories.

One of the benefits that comes from this type of dynamic is that the angry/uncontrollable character is already attractive to male watchers as he embodies a natural aggressive tendency in men, but also immediately becomes more attractive to women who are watching the show, especially in moments where he shows weakness and the main character comes to their rescue (be it physical/combat or otherwise). The other benefit is that the main character also has this great duality to him, where he can be an adventurer constantly getting more powerful, which men will identify with, but by having him be kinder than general and also having this strong behavioral contrast with the supporting character, also makes the main character more attractive to women watchers too. And please note that when I say male/female, I'm not necessarily talking about someone's literal sex, even though for most people that will be the case. You can be a man that is very effeminate personality wise, and so most of the things that I'm talking about here and saying that are related to women, will apply to you instead.

Also, another thing that comes out of these dynamics is that the kind main character and the angry supporting character are generally the most "shipped" characters by people online, which sort of supports what I'm saying. This already happens with Deku/Bakugo and I'm sure will also happen with Tanjirou/Inosuke.

Zenitsu is the other supporting character and one of the reasons why I had my realization about this show on episode 17 was because this was the episode where Zenitsu's character arc starts properly. In that episode we're given his backstory and essentially he's a very afraid boy who doesn't believe in and constantly has negative thoughts about himself. He's essentially a very high neuroticism character. Another sign of this is the fact that he's mastered a single attack to perfection. Something that people who are high in neuroticism and conscientiousness tend to do is work themselves to death on perfecting something, since their neuroticism tells them it isn't good enough, and their conscientiousness makes them not have any trouble with working harder and more.

His character arc is going to be about him becoming less afraid, more self-reliant and more assertive. This is the primary mode of being for someone who's extremely high in neuroticism and Zenitsu happens to be a fairly well written high neuroticism character. Characters like these are not common in shounen shows because people watching shounen shows are generally not that high in neuroticism (men are generally lower in neuroticism than women). One of the reasons why I thought this character was so unappealing and dumb was because I'm fairly low in neuroticism myself and so I really can't identify with this character's struggles at all. But I can see how if fits with everything I know of neuroticism and how people high in it tend to behave.

The main conclusion that I reached on seeing this is that this is a show primarily written for women or for more agreeable men and one of the reasons why you'd have a character high in neuroticism like this is to explore what high neuroticism means and how you can overcome it. The depression/self-doubt/anxiety that comes with being a high neuroticism individual can be expressed through Zenitsu and can be alleviated by the main character. Even though so far Zenitsu has been mostly going through his moments of increase self-reliance by himself, I'd expect the author to start having Tanjirou take a more active role in helping Zenitsu through his troubles.

It's also worth nothing that the reason this author can have this character in a show like this and actually make it work is because the entire show supports it through the main character and the themes the show talks about always being around protection/family/kindness, and so on. You can't have a serious high neuroticism character in your average shounen story because they just don't fit in anywhere.


END

So, to end, what I wanted to say is that this is a good example of a show that manages to balance usual shounen tropes with a high agreeableness bent to it. If I had to guess this show will be more popular among women than the average shounen (as women are on average higher in agreeableness and neuroticism than men), and also be more popular among non-shounen watching higher agreeableness people in general.

And we often hear of people talking about how media should cater to women more and so on, and I think that it's important to think about what kinds of stories can achieve that while doing so without being at the detriment of men's tastes (as men's tastes are often what drives financial success in various fields, like PC gaming for instance). I think this story manages to balance that pretty well and it's a good case study.

Replay System and Lua's Flexibility

2015-12-25 12:55

In this post I'll explain how I approached creating a simple replay system using Lua. There are two main ways I know of to do this, one is storing inputs and ensuring your simulation is deterministic, another is storing all state needed every frame to play the game back. I decided to go with the second one for now because it seemed easier. The game I'll use as an example for this is the one I made during the last Ludum Dare and you can find the code for all this here.


Objects, attributes and methods in Lua

All objects in Lua are made out of tables (essentially hash tables), with attribute and method names being the keys, and values and functions being the values. So, for instance, suppose a class definition like this:

GameObject = class('GameObject')

function GameObject:new(x, y)
    self.x = x
    self.y = y
end

Since In Lua, table.something is the same as table["something"], accessing a key in a table looks the same as accessing the attribute of an object. So we could say that the class definition above is essentially the same as this:

object = {}
object["new"] = function(x, y)
    object.x = x
    object["y"] = y
end

This property of the language has a few interesting results. For this article the one we're going to focus on in this post is the ability to add and change attributes of an object with a lot of ease. For instance, consider the slightly modified class definition:

GameObject = class('GameObject')

function GameObject:new(x, y, opts)
    self.x = x
    self.y = y
    for k, v in pairs(opts) do self[k] = v end
end

We've added an opts argument there and we're doing something with it in a loop. Assuming opts is a table, what the loop is doing is going through all of its keys and values (named k and v respectively) and then assigning v to self[k]. What this does is that if you have a GameObject construction call that looks like this:

game_object = GameObject(100, 200, {hp = 100, damage = 10, layer = 'Default'})

Now on top of game_object.x and game_object.y being 100 and 200, you also have game_object.layer being 'Default', and game_object.damage being 10. This is because the loop in the constructor went through all keys and values in the opts table and assigned them to the object being constructed by saying self[k] = v.


Replay State

The replay system we're building uses this idea heavily. But before we explore that I need to explain at a high level how it will work. The way we're doing replays is by storing state every frame and then when we need to play it back we just read that stored state. The easiest way I could imagine of doing this was, when replaying, having multiple instances of a class called ReplayObject. These objects will be responsible for reading the saved state and changing themselves (much like in the opts example above) to match the state of some object that was saved on that frame. Then when all that is done for all objects, all replay objects will be drawn, and since their state is matching that of some original object that was recorded, it will be just like if it was the real thing.

If this didn't make much sense let's try looking at a real world example. Below I have the draw definition of an object in my game, a particle effect for when bats spawn:

function BatSpawnParticle:draw()
    love.graphics.setColor(52, 52, 54)
    local w, h = math.rangec(self.v, 0, 400, 0, 1), math.rangec(self.v, 0, 400, 3, 1)
    pushRotate(self.x, self.y, self.angle)
    draft:ellipse(self.x, self.y, math.ceil(self.m*math.max(self.v/8, 3)), 
        math.ceil(self.m*math.ceil(h)), 60, 'fill')
    love.graphics.pop()
    love.graphics.setColor(255, 255, 255)
end

Inside a ReplayObject we'll be reusing the BatSpawnParticle.draw function somehow, since the replay object will be sort of mimicking a BatSpawnParticle, and because of that we need it to have all the state that is used in that draw function. Usually this boils down to saving everything that uses an attribute from self, so in this case: self.x, self.y, self.angle, self.m and self.v. And this would look like this:

function BatSpawnParticle:update(dt)
    self.timer:update(dt)
    self.x = self.x + self.v*math.cos(self.angle)*dt
    self.y = self.y + self.v*math.sin(self.angle)*dt

    if recording_replay then
        table.insert(replay_data[frame], {source_class_name = self.class_name, 
            layer = self.layer, x = self.x, y = self.y, angle = self.angle, 
            v = self.v, m = self.m})
    end
end

replay_data is the table we're using to store everything, and replay_data[frame] is just another table that holds all the saved state for this frame. We'll do this for every class in the game that we're interesting in saving for playback. In any case, now that we've added all the state needed to draw this object when it's replaying, we can look at what a ReplayObject looks like.


ReplayObject

At its core the replay object is very simple and it looks like this:

ReplayObject = class('ReplayObject')

function ReplayObject:new(x, y, opts)
    self.x = x
    self.y = y
    for k, v in pairs(opts) do self[k] = v end
end

function ReplayObject:draw()
    _G[self.source_class_name].draw(self)
end

The single line in the draw function is the one that makes use of another object's draw function. If you remember when saving the state of BatSpawnParticle, I said source_class_name = self.class_name. All my objects have their class name stored in self.class_name (including ReplayObjects), so source_class_name stores the class name of the object that this replay object is supposed to mimick. In this case it stores the string 'BatSpawnParticle'. Another thing to note is that all my class definitions look like this:

BatSpawnParticle = class('BatSpawnParticle')

class is a function that creates the class definition, and BatSpawnParticle is just a variable that holds that class definition. Variables are global by default in Lua, so BatSpawnParticle is now a global variable that holds that class definition. All global variables in Lua are stored in the table _G, so, for instance, we can access the draw function of BatSpawnParticle by saying _G['BatSpawnParticle'].draw. Again, since functions are also just key/value pairs in a table we can do that safely.

Finally, one last thing about that line in replay object's draw function is that it passes self to the draw function. If you go back and look at the BatSpawnParticle:draw definition it uses a :. In Lua, that's a shorthand for passing self as the first parameter, so BatSpawnParticle:draw() is exactly the same as BatSpawnParticle.draw(self). The trick here is that ReplayObject is passing itself as self to BatSpawnParticle.draw, but since it already has all the state needed by that function (since that was what was saved), the function will work properly and the replay object will be drawn as if it were a BatSpawnParticle. This trick is what allows us to use only ReplayObjects to mimick and draw every single object in the game while replaying.


Level

Finally, one last thing is needed to bring this all together, which is coordinating ReplayObject creation/destruction based on the replay_data for this frame. I have a Level class where all my game objects are updated, and there I can do something like this:

function Level:update(dt)
    if replaying then
        -- replay update
        1. match number of replay objects to that of tables saved in replay_data[frame]
        for i, replay_object in replay objects
            2. clear replay_object
            3. morph replay_object into replay_data[frame][i] 
            update replay_object
    else
        -- normal update
    end
end

Step 1 is important because it means we'll be reusing replay objects as much as possible. If the number of saved tables in replay_data[frame] is bigger than the current number of replay objects then we create new ones, otherwise we delete extra ones. This is going to be happening every frame such that at all times the number of replay objects alive is exactly the same as the number of objects saved for that frame. And that looks like this:

...
if #replay_data[frame] > #self.game_objects then
    local d = #replay_data[frame] - #self.game_objects
    for i = 1, d do self:createGameObject('ReplayObject', 0, 0) end

elseif #self.game_objects > #replay_data[frame] then
    local d = #self.game_objects - #replay_data[frame] 
    for i = 1, d do table.remove(self.game_objects, #self.game_objects) end
end

Step 2 is needed so that we clear the state of this replay object so that it can be reused again. If we don't do this what can happen is that in one frame a certain ReplayObject is used as a BatSpawnParticle and in the next it's used as the Player, and since we aren't clearing it now it has state left over from when it was a BatSpawnParticle and this can quickly lead to bugs. An easy way to clear an object is like this:

function ReplayObject:clear()
    for k, v in pairs(self) do
        if k ~= 'new' and k ~= 'update' and k ~= 'draw' and k ~= 'clear' then
            self[k] = nil
        end
    end
end 

We just go over all keys and values in self and verify that they're not things we don't want to clear, for instance, we don't want to erase the reference to the clear function itself. So once we know it's safe we just nil that value. This way, all attributes from a previous frame will be cleared and the object will be brand new for step 3, where we make this replay object mimick a saved object. This boils down to a single simple line:

-- while going over all replay objects...
for k, v in pairs(replay_data[frame][i]) do replay_object[k] = v end

After this, we should be able to draw each replay object as if it were a normal object that we saved and so the replay system works completely. It's important to notice that what we did for BatSpawnParticle in saving its state needs to be done for ALL game objects that need their state saved. Depending on how many objects you have and how complex your draw operations are this can be a bit of work.


Replaying

I glossed over some details but here's an important one: every frame you're doing frame++ on your frame counter, and at the start of every frame you're starting saying replay_data[frame] = {} so that the table that is going to hold all stored data in this frame is initialized. When replaying all you have to do is say frame = 1 and set some replaying bool to true, and then you'll be back at frame 1, reading the replay data you saved back then. This means that you can also go back and forwards in time at will, as the gif below shows.

While replaying, pausing the replay means not increasing the frame counter. Jumping forwards in time means increasing the frame counter by an amount bigger than 1. Jumping backwards in time means decreasing the frame counter by an amount bigger than 1. Going to the end of the replay means setting the frame counter to the size of replay_data. Going to the start of the replay means setting the frame counter to 1 or to the number of the frame where you started recording. All of these are relatively simple operations that let you have control over your playback.


END

There are some drawbacks to this way of doing things, the main one being the size of a replay. Initially I built this because I wanted to be able to watch players play the game and be able to learn something from it. For instance, in this video, the player takes 4 minutes to figure out what exactly he's supposed to do in the game: (click to watch)

This is totally not a cool thing because I'd randomly guess that like 50% of people who play the game end up quitting before they figure out what they have to do. This is an easy mistake to fix but it's the kind of thing that's also easy to miss if you don't actively watch people playing your game, and a replay system helps with that.

Sadly, using this current technique (with no optimizations, although I've tried a few optimizations and they didn't help that much), about 10 seconds of gameplay results in a 10mb file. This is a prohibitive size if I want people to send files to me, so eventually I'll get around to implementing the other way where I only record inputs (and maybe write an article about it too) for this purpose. Overall though this is was a cool thing to build because now at least making gifs out of the game is easier, since I can just pause everything, go back, forwards, move the camera around, and so on.

BYTEPATH #2 - Libraries

@SSYGEA

Introduction

In this tutorial I'll cover a few Lua/LÖVE libraries that are necessary for the project and I'll also explore some ideas unique to Lua that you should start to get comfortable with. There will be a total of 4 libraries used by the end of it, and part of the goal is to also get you used to the idea of downloading libraries built by other people, reading through the documentation of those and figuring out how they work and how you can use them in your game. Lua and LÖVE don't come with lots of features by themselves, so downloading code written by other people and using it is a very common and necessary thing to do.


Object Orientation

The first thing I'll cover here is object orientation. There are many many different ways to get object orientation working with Lua, but I'll just use a library. The OOP library I like the most is rxi/classic because of how small and effective it is. To install it just download it and drop the classic folder inside the project folder. Generally I create a libraries folder and drop all libraries there.

Once that's done you can import the library to the game at the top of the main.lua file by doing:

Object = require 'libraries/classic/classic'

As the github page states, you can do all the normal OOP stuff with this library and it should work fine. When creating a new class I usually do it in a separate file and place that file inside an objects folder. So, for instance, creating a Test class and instantiating it once would look like this:

-- in objects/Test.lua
Test = Object:extend()

function Test:new()

end

function Test:update(dt)

end

function Test:draw()

end
-- in main.lua
Object = require 'libraries/classic/classic'
require 'objects/Test'

function love.load()
    test_instance = Test()
end

So when require 'objects/Test' is called in main.lua, everything that is defined in the Test.lua file happens, which means that the Test global variable now contains the definition for the Test class. For this game, every class definition will be done like this, which means that class names must be unique since they are bound to a global variable. If you don't want to do things like this you can make the following changes:

-- in objects/Test.lua
local Test = Object:extend()
...
return Test
-- in main.lua
Test = require 'objects/Test'

By defining the Test variable as local in Test.lua it won't be bound to a global variable, which means you can bind it to whatever name you want when requiring it in main.lua. At the end of the Test.lua script the local variable is returned, and so in main.lua when Test = require 'objects/Test' is declared, the Test class definition is being assigned to the global variable Test.

Sometimes, like when writing libraries for other people, this is a better way of doing things so you don't pollute their global state with your library's variables. This is what classic does as well, which is why you have to initialize it by assigning it to the Object variable. One good result of this is that since we're assigning a library to a variable, if you wanted to you could have named Object as Class instead, and then your class definitions would look like Test = Class:extend().

One last thing that I do is to automate the require process for all classes. To add a class to the environment you need to type require 'objects/ClassName'. The problem with this is that there will be lots of classes and typing it for every class can be tiresome. So something like this can be done to automate that process:

function love.load()
    local object_files = {}
    recursiveEnumerate('objects', object_files)
end

function recursiveEnumerate(folder, file_list)
    local items = love.filesystem.getDirectoryItems(folder)
    for _, item in ipairs(items) do
        local file = folder .. '/' .. item
        if love.filesystem.isFile(file) then
            table.insert(file_list, file)
        elseif love.filesystem.isDirectory(file) then
            recursiveEnumerate(file, file_list)
        end
    end
end

So let's break this down. The recursiveEnumerate function recursively enumerates all files inside a given folder and add them as strings to a table. It makes use of LÖVE's filesystem module, which contains lots of useful functions for doing stuff like this.

The first line inside the loop lists all files and folders in the given folder and returns them as a table of strings using love.filesystem.getDirectoryItems. Next, it iterates over all those and gets the full file path of each item by concatenating (concatenation of strings in Lua is done by using ..) the folder string and the item string. Let's say that the folder string is 'objects' and that inside the objects folder there is a single file named GameObject.lua. And so the items list will look like this: items = {'GameObject.lua'}. When that list is iterated over, the local file = folder .. '/' .. item line will parse to local file = 'objects/GameObject.lua', which is the full path of the file in question.

Then, this full path is used to check if it is a file or a directory using the love.filesystem.isFile and love.filesystem.isDirectory functions. If it is a file then simply add it to the file_list table that was passed in from the caller, otherwise call recursiveEnumerate again, but now using this path as the folder variable. When this finishes running, the file_list table will be full of strings corresponding to the paths of all files inside folder. In our case, the object_files variable will be a table full of strings corresponding to all the classes in the objects folder.

There's still a step left, which is to take all those paths and require them:

function love.load()
    local object_files = {}
    recursiveEnumerate('objects', object_files)
    requireFiles(object_files)
end

function requireFiles(files)
    for _, file in ipairs(files) do
        local file = file:sub(1, -5)
        require(file)
    end
end

This is a lot more straightforward. It simply goes over the files and calls require on them. The only thing left to do is to remove the .lua from the end of the string, since the require function spits out an error if it's left in. The line that does that is local file = file:sub(1, -5) and it uses one of Lua's builtin string functions. So after this is done all classes defined inside the objects folder can be automatically loaded. The recursiveEnumerate function will also be used later to automatically load other resources like images, sounds and shaders.


OOP Exercises

1. Create a Circle class that receives x, y and radius arguments in its constructor, has x, y, radius and creation_time attributes and has update and draw methods. The x, y and radius attributes should be initialized to the values passed in from the constructor and the creation_time attribute should be initialized to the relative time the instance was created (see love.timer). The update method should receive a dt argument and the draw function should draw a white filled circle centered at x, y with radius radius (see love.graphics). An instance of this Circle class should be created at position 400, 300 with radius 50. It should also be updated and drawn to the screen. This is what the screen should look like:

2. Create an HyperCircle class that inherits from the Circle class. An HyperCircle is just like a Circle, except it also has an outer ring drawn around it. It should receive additional arguments line_width and outer_radius in its constructor. An instance of this HyperCircle class should be created at position 400, 300 with radius 50, line width 10 and outer radius 120. This is what the screen should look like:

3. What is the purpose of the : operator in Lua? How is it different from . and when should either be used?

4. Suppose we have the following code:

function createCounterTable()
    return {
        value = 1,
        increment = function(self) self.value = self.value + 1 end,
    }
end

function love.load()
    counter_table = createCounterTable()
    counter_table:increment()
end

What is the value of counter_table.value? Why does the increment function receive an argument named self? Could this argument be named something else? And what is the variable that self represents in this example?

5. Create a function that returns a table that contains the attributes a, b, c and sum. a, b and c should be initiated to 1, 2 and 3 respectively, and sum should be a function that adds a, b and c together. The final result of the sum should be stored in the c attribute of the table (meaning, after you do everything, the table should have an attribute c with the value 6 in it).

6. If a class has a method with the name of someMethod can there be an attribute of the same name? If not, why not?

7. What is the global table in Lua?

8. Based on the way we made classes be automatically loaded, whenever one class inherits from another we have code that looks like this:

SomeClass = ParentClass:extend()

Is there any guarantee that when this line is being processed the ParentClass variable is already defined? Or, to put it another way, is there any guarantee that ParentClass is required before SomeClass? If yes, what is that guarantee? If not, what could be done to fix this problem?

9. Suppose that all class files do not define the class globally but do so locally, like:

local ClassName = Object:extend()
...
return ClassName

How would the requireFiles function need to be changed so that we could still automatically load all classes?


OOP Exercises HELP!

I'm going to go over the solution for the first two exercises. In general I'm not going to provide answers to exercises because it would take me a lot of time to do so and because the process you take to get to the answers is the most important part of it all and often that will involve some googling. But for these first parts I'll go over a few of the solutions to set a sort of example on how I would be exploring the questions.


The first question is asking for an specific implementation of some class. We can use the examples from rxi/classic to figure out how to do everything we need. First, let's create the skeleton of the class in Circle.lua:

Circle = Object:extend()

function Circle:new()

end

Then the question mentions how the constructor should receive x, y and radius arguments, and how those should also be the attributes the class will have. Based on the Creating a new class example on the github page, we can come up with this:

Circle = Object:extend()

function Circle:new(x, y, radius)
    self.x, self.y, self.radius = x, y, radius
    self.creation_time = love.timer.getTime()
end

The question then says how there should be update and draw methods:

Circle = Object:extend()

function Circle:new(x, y, radius)
    self.x, self.y, self.radius = x, y, radius
    self.creation_time = love.timer.getTime()
end

function Circle:update(dt)

end

function Circle:draw()

end

And then we can implement the main function of drawing the circle:

function Circle:draw()
    love.graphics.circle('fill', self.x, self.y, self.radius)
end

To instantiate an object of this class we need to go back to main.lua, create the object there and then update and draw it:

function love.load()
    circle = Circle(400, 300, 50)
end

function love.update(dt)
    circle:update(dt)
end

function love.draw()
    circle:draw()
end

Here the circle variable is an instance of the Circle class. All of this can be done by looking at the examples in the github page as well as some searching around the LÖVE wiki.


The second question is asking for more of the same, but now so that we can learn how classic deals with inheritance. Inheritance will be used very sparingly throughout this game (I think there's only really 1 class that other classes inherit from if I remember correctly), but it's still a necessary thing to know how to do. Based on the logic used for the first question, we can look at the examples in the github page and arrive at this:

HyperCircle = Circle:extend()

function HyperCircle:new(x, y, radius, line_width, outer_radius)
    HyperCircle.super.new(self, x, y, radius)
    self.line_width = line_width
    self.outer_radius = outer_radius
end

function HyperCircle:update(dt)
    HyperCircle.super.update(self, dt)
end

function HyperCircle:draw()
    HyperCircle.super.draw(self)
    love.graphics.setLineWidth(self.line_width)
    love.graphics.circle('line', self.x, self.y, self.outer_radius)
    love.graphics.setLineWidth(1)
end

The way inheritance works with this library is that whenever you want to execute functionality from your child class, you need to add the line ClassName.super.methodName(self) to what you're doing. So, for instance, in the draw function, HyperCircle.super.draw(self) draws the inner filled circle, and then the next 3 lines draw the outer circle that the HyperCircle class is concerned with. There's a reason why those functions are called with . and self instead of with a : like you would normally, but that's a concern of the next question that you should figure out for yourself :)


Input

Now for how to handle input. The default way to do it in LÖVE is through a few callbacks. When defined, these callback functions will be called whenever the relevant event happens and then you can hook the game in there and do whatever you want with it:

function love.load()

end

function love.update(dt)

end

function love.draw()

end

function love.keypressed(key)
    print(key)
end

function love.keyreleased(key)
    print(key)
end

function love.mousepressed(x, y, button)
    print(x, y, button)
end

function love.mousereleased(x, y, button)
    print(x, y, button)
end

So in this case, whenever you press a key or click anywhere on the screen the information will be printed out to the console. One of the big problems I've always had with this way of doing things is that it forces you to structure everything you do that needs to receive input around these calls.

So, let's say you have a game object which has inside it a level object which has inside a player object. To get the player object receive keyboard input, all those 3 objects need to have the two keyboard related callbacks defined, because at the top level you only want to call game:keypressed inside love.keypressed, since you don't want the lower levels to know about the level or the player. So I created a library to deal with this problem. You can download it and install it like the other library that was covered. Here's a few examples of how it works:

function love.load()
    input = Input()
    input:bind('mouse1', 'test')
end

function love.update(dt)
    if input:pressed('test') then print('pressed') end
    if input:released('test') then print('released') end
    if input:down('test') then print('down') end
end

So what the library does is that instead of relying on callback functions for input, it simply asks if a certain key has been pressed on this frame and receives a response of true or false. In the example above on the frame that you press the mouse1 button, pressed will be printed to the screen, and on the frame that you release it, released will be printed. On all the other frames where the press didn't happen the input:pressed or input:released calls would have returned false and so whatever is inside of the conditional wouldn't be run. The same applies to the input:down function, except it returns true on every frame that the button is held down and false otherwise.

Often times you want behavior that repeats at a certain interval when a key is held down, instead of happening every frame. For that purpose another function that exists is the pressRepeat one:

function love.update(dt)
    if input:pressRepeat('test', 0.5) then print('test event') end
end

So in this example, once the key bound to the test action is held down, every 0.5 seconds test event will be printed to the console.


Input Exercises

1. Suppose we have the following code:

function love.load()
    input = Input()
    input:bind('mouse1', function() print(love.math.random()) end)
end

Will anything happen when mouse1 is pressed? What about when it is released? And held down?

2. Bind the keypad + key to an action named add, then increment the value of a variable named sum (which starts at 0) by 1 every 0.25 seconds when the add action key is held down. Print the value of sum to the console every time it is incremented.

3. Can multiple keys be bound to the same action? If not, why not? And can multiple actions be bound to the same key? If not, why not?

3. If you have a gamepad, bind its DPAD buttons(fup, fdown...) to actions up, left, right and down and then print the name of the action to the console once each button is pressed.

4. If you have a gamepad, bind one of its trigger buttons (l2, r2) to an action named trigger. Trigger buttons return a value from 0 to 1 instead of a boolean saying if its pressed or not. How would you get this value?

5. Repeat the same as the previous exercise but for the left and right stick's horizontal and vertical position.


Input Exercises HELP!

For the first question all that really has to be done is to test the code and see that whenever mouse1 is pressed (not released nor held) the function runs. This is an alternate thing you can do with the bind function, which is to just bind a key to a function that will be executed when the key is pressed.

For the second question we need to first bind the keypad + key to an action named add. To do that we need to figure out what's the string used to represent keypad +. The github page links to this page for key constants and says that for the keyboard they are the same, which means that keypad + is kp+. And so the code looks like this:

function love.load()
    input = Input()
    input:bind('kp+', 'add')
end

Then the question asks to increment a sum variable every 0.25 seconds when the add action key is held down and to print the result to the console. This is simply an application of the pressRepeat function:

function love.load()
    input = Input()
    input:bind('kp+', 'add')
    sum = 0
end

function love.update(dt)
    if input:pressRepeat('add', 0.25) then
        sum = sum + 1
        print(sum)
    end
end

Timer

Now another crucial piece of code to have are general timing functions. For this I'll use hump, more especifically hump.timer.

Timer = require 'libraries/hump/timer'

function love.load()
    timer = Timer()
end

function love.update(dt)
    timer:update(dt)
end

According to the documentation it can be used directly through the Timer variable or it can be instantiated to a new one instead. I decided to do the latter. I'll use this global timer variable for global timers and then whenever timers inside objects are needed, like inside the Player class, it will have its own timer instantiated locally.

The most important timing functions used throughout the entire game are after, every and tween. And while I personally don't use the script function, some people might find it useful so it's worth a mention. So let's go through them:

function love.load()
    timer = Timer()
    timer:after(2, function() print(love.math.random()) end)
end

after is pretty straightfoward. It takes in a number and a function, and it executes the function after number seconds. In the example above, a random number would be printed to the console 2 seconds after the game is run. One of the cool things you can do with after is that you can chain multiple of those together, so for instance:

function love.load()
    timer = Timer()
    timer:after(2, function()
        print(love.math.random())
        timer:after(1, function()
            print(love.math.random())
            timer:after(1, function()
                print(love.math.random())
            end)
        end)
    end)
end

In this example, a random number would be printed 2 seconds after the start, then another one 1 second after that (3 seconds since the start), and finally another one another second after that (4 seconds since the start). This is somewhat similar to what the script function does, so you can choose which one you like best.

function love.load()
    timer = Timer()
    timer:every(1, function() print(love.math.random()) end)
end

In this example, a random number would be printed every 1 second. Like the after function it takes in a number and a function and executes the function after number seconds. Optionally it can also take a third argument which is the amount of times it should pulse for, so, for instance:

function love.load()
    timer = Timer()
    timer:every(1, function() print(love.math.random()) end, 5)
end

Would only print 5 numbers in the first 5 pulses. One way to get the every function to stop pulsing without specifying how many times it should be run for is by having it return false. This is useful for situations where the stop condition is not fixed or known at the time the every call was made.

Another way you can get the behavior of the every function is through the after function, like so:

function love.load()
    timer = Timer()
    timer:after(1, function(f)
        print(love.math.random())
        timer:after(1, f)
    end)
end

I never looked into how this works internally, but the creator of the library decided to do it this way and document it in the instructions so I'll just take it ^^. The usefulness of getting the funcionality of every in this way is that we can change the time taken between each pulse by changing the value of the second after call inside the first:

function love.load()
    timer = Timer()
    timer:after(1, function(f)
        print(love.math.random())
        timer:after(love.math.random(), f)
    end)
end

So in this example the time between each pulse is variable (between 0 and 1, since love.math.random returns values in that range by default), something that can't be achieved by default with the every function. Variable pulses are very useful in a number of situations so it's good to know how to do them. Now, on to the tween function:

function love.load()
    timer = Timer()
    circle = {radius = 24}
    timer:tween(6, circle, {radius = 96}, 'in-out-cubic')
end

function love.update(dt)
    timer:update(dt)
end

function love.draw()
    love.graphics.circle('fill', 400, 300, circle.radius)
end

The tween function is the hardest one to get used to because there are so many arguments, but it takes in a number of seconds, the subject table, the target table and a tween mode. Then it performs the tween on the subject table towards the values in the target table. So in the example above, the table circle has a key radius in it with the initial value of 24. Over the span of 6 seconds this value will changed to 96 using the in-out-cubic tween mode. (here's a useful list of all tweening modes) It sounds complicated but it looks like this:

The tween function can also take an additional argument after the tween mode which is a function to be called when the tween ends. This can be used for a number of purposes, but taking the previous example, we could use it to make the circle shrink back to normal after it finishes expanding:

function love.load()
    timer = Timer()
    circle = {radius = 24}
    timer:after(2, function()
        timer:tween(6, circle, {radius = 96}, 'in-out-cubic', function()
            timer:tween(6, circle, {radius = 24}, 'in-out-cubic')
        end)
    end)
end

And that looks like this:

These 3 functions - after, every and tween - are by far in the group of most useful functions in my code base. They are very versatile and they can achieve a lot of stuff. So make you sure you have some intuitive understanding of what they're doing!


One important thing about the timer library is that each one of those calls returns a handle. This handle can be used in conjunction with the cancel call to abort a specific timer:

function love.load()
    timer = Timer()
    local handle_1 = timer:after(2, function() print(love.math.random()) end)
    timer:cancel(handle_1)

So in this example what's happening is that first we call after to print a random number to the console after 2 seconds, and we store the handle of this timer in the handle_1 variable. Then we cancel that call by calling cancel with handle_1 as an argument. This is an extremely important thing to be able to do because often times we will get into a situation where we'll create timed calls based on certain events. Say, when someone presses the key r we want to print a random number to the console after 2 seconds:

function love.keypressed(key)
    if key == 'r' then
        timer:after(2, function() print(love.math.random()) end)
    end
end

If you add the code above to the main.lua file and run the project, after you press r a random number should appear on the screen with a delay. If you press r multiple times repeatedly, multiple numbers will appear with a delay in quick succession. But sometimes we want the behavior that if the event happens repeated times it should reset the timer and start counting from 0 again. This means that whenever we press r we want to cancel all previous timers created from when this event happened in the past. One way of doing this is to somehow store all handles created somewhere, bind them to an event identifier of some sort, and then call some cancel function on the event identifier itself which will cancel all timer handles associated with that event. This is what that solution looks like:

function love.keypressed(key)
    if key == 'r' then
        timer:after('r_key_press', 2, function() print(love.math.random()) end)
    end
end

I created an enhancement of the current timer module that supports the addition of event tags. So in this case, the event r_key_press is attached to the timer that is created whenever the r key is pressed. If the key is pressed multiple times repeatedly, the module will automatically see that this event has other timers registered to it and cancel those previous timers as a default behavior, which is what we wanted. If the tag is not used then it defaults to the normal behavior of the module.

You can download this enhanced version here and swap the timer import in main.lua from libraries/hump/timer to wherever you end up placing the EnhancedTimer.lua file, I personally placed it in libraries/enhanced_timer/EnhancedTimer. This also assumes that the hump library was placed inside the libraries folder. If you named your folders something different you must change the path at the top of the EnhancedTimer file. This is what the main.lua file is looking like right now in case you're lost:

Object = require 'libraries/classic/classic'
Timer = require 'libraries/enhanced_timer/EnhancedTimer'

function love.load()
    local object_files = {}
    recursiveEnumerate('objects', object_files)
    requireFiles(object_files)

    timer = Timer()
end

function love.update(dt)
    timer:update(dt)
end

function love.draw()

end

function love.keypressed(key)
    if key == 'r' then
        timer:after('r_key_press', 2, function() print(love.math.random()) end)
    end
end

Timer Exercises

1. Using only a for loop and one declaration of the after function inside that loop, print 10 random numbers to the screen with an interval of 0.5 seconds between each print.

2. Suppose we have the following code:

function love.load()
    timer = Timer()
    rect_1 = {x = 400, y = 300, w = 50, h = 200}
    rect_2 = {x = 400, y = 300, w = 200, h = 50}
end

function love.update(dt)
    timer:update(dt)
end

function love.draw()
    love.graphics.rectangle('fill', rect_1.x - rect_1.w/2, rect_1.y - rect_1.h/2, rect_1.w, rect_1.h)
    love.graphics.rectangle('fill', rect_2.x - rect_2.w/2, rect_2.y - rect_2.h/2, rect_2.w, rect_2.h)
end

Using only the tween function, tween the w attribute of the first rectangle over 1 second using the in-out-cubic tween mode. After that is done, tween the h attribute of the second rectangle over 1 second using the in-out-cubic tween mode. After that is done, tween both rectangles back to their original attributes over 2 seconds using the in-out-cubic tween mode. It should look like this:

3. For this exercise you should create an HP bar. Whenever the user presses the d key the HP bar should simulate damage taken. It should look like this:

As you can see there are two layers to this HP bar, and whenever damage is taken the top layer moves faster while the background one lags behind for a while.

4. Taking the previous example of the expanding and shrinking circle, it expands once and then shrinks once. How would you change that code so that it expands and shrinks continually forever?

5. Accomplish the results of the previous exercise using only the after function.

6. Bind the e key to expand the circle when pressed and the s to shrink the circle when pressed. Each new key press should cancel any expansion/shrinking that is still happening.

7. Suppose we have the following code:

function love.load()
    timer = Timer()
    a = 10  
end

function love.update(dt)
    timer:update(dt)
end

Using only the tween function and without placing the a variable inside another table, how would you tween its value to 20 over 1 second using the linear tween mode?


Timer Exercises HELP!

For the first question we need to print 10 random numbers with a 0.5 interval between each print using only a for and an after call inside that loop. The thing to do on instinct is something like this:

for i = 1, 10 do
    timer:after(0.5, function() print(love.math.random()) end)
end

But this will print 10 numbers exactly at the same time after an initial interval of 0.5 seconds, which is not what we wanted. What we did here is just call timer:after 10 times. Another thing one might try is to somehow chain after calls together like this:

timer:after(0.5, function()
    print(love.math.random())
    timer:after(0.5, function()
        print(love.math.random())
        ...
    end)
end)

And then somehow translate that into a for, but there's no reasonable way to do that. The solution lies in figuring out that if you use the i index from the loop and multiply that by the 0.5 delay, you'll get delays of 0.5, then 1, then 1.5, ... until you get to the last value of 5. And that looks like this:

for i = 1, 10 do
    timer:after(0.5*i, function() print(love.math.random()) end)
end

The first number is printed after 0.5 seconds and then the others follow. If we needed the first number to printed immediately (instead of with an initial 0.5 seconds delay) then we needed to use i-1 instead of i.


The second question asks for a bunch of tweens that happens in sequence after one another. This is just a simple application of the function using the optional last argument to chain tweens together. That looks like this:

timer:tween(1, rect_1, {w = 0}, 'in-out-cubic', function()
    timer:tween(1, rect_2, {h = 0}, 'in-out-cubic', function()
        timer:tween(2, rect_1, {w = 50}, 'in-out-cubic')
        timer:tween(2, rect_2, {h = 50}, 'in-out-cubic')
    end)
end)

Table Functions

Now for the final library I'll go over Yonaba/Moses which contains a bunch of functions to handle tables more easily in Lua. The documentation for it can be found here. By now you should be able to read through it and figure out how to install it and use it yourself.

But before going straight to exercises you should know how to print a table to the console and verify its values:

for k, v in pairs(some_table) do
    print(k, v)
end

So if we had a table defined with the following values:

a = {1, 3, 'LUL', 4, 'xD', 6, 7, 9, true, 12, 1, 1, a = 1, b = 2, c = 3, {1, 2, 3}}

When printed to the console using the code above it would look like this:


Table Exercises

For all exercises assume you have the following tables defined:

a = {1, 3, 'LUL', 4, 'xD', 6, 9, true, 12, 1, 1, a = 1, b = 2, c = 3, {1, 2, 3}}
b = {1, 4, 9, 10, 11, 1, 1, false}
c = {'LUL', 'xD', 'GODLUL', 7, 1, 7}
d = {4, 4, 9, 1, 2, 6}

You are also required to use only one function from the library per exercise unless explicitly told otherwise.

1. Print the contents of the a table to the console using the each function.

2. Count the number of 1 values inside the b table.

3. Add 1 to all the values of the d table using the map function.

4. Using the map function, apply the following transformations to the a table: if the value is a number, it should be doubled; if the value is a string, it should have 'xD' concatenated to it; if the value is a boolean, it should have its value flipped; and finally, if the value is a table it should be omitted.

5. Sum all the values of the d list. The result should be 26.

6. Suppose you have the following code:

if _______ then
    print('table contains the value 9')
end

Which function from the library should be used in the underscored spot to verify if the b table contains or doesn't contain the value 9?

7. Find the first index in which the value 7 is found in the c table.

8. Filter the d table so that only numbers lower than 5 remain.

9. Filter the c table so that only strings remain.

10. Check if all values of the c and d tables are numbers or not. It should return false for the first and true for the second.

11. Shuffle the d table randomly.

12. Reverse the d table.

13. Remove all occurrences of the values 1 and 4 from the d table.

14. Create a combination of the b, c and d tables that doesn't have any duplicates.

15. Find the common values between b and d tables.

16. Append the b table to the d table.


END

And with that this part of the tutorial is over. By now you should be more comfortable with how tables in Lua work in general and how to integrate other people's library into your project. For the next part I'll start focusing on some code that will provide the structure needed to build the game upon.

Timer and Tween Functions in Lua

Timer and tween functions are some of the most fundamental and useful functions for gameplay code in my opinion and so in this article I'll go over getting those properly implemented in Lua. If you use other languages you can probably use this article as a reference but some implementation details will likely have to be different.


Motivation

The standard way of doing something after n seconds in a game looks something like this:

timer_current = 0
timer_delay = 5

function update(dt)
    timer_current = timer_current + dt
    if timer_current > timer_delay then
        -- do the thing
    end
end

And so after 5 seconds from when timer_current was first set to 0 the action will be performed. Doing things this way is mostly fine but it's noisy and cumbersome and gets even more so when you want to do timers that are bit more involved, like chained actions. So the way we'll solve this is...


after

We'll use a function called after, which takes in a delay and a function, and then performs the function after the given delay. It looks like this:

after(2, function() print(1) end)

This simplifies the problem quite a bit because now we don't have to care about additional variables or updating things, we just say that we want this to happen and it will. In Lua this works fine because Lua allows for functions to be passed as arguments like this, but depending on your language this approach might be harder.

Either way, let's get into the details of implementing the after function. The high level way it will work is that we'll keep an internal table of all timers that have been registered (the after function could be called register too, and some timer libraries do call it something like that) and then we'll update all registered timers and perform their functions when appropriate.

timers = {}

function after(delay, action)
    table.insert(timers, {current_time = 0, delay = delay, action = action})
end

function update(dt)
    for _, timer in ipairs(timers) do
        -- update timer
    end
end

So here what we're doing is just that: adding a structure that contains the delay and the action to be performed to a timers table, and then updating each of those structures. What that update looks like would be something like this:

function update_timers(dt)
    for _, timer in ipairs(timers) do
        timer.current_time = timer.current_time + dt
        if timer.current_time > timer.delay then
            if timer.action then timer.action() end
        end
    end
end

And so in this way we have a simple after function that works. One simple example of it in use is this:

function Player:hit()
    self.just_hit = true
    after(1, function() self.just_hit = false end)
end

We have some kind of hit function that gets called when the player is hit, it sets some flag saying that the player was just hit to true, and then 1 second after that flag is set to false. This flag can then be used to make the player flash white, for instance.


Tags

One necessary thing that comes with this setup is a way to cancel existing timers. Think of what happens in the previous example when the player is hit once and then again after 0.3 seconds. The behavior we want is that just_hit is set to true again on the second hit, and 1 second after that second hit it's set to false, but what will actually happen is that as the 1 second is over for the first timer just_hit will be set to false, 0.3 seconds earlier than it should have been. Most of the time when I use timers in my game I actually want to cancel the previous timer that performs that same action.

We can solve this problem by identifying a timer by its tag (a string or number) and then whenever we add a new timer with a tag we automatically cancel any timer that already exists with the same tag:

function after(delay, action, tag)
    timers[tag] = {current_time = 0, delay = delay, action = action})
end

And so here we use the fact that tables in Lua can also be used as dictionaries and then we use the tag as the key, and the table we were creating previously as the value. Whenever we call this function with a tag defined we will overwrite the previous value set in timers[tag], and so we get our desired outcome. The way to call this now would be something like this:

function Player:hit()
    self.just_hit = true
    after(1, function() self.just_hit = false end, 'just_hit')

And so here this particular timer created in the player's hit function will always have the just_hit tag attached to it, meaning that any time a new timer is created here, a previous one that is currently running will be automatically cancelled.

There are all sorts of details to solve with this setup, like the fact that all tags have to be unique across multiple objects for this to work properly, or that tags can be optional so we have to change the after function slightly to deal with when the user doesn't provide a tag, and so on. I'm not sure if this is the ideal solution to this problem, but it's a problem that happens often enough and it's a solution that has worked well for me so far.


Repeatable after

Another type of behavior that's really useful is repeatable behavior. Say we want something to make the player flash really fast whenever he gets hit. One way to do it is being able to define repeatable behavior in the after function, something like this:

function Player:hit()
    self.just_hit = true
    after(0.04, function() self.just_hit = not self.just_hit end, true, 'just_hit')
end

And here the argument after the action is a boolean that will signal that the action should be repeated indefinitely. In this example, every 0.04 seconds just_hit will change between true and false, which means that in our draw function we can make the player appear and disappear for small amounts of time, which gives the desired "flash really fast" effect. In any case, the way this idea looks like implementation wise:

function after(delay, action, repeatable, tag)
    timers[tag] = {current_time = 0, delay = delay, action = action, repeatable = repeatable})
end

function update_timers(dt)
    for _, timer in pairs(timers) do
        timer.current_time = timer.current_time + dt
        if timer.current_time > timer.delay then
            if timer.action then timer.action() end
            if timer.repeatable then timer.current_time = 0 end    
        end
    end
end

Here the only differences are that we add the repeatable key to our timer structure, and then whenever we perform our action we check if this attribute is set to true, and if it is we set current_time to 0, which will restart the counter and make the action happen again after delay.

If we want to control how many times we should repeat an action we can make the repeatable attribute also be valid as a number, so, for instance, after(1, function() end, 5) would repeat the function every 1 second for a total of 5 times. The way this would look implemented would be something like this:

function update_timers(dt)
    for tag, timer in pairs(timers) do
        timer.current_time = timer.current_time + dt
        if timer.current_time > timer.delay then
            if timer.action then timer.action() end
            if timer.repeatable then 
                if type(timer.repeatable) == 'number' then
                    timer.repeatable = timer.repeatable - 1
                    if timer.repeatable >= 0 then timer.current_time = 0
                    else timers[tag] = nil end -- this timer is done, destroy it
                else timer.current_time = 0 end
            end    
        end
    end
end

Here the only additional thing we do is checking the type of the repeatable variable, if it is a number then we subtract 1 from it and then reset current_time, and if that number gets to 0 then we destroy this timer instead, since it has already repeated the action for the required amount of times. Destroying the timer can be as simple as doing timers[tag] = nil.

One last additional thing we can do is add an optional function that gets called when the repeatable timer is done. And so our function would have the following description after(delay, action, repeatable, after, tag), with the last 3 arguments being optional. One possible use of this, using the player hit example from above:

function Player:hit()
    self.just_hit = true
    after(0.04, function() self.just_hit = not self.just_hit end, 25, function() self.just_hit = false end, 'just_hit')
end

And so in this example the just_hit attribute will oscillate between true and false 25 times over 1 second (25*0.04), and then once that's done we'll make sure that it's set to false.


tween

Now for the next function which is the tween one. The way I've settled on this function generally is like this:

tween(2, self, {sx = 0, sy = 0}, 'cubic_in_out')

And so this will make the attributes self.sx and self.sy go to 0 over 2 seconds using the cubin_in_out tweening method. Because of the way Lua tables work we can very easily change attributes like this and so this became my preferred way of doing it. The implementation looks like this:

function tween(delay, target, source, method, after, tag)
    local initial_values = {}
    for k, _ in pairs(source) do initial_values[k] = target[k] end
    timers[tag] = {type = 'tween', current_time = 0, delay = delay, target = target, initial_values = initial_values, source = source, method = method, after = after, tag = tag}
end

The main difference here from the after function is that we need to get the initial values as they are on the source and store them in another table. So using the example above, initial_values will have the keys sx and sy as the values of self.sx and self.sy when the tween function was called. We need these initial values to perform the tween in the update function, which looks like this:

function update_timers(dt)
    for tag, timer in pairs(timers) do
        timer.current_time = timer.current_time + dt

        if timer.type == 'after' then
            ...
        elseif timer.type == 'tween' then
            local t = _G[timer.method](timer.current_time/timer.delay)
            for k, v in pairs(timer.source) do timer.target[k] = lerp(t, timer.initial_values[k], v) end
            if timer.current_time > timer.delay then
                if timer.after then timer.after() end
                timers[tag] = nil
            end
        end
    end
end

There are a number of things that are pretty Lua specific here so let's go step by step. The first line accesses _G[timer.method]. _G is Lua's global environment table, which holds a reference to all global variables. If you define a global function in Lua then you can also access it via the _G table. For instance, we just defined the tween function, and instead of calling tween(2, self, {sx = 0, sy = 0}, 'cubic_in_out') we could also do _G['tween'](2, self, {sx = 0, sy = 0}, 'cubic_in_out'). Those are exactly the same thing. Which means that the method attribute is being used as the name of some function we defined, in this case the tween method. In any tweening library there are numerous tween methods available, for instance, here's cubic_in_out:

function cubic_in_out(t)
    t = t*2
    if t < 1 then return 0.5*t*t*t
    else
        t = t - 2
        return 0.5*(t*t*t + 2)
    end
end

All these methods receive a value from 0 to 1 and return a value from 0 to 1 with some given curve being applied to it. The source code at the end of this article has all tween methods I defined, and you should be able to find these methods implemented in many languages. I got mine from this tween library which is written in Haxe, but this is pretty standard code that should be nearly the same across most tween library implementations.

In any case, we get the modified value from the function by passing in timer.current_time/timer.delay, which is how far along in our tween we are currently. For instance, if the delay is 5 and the current time is 2.5, then we currently are at 50%, which is 0.5 in a 0-1 range. After this we do the main work:

for k, v in pairs(timer.source) do timer.target[k] = lerp(t, timer.initial_values[k], v) end

Here we go over all keys and values in the source table, apply a lerp from the initial value of a particular key timer.initial_values[k] to the desired value v, and then apply that to timer.target[k].

So, for instance, in our previous example our source table was {sx = 0, sy = 0}, so first going through sx we'd be doing self.sx = lerp(t, initial value of sx, desired value of sx which is 0), and as t increases we get self.sx closer and closer to the desired value. We'd then repeat the same for sy given that we're going over all keys in timer.source. The code after this part simply runs a function after the tween is done if one is defined and that's it!


Examples

Having after and tween defined in this manner we can do lots of cool things with them. I'll go over some examples of how I've used them before.

Visualizations

In the previous article I wrote all visualizations are powered by the after function. This is what the visualization looks like:

And so, for instance, the code to generate the first part this (the physics circles) looks like this:

for i = 1, #radii do 
    create_physics_circle(game_width/2 + 4*(2*love.math.random()-1), game_height/2 + 4*(2*love.math.random()-1), table.remove(radii, love.math.random(1, #radii))) 
end

And if the code is left like this it simply generates all circles at the same time. But with a very small change like this:

for i = 1, #radii do 
    after((i-1)*0.05, function()
        create_physics_circle(game_width/2 + 4*(2*love.math.random()-1), game_height/2 + 4*(2*love.math.random()-1), table.remove(radii, love.math.random(1, #radii)))
    end) 
end

We're making it so that we generate one circle every 0.05 seconds. And that looks like this:

All the next steps follow the same idea, where I simply wrapped action being done into an after call that is offset properly based on the index of the for loop. The fact that it's so easy to do this also means it's a great way to debug these algorithms that can get a bit opaque and blackboxy after a while.


Event scale

These functions are very useful when juicing up your game in general but one particular use that's easy to get across is scaling entities on important enough events. The way it goes is roughly like this:

function Player:hit()
    self.sx, self.sy = 1.4, 1.4
    tween(0.15, self, {sx = 1, sy = 1}, 'cubic_in_out', nil, 'hit_scale')
end

The player's sx and sy variables are naturally at 1, since the player isn't being scaled in any way by default. But when he gets hit, they will go up to 1.4 and then decay down back to 1 over a small amount of time, like 0.15 seconds. This gives the hit a bit more oomph and makes it feel nicer. The same idea can be applied to when the player attack, or when enemies get hit, or when a new item is acquired, and so on.


Chained timers

Some more complicated uses involve lots of chained timers and cancelling of previous timers on weird conditions and so on. About 2 years ago I was playing around with doing some animations using timers only and one example of it can be seen here:

This looks fairly simple but the code for it looks something like this:

And it just keeps going and going. But as you can see, there's a fair bit of use of the after and tween functions to do various things. If I were to do this again today I'd probably not choose this route but it shows that these two functions alone can create some OK things with some creative use.


Source code

The source code for what was implemented in this article can be found here and it's implemented in Lua. Most of these ideas can carry over to most modern languages without significant drawbacks, but for some languages you might wanna choose something different altogether because the drawbacks outweigh the benefits. Good luck!

How I made my own 2D game engine in under 2 months

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:

bool running = true;

int main(int argc, const char *argv[]) {
    GPU_SetDebugLevel(GPU_DEBUG_LEVEL_MAX);
    SDL_Init(SDL_INIT_EVERYTHING);

    GPU_Target *screen = GPU_Init(screen_width, screen_height, GPU_DEFAULT_INIT_FLAGS);

    // Load game

    // Main loop
    SDL_Event event;
    while (running) {

        // Handle events
        while (SDL_PollEvent(&event)) {
            ...
        }

        // Update game

        GPU_Clear(screen);
        // Draw game
        GPU_Flip(screen);
        SDL_Delay(1);
    }

    GPU_Quit();
    return 0;
}

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 functions love.load, love.update and love.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 and Draw game will be replaced by Lua functions aika_load, aika_update and aika_draw. This ends up looking like this:

static int traceback(lua_State *L) {
    lua_getfield(L, LUA_GLOBALSINDEX, "debug");
    lua_getfield(L, -1, "traceback");
    lua_pushvalue(L, 1);
    lua_pushinteger(L, 2);
    lua_call(L, 2, 1);
    return 1;
}

int main(int argc, const char *argv[]) {
    GPU_SetDebugLevel(GPU_DEBUG_LEVEL_MAX);
    SDL_Init(SDL_INIT_EVERYTHING);

    // Create Lua VM and load aika.lua
    lua_State *L;
    L = luaL_newstate();
    luaL_openlibs(L);
    int rv = luaL_loadfile(L, "aika.lua");
    if (rv) {
        fprintf(stderr, "%s\n", lua_tostring(L, -1));
        return rv;
    } else {
        lua_pcall(L, 0, 0, lua_gettop(L) - 1);
    }

    GPU_Target *screen = GPU_Init(screen_width, screen_height, GPU_DEFAULT_INIT_FLAGS);

    // Call aika_load()
    lua_pushcfunction(L, traceback);
    lua_getglobal(L, "aika_load");
    if (lua_pcall(L, 0, 0, lua_gettop(L) - 1) != 0) {
        const char *error = lua_tostring(L, -1);
        lua_getglobal(L, "aika_error");
        lua_pushstring(L, error);
        lua_pcall(L, 1, 0, 0);
    }
    timer_now = SDL_GetPerformanceCounter();

    // Main loop
    SDL_Event event;
    while (running) {

        // Handle events
        while (SDL_PollEvent(&event)) {
            ...
        }

        // Call aika_update(dt)
        lua_pushcfunction(L, traceback);
        lua_getglobal(L, "aika_update");
        lua_pushnumber(L, timer_fixed_dt);
        if (lua_pcall(L, 1, 0, lua_gettop(L) - 2) != 0) {
            const char *error = lua_tostring(L, -1);
            lua_getglobal(L, "aika_error");
            lua_pushstring(L, error);
            lua_pcall(L, 1, 0, 0);
        }

        GPU_Clear(screen);

        // Call aika_draw()
        lua_pushcfunction(L, traceback);
        lua_getglobal(L, "aika_draw");
        if (lua_pcall(L, 0, 0, lua_gettop(L) - 1) != 0) {
            const char *error = lua_tostring(L, -1);
            lua_getglobal(L, "aika_error");
            lua_pushstring(L, error);
            lua_pcall(L, 1, 0, 0);
        }

        GPU_Flip(screen);
        SDL_Delay(1);
    }

    GPU_Quit();
    lua_close(L);
    return 0;
}

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 of aika_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 to lua_pcall which gets called whenever an error occurs. This in turn calls a Lua function called aika_error, which is also defined in aika.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:

function aika_error(error_message)
    print(error_message)
    os.exit()
end

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:

function aika_load()

end

function aika_update(dt)

end

function aika_draw()

end

function aika_error(error_message)
    print(error_message)
    os.exit()
end

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:

// Update timer
timer_last = timer_now;
timer_now = SDL_GetPerformanceCounter();
timer_dt = ((timer_now - timer_last)/(double)SDL_GetPerformanceFrequency());

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 use SDL_GetPerformanceFrequency to translate these values into actual seconds.

Before the loop starts I initialize all timer values like this:

// Initiate timer
timer_now = SDL_GetPerformanceCounter();
timer_last = 0;
timer_dt = 0;
timer_fixed_dt = 1.0/60.0;
timer_accumulator = 0;

And then change the update function like this:

// This timing method is the 4th (Free the Physics) from this article: https://gafferongames.com/post/fix_your_timestep/
timer_accumulator += timer_dt;
while (timer_accumulator >= timer_fixed_dt) {
    // Call aika_update(dt)
    ...

    timer_accumulator -= timer_fixed_dt;
}

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:

function update()
    if input:pressed('a') then
        print('a was pressed')
    end
end

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:

// Main loop
SDL_Event event;
while (running) {

    // Handle all events, making sure previous and current input states are updated properly
    memcpy(input_previous_keyboard_state, input_current_keyboard_state, 512);
    memcpy(input_previous_gamepad_button_state, input_current_gamepad_button_state, 24);
    memcpy(input_previous_mouse_state, input_current_mouse_state, 12);
    memset(input_current_mouse_state, 0, 12);
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) running = false;
        if (event.type == SDL_MOUSEBUTTONDOWN) {
            if (event.button.button == SDL_BUTTON_LEFT) input_current_mouse_state[MOUSE_1] = 1;
            if (event.button.button == SDL_BUTTON_RIGHT) input_current_mouse_state[MOUSE_2] = 1;
            if (event.button.button == SDL_BUTTON_MIDDLE) input_current_mouse_state[MOUSE_3] = 1;
            if (event.button.button == SDL_BUTTON_X1) input_current_mouse_state[MOUSE_4] = 1;
            if (event.button.button == SDL_BUTTON_X2) input_current_mouse_state[MOUSE_5] = 1;
        }
        else if (event.type == SDL_MOUSEWHEEL) {
            if (event.wheel.x == 1) input_current_mouse_state[MOUSE_WHEEL_RIGHT] = 1;
            if (event.wheel.x == -1) input_current_mouse_state[MOUSE_WHEEL_LEFT] = 1;
            if (event.wheel.y == 1) input_current_mouse_state[MOUSE_WHEEL_DOWN] = 1;
            if (event.wheel.y == -1) input_current_mouse_state[MOUSE_WHEEL_UP] = 1;
        }
    }
    input_current_keyboard_state = SDL_GetKeyboardState(NULL);
    for (int i = SDL_CONTROLLER_BUTTON_INVALID; i < SDL_CONTROLLER_BUTTON_MAX; i++) input_current_gamepad_button_state[i] = SDL_GameControllerGetButton(controller, i);
    for (int i = SDL_CONTROLLER_AXIS_INVALID; i < SDL_CONTROLLER_AXIS_MAX; i++) input_gamepad_axis_state[i] = SDL_GameControllerGetAxis(controller, i);
    ...

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 and input_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 contents input_current_keyboard_state to input_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 the SDL_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:

aika_input_is_pressed(key);
aika_input_is_released(key);
aika_input_is_down(key);
aika_input_get_axis(key);
aika_input_get_mouse_position();

The way one of these functions would be use in Lua would be like this:

function aika_update(dt)
    if aika_input_is_pressed('a') then
        print(1)
    end
end

And whenever a was pressed, 1 would be printed to the console.

One of these functions partially looks like this:

static int aika_input_is_pressed(lua_State *L) {
    const char *key = luaL_checkstring(L, 1);

    SDL_Keycode *keycode = map_get(&input_keyboard_map, key);
    if (keycode) {
        SDL_Scancode scancode = SDL_GetScancodeFromKey(*keycode);
        if (input_current_keyboard_state[scancode] && !input_previous_keyboard_state[scancode]) lua_pushboolean(L, 1);
        else lua_pushboolean(L, 0);
    }
    else {
        SDL_Log("Invalid key '%s' in aika_input_is_pressed\n", key);
        exit(1);
    }
    return 1;
}

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 the key variable. Then from that key we get the SDL_Keycode value from a map, which is initialized in the main function like this:

map_init(&input_keyboard_map);
map_set(&input_keyboard_map, "a", SDLK_a);
map_set(&input_keyboard_map, "b", SDLK_b);
...

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 the input_current_keyboard_state and input_previous_keyboard_state arrays. In the case of is_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:

static int aika_input_is_pressed(lua_State *L) {
    const char *key = luaL_checkstring(L, 1);

    if (strstr(key, "gamepad") != NULL) {
        SDL_GameControllerButton *button = map_get(&input_gamepad_button_map, key);
        if (button) {
            if (input_current_gamepad_button_state[*button] && !input_previous_gamepad_button_state[*button]) lua_pushboolean(L, 1);
            else lua_pushboolean(L, 0);
        }
        else {
            SDL_Log("Invalid gamepad button '%s' in aika_input_is_pressed\n", key);
            exit(1);
        }
        return 1;
    }

    else if (strstr(key, "mouse") != NULL) {
        SDL_MouseButton *button = map_get(&input_mouse_map, key);
        if (button) {
            if (input_current_mouse_state[*button] && !input_previous_mouse_state[*button]) lua_pushboolean(L, 1);
            else lua_pushboolean(L, 0);
        }
        else {
            SDL_Log("Invalid mouse button '%s' in aika_input_is_pressed\n", key);
            exit(1);
        }
        return 1;
    } 

    else {
        SDL_Keycode *keycode = map_get(&input_keyboard_map, key);
        if (keycode) {
            SDL_Scancode scancode = SDL_GetScancodeFromKey(*keycode);
            if (input_current_keyboard_state[scancode] && !input_previous_keyboard_state[scancode]) lua_pushboolean(L, 1);
            else lua_pushboolean(L, 0);
        }
        else {
            SDL_Log("Invalid key '%s' in aika_input_is_pressed\n", key);
            exit(1);
        }
        return 1;
    }
}

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:

lua_register(L, "aika_input_is_pressed", aika_input_is_pressed);

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:

input:bind(key, action)
input:unbind(key)
input:unbind_all()
input:pressed(action)
input:released(action)
input:down(action, interval, delay)
input:update()

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 a GPU_Target, which is the equivalent of a Canvas 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:

static int aika_graphics_draw_circle(lua_State *L) {
    GPU_Image *image = (GPU_Image *)lua_touserdata(L, 1);
    double x = luaL_checknumber(L, 2);
    double y = luaL_checknumber(L, 3);
    double r = luaL_checknumber(L, 4);

    GPU_Circle(image->target, x, y, r, graphics_main_color);
    return 0;
}

And the function in Lua looks like:

graphics.circle = function(render_target, x, y, r)
    aika_graphics_draw_circle(render_target.pointer, x, y, r)
end

Now, for this to work I also needed to create functions to create a new render target:

static int aika_graphics_create_render_target(lua_State *L) {
    double w = luaL_checknumber(L, 1);
    double h = luaL_checknumber(L, 2);

    GPU_Image *image = GPU_CreateImage(w, h, GPU_FORMAT_RGBA);
    GPU_SetImageFilter(image, graphics_main_filter);
    GPU_LoadTarget(image);
    lua_pushlightuserdata(L, image);
    return 1;
}

Using creating a GPU_Image using GPU_CreateImage and then creating the target on that image using GPU_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 with lua_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:

graphics.new_target = function(w, h)
    local rt = {type = 'render_target', w = w, h = h, pointer = aika_graphics_create_render_target(w, h)}
    table.insert(graphics.targets, rt)
    return rt
end

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:

aika_graphics_set_screen_size(w, h);
aika_graphics_get_screen_width();
aika_graphics_get_screen_height();
aika_graphics_set_color(r, g, b, a);
aika_graphics_set_background_color(r, g, b);
aika_graphics_get_color();
aika_graphics_get_background_color();
aika_graphics_set_filter(filter_mode);
aika_graphics_draw_circle(render_target, x, y, r);
aika_graphics_draw_rectangle(render_target, x1, y1, x2, y2);
aika_graphics_draw_line(render_target, x1, y1, x2, y2);
aika_graphics_create_render_target(w, h);
aika_graphics_draw_to_screen(render_target, x, y, px, py, r, sx, sy);
aika_graphics_draw_to_render_target(dst_render_target, src_image, x, y, px, py, r, sx, sy);
aika_graphics_clear_render_target(render_target);
aika_graphics_load_image(filename);
aika_graphics_get_image_size();

The only additional thing to note here is the difference between draw_to_screen and draw_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:

aika_graphics_set_shader(shader);
aika_graphics_create_shader(filename);
aika_graphics_send_float_to_shader(shader, argument_name, value);
aika_graphics_send_vec4_to_shader(shader, argument_name, value);

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 the graphics.send_to_shader function will happen on an as needed basis.

As for the basics of how the implementation here looks:

static int aika_graphics_create_shader(lua_State *L) {
    const char *filename = luaL_checkstring(L, 1);

    Uint32 v = GPU_LoadShader(GPU_VERTEX_SHADER, "default.vert");
    if (!v) GPU_LogError("Failed to load vertex shader: %s\n", GPU_GetShaderMessage());
    Uint32 f = GPU_LoadShader(GPU_FRAGMENT_SHADER, filename);
    if (!f) GPU_LogError("Failed to load fragment shader: %s\n", GPU_GetShaderMessage());
    Uint32 p = GPU_LinkShaders(v, f);
    if (!p) GPU_LogError("Failed to link shader program: %s\n", GPU_GetShaderMessage());

    graphics_shader_blocks[graphics_shader_index] = GPU_LoadShaderBlock(p, "gpu_Vertex", "gpu_TexCoord", "gpu_Color", "gpu_ModelViewProjectionMatrix");
    graphics_shader_programs[graphics_shader_index] = p;
    lua_pushnumber(L, graphics_shader_index);
    graphics_shader_index++;
    return 1;
}

Here I'm taking a filename, and then vertex and fragment shaders together using GPU_LinkShaders. I'm using a default.vert file for the vertex shader that looks like this:

attribute vec3 gpu_Vertex;
attribute vec2 gpu_TexCoord;
attribute vec4 gpu_Color;
uniform mat4 gpu_ModelViewProjectionMatrix;

varying vec4 color;
varying vec2 texCoord;

void main(void) {
    color = gpu_Color;
    texCoord = vec2(gpu_TexCoord);
    gl_Position = gpu_ModelViewProjectionMatrix*vec4(gpu_Vertex, 1.0);
}

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:

varying vec2 texCoord;

uniform sampler2D tex;
uniform vec4 color;

void main(void) {
    gl_FragColor = texture2D(tex, texCoord) * color;
}

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:

main_shader = graphics.new_shader("default.frag")
graphics.set_shader(main_shader)
graphics.send_to_shader(main_shader, "color", {graphics.get_color()})

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:

graphics.set_color(1, 1, 1, 0.5)
graphics.draw(screen, box_shadow, math.floor(box.x), math.floor(box.y + 3), 0, 1.1, 1, box_shadow.w/2, box_shadow.h/2)
graphics.set_color(1, 1, 1, 1)

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:

pushRotateScale(x + 10, y + 10, r, sx, sy)
love.graphics.draw(something, x, y, r, sx, sy, ox, oy)
love.graphics.draw(something_else, x + 20, y + 20, r, sx, sy, ox, oy)
love.graphics.pop()

Where pushRotateScale looks like this:

function pushRotateScale(x, y, r, sx, sy)
    love.graphics.push()
    love.graphics.translate(x, y)
    love.graphics.rotate(r or 0)
    love.graphics.scale(sx or 1, sy or sx or 1)
    love.graphics.translate(-x, -y)
end

This allows me to rotate and scale something and something_else around their midpoint, while also rotating each of them locally around their ox, 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:

aika_graphics_push();
aika_graphics_push_translate_rotate_scale(x, y, r, sx, sy);
aika_graphics_pop();
aika_graphics_translate(x, y);
aika_graphics_rotate(r);
aika_graphics_scale(sx, sy);

And luckily these are very easy to get working because of SDL-gpu, here's an example of aika_graphics_push_translate_rotate_scale:

static int aika_graphics_push_translate_rotate_scale(lua_State *L) {
    double x = luaL_checknumber(L, 1);
    double y = luaL_checknumber(L, 2);
    double r = luaL_checknumber(L, 3);
    double sx = luaL_checknumber(L, 4);
    double sy = luaL_checknumber(L, 5);

    GPU_MatrixMode(GPU_MODELVIEW);
    GPU_PushMatrix();
    GPU_Translate(x, y, 0.0f);
    GPU_Rotate(r, 0.0f, 0.0f, 1.0f);
    GPU_Scale(sx, sy, 1.0f);
    GPU_Translate(-x, -y, 0.0f);
    return 0;
}

And in Lua this function looks like this:

graphics.push = function(x, y, r, sx, sy)
    if x then aika_graphics_push_translate_rotate_scale(x, y, r or 0, sx or 1, sy or 1)
    else aika_graphics_push() end
end

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:

aika_graphics_load_font(filename);
aika_graphics_draw_text_solid(render_target, font, text, x, y, r, sx, sy);
aika_graphics_draw_text_blended(render_target, font, text, x, y, r, sx, sy);
aika_graphics_set_font_style(font, style);
aika_graphics_get_font_style();
aika_graphics_set_font_outline(font, outline_thickness);
aika_graphics_get_font_outline();
aika_graphics_get_font_height();
aika_graphics_get_text_width(font, text);

And here's what one of the draw function looks like:

static int aika_graphics_draw_text_blended(lua_State *L) {
    GPU_Image *target = (GPU_Image *)lua_touserdata(L, 1);
    TTF_Font *font = lua_touserdata(L, 2);
    const char *text = luaL_checkstring(L, 3);
    double x = luaL_checknumber(L, 4);
    double y = luaL_checknumber(L, 5);
    double r = luaL_checknumber(L, 6);
    double sx = luaL_checknumber(L, 7);
    double sy = luaL_checknumber(L, 8);

    SDL_Surface *surface = TTF_RenderUTF8_Blended(font, text, graphics_main_color);
    GPU_Image *image = GPU_CopyImageFromSurface(surface);
    GPU_BlitTransformX(image, NULL, target->target, x, y, image->w/2, image->h/2, r, sx, sy);
    SDL_FreeSurface(surface);
    return 0;
}

The TTF_RenderUTF8_Blended function returns an SDL_Surface, but if we want to draw this using SDL-gpu then we need to use a GPU_Image. Luckily, SDL-gpu provides an easy way to transform one into the other with GPU_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:

graphics.set_color(0, 0, 0, 1)
graphics.draw_text(screen, font, "font test!", 200, 50)
graphics.set_font_style(font, BOLD + ITALIC + UNDERLINE)
graphics.draw_text(screen, font, "font test!", 200, 125)
graphics.set_font_style(font, NORMAL)
graphics.set_font_outline(font, 1)
graphics.draw_text(screen, font, "font test!", 200, 200)
graphics.set_font_outline(font, 0)
graphics.set_color(1, 1, 1, 1)

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:

aika_audio_load_sound(filename);
aika_audio_load_music(filename);
aika_audio_play_sound(sound);
aika_audio_play_music(music);
aika_audio_set_volume(volume);

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:

aika_utils_get_random -> love.math.random

aika_timer_get_delta -> love.timer.getDelta
aika_timer_get_time -> love.timer.getTime

aika_input_is_pressed -> love.key/mouse/gamepadpressed callback
aika_input_is_released -> love.key/mouse/gamepadreleased callback
aika_input_is_down -> love.keyboard/mouse/joystick.isDown
aika_input_get_axis -> love.joystick.getGamepadAxis
aika_input_get_mouse_position -> love.mouse.getPosition

aika_graphics_set_screen_size -> love.window.setMode
aika_graphics_get_screen_width -> love.window.getWidth
aika_graphics_get_screen_height -> love.window.getHeight
aika_graphics_set_color -> love.graphics.setColor
aika_graphics_set_background_color -> love.graphics.setBackgroundColor
aika_graphics_get_color -> love.graphics.getColor
aika_graphics_get_background_color -> love.graphics.getBackgroundColor
aika_graphics_set_filter -> love.graphics.setFilter
aika_graphics_draw_circle -> love.graphics.circle
aika_graphics_draw_rectangle -> love.graphics.rectangle
aika_graphics_draw_line -> love.graphics.line
aika_graphics_create_render_target -> love.graphics.newCanvas
aika_graphics_draw_to_screen -> love.graphics.draw
aika_graphics_draw_to_render_target -> love.graphics.draw
aika_graphics_clear_render_target -> love.graphics.clear
aika_graphics_load_image -> love.graphics.newImage
aika_graphics_get_image_size -> Image:getWidth, Image:getHeight
aika_graphics_push -> love.graphics.push
aika_graphics_pop -> love.graphics.pop
aika_graphics_push_translate_rotate_scale -> no equivalent
aika_graphics_translate -> love.graphics.translate
aika_graphics_rotate -> love.graphics.rotate
aika_graphics_scale -> love.graphics.scale
aika_graphics_set_shader -> love.graphics.setShader
aika_graphics_create_shader -> love.graphics.newShader
aika_graphics_send_float_to_shader -> Shader:send
aika_graphics_send_vec4_to_shader -> Shader:send
aika_graphics_set_line_width -> love.graphics.setLineWidth
aika_graphics_get_line_width -> love.graphics.getLineWidth
aika_graphics_load_font -> love.graphics.newFont
aika_graphics_draw_text_solid -> love.graphics.print
aika_graphics_draw_text_blended -> love.graphics.print
aika_graphics_set_font_style -> no equivalent
aika_graphics_get_font_style -> no equivalent
aika_graphics_set_font_outline -> no equivalent
aika_graphics_get_font_outline -> no equivalent
aika_graphics_get_font_height -> font:getHeight
aika_graphics_get_text_width -> font:getWidth

aika_audio_load_sound -> love.audio.newSource
aika_audio_load_music -> love.audio.newSource
aika_audio_play_sound -> love.audio.play
aika_audio_play_music -> love.audio.play
aika_audio_set_volume -> love.audio.setVolume

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:

Game~!!! #screenshotsaturday #gamedev #indiedev #indiegame pic.twitter.com/tmp8oCZGd4

— adn (@SSYGEN) February 4, 2017
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

BYTEPATH #4 - Exercises

In the previous three tutorials we went over a lot of code that didn't have anything to do directly with the game. All of that code can be used independently of the game you're making which is why I call it the engine code in my head, even though I guess it's not really an engine. As we make more progress in the game I'll constantly be adding more and more code that falls into that category and that can be used across multiple games. If you take anything out of these tutorials that code should definitely be it and it has been extremely useful to me over time.

Before moving on to the next part where we'll start with the game itself you need to be comfortable with some of the concepts taught in the previous tutorials, so here are some more exercises.


Exercises

52. Create a getGameObjects function inside the Area class that works as follows:

-- Get all game objects of the Enemy class
all_enemies = area:getGameObjects(function(e)
    if e:is(Enemy) then
        return true
    end
end)

-- Get all game objects with over 50 HP
healthy_objects = area:getGameObjects(function(e)
    if e.hp and e.hp >= 50 then
        return true
    end
end)

It receives a function that receives a game object and performs some test on it. If the result of the test is true then the game object will be added to the table that is returned once getGameObjects is fully run.

53. What is the value in a, b, c, d, e, f and g?

a = 1 and 2
b = nil and 2
c = 3 or 4
d = 4 or false
e = nil or 4
f = (4 > 3) and 1 or 2
g = (3 > 4) and 1 or 2

54. Create a function named printAll that receives an unknown number of arguments and prints them all to the console. printAll(1, 2, 3) will print 1, 2 and 3 to the console and printAll(1, 2, 3, 4, 5, 6, 7, 8, 9) will print from 1 to 9 to the console, for instance. The number of arguments passed in is unknown and may vary.

55. Similarly to the previous exercise, create a function named printText that receives an unknown number of strings, concatenates them all into a single string and then prints that single string to the console.

56. How can you trigger a garbage collection cycle?

57. How can you show how much memory is currently being used up by your Lua program?

58. How can you trigger an error that halts the execution of the program and prints out a custom error message?

59. Create a class named Rectangle that draws a rectangle with some width and height at the position it was created. Create 10 instances of this class at random positions of the screen and with random widths and heights. When the d key is pressed a random instance should be deleted from the environment. When the number of instances left reaches 0, another 10 new instances should be created at random positions of the screen and with random widths and heights.

60. Create a class named Circle that draws a circle with some radius at the position it was created. Create 10 instances of this class at random positions of the screen with random radius, and also with an interval of 0.25 seconds between the creation of each instance. After all instances are created (so after 2.5 seconds) start deleting once random instance every [0.5, 1] second (a random number between 0.5 and 1). After all instances are deleted, repeat the entire process of recreation of the 10 instances and their eventual deletion. This process should repeat forever.

61. Create a queryCircleArea function inside the Area class that works as follows:

-- Get all objects of class 'Enemy' and 'Projectile' in a circle of 50 radius around point 100, 100
objects = area:queryCircleArea(100, 100, 50, {'Enemy', 'Projectile'})

It receives an x, y position, a radius and a list of strings containing names of target classes. Then it returns all objects belonging to those classes inside the circle of radius radius centered in position x, y.

62. Create a getClosestGameObject function inside the Area class that works follows:

-- Get the closest object of class 'Enemy' in a circle of 50 radius around point 100, 100
closest_object = area:getClosestObject(100, 100, 50, {'Enemy'})

It receives the same arguments as the queryCircleArea function but returns only one object (the closest one) instead.

63. How would you check if a method exists on an object before calling it? And how would you check if an attribute exists before using its value?

64. Using only one for loop, how can you write the contents of one table to another?

BYTEPATH #9 - Director and Gameplay Loop

Introduction

In this article we'll finish up the basic implementation of the entire game with a minimal amount of content. We'll go over the Director, which is the code that will handle spawning of enemies and resources. Then we'll go over restarting the game once the player dies. And after that we'll take care of a basic score system as well as some basic UI so that the player can tell what his stats are.


Director

The Director is the piece of code that will control the creation of enemies, attacks and resources in the game. The goal of the game is to survive as long as possible and get as high a score as possible, and the challenge comes from the ever increasing number and difficulty of enemies that are spawned. This difficulty will be controlled entirely by the code that we will start writing now.

The rules of that the director will follow are somewhat simple:

  1. Every 22 seconds difficulty will go up;

  2. In the duration of each difficulty enemies will be spawned based on a point system:

    • Each difficulty (or round) has a certain amount of points available to be used;
    • Enemies cost a fixed amount of points (harder enemies cost more);
    • Higher difficulties have a higher amount of points available;
    • Enemies are chosen to be spawned along the round's duration randomly until it runs out of points.
  3. Every 16 seconds a resource (HP, SP or Boost) will be spawned;

  4. Every 30 seconds an attack will be spawned.


We'll start by creating the Director object, which is just a normal object (not one that inherits from GameObject to be used in an Area) where we'll place our code:

Director = Object:extend()

function Director:new(stage)
    self.stage = stage
end

function Director:update(dt)
  
end

We can create this and then instantiate it in the Stage room like this:

function Stage:new()
    ...
    self.director = Director(self)
end

function Stage:update(dt)
    self.director:update(dt)
    ...
end

We want the Director object to have a reference to the Stage room because we'll need it to spawn enemies and resources, and the only way to do that is through stage.area. The director will also have timing needs so it will need to be updated accordingly.

To start with rule 1, we can just define a simple difficulty attribute and a few extra ones to handle the timing of when that attribute goes up. This timing code will be just like the one we did for the Player's boost or cycle mechanisms.

function Director:new(...)
    ...

    self.difficulty = 1
    self.round_duration = 22
    self.round_timer = 0
end

function Director:update(dt)
    self.round_timer = self.round_timer + dt
    if self.round_timer > self.round_duration then
        self.round_timer = 0
        self.difficulty = self.difficulty + 1
        self:setEnemySpawnsForThisRound()
    end
end

And so difficulty goes up every 22 seconds, according to how we described rule 1. Additionally, here we also call a function called setEnemySpawnsForThisRound, which is essentially where rule 2 will take place.

The first part of rule 2 is that every difficulty has a certain amount of points to spend. The first thing we need to figure out here is how many difficulties we want the game to have and if we want to define all these points manually or through some formula. I decided to do the later and say that the game essentially is infinite and gets harder and harder until the player won't be able to handle it anymore. So for the this purpose I decided that the game would have 1024 difficulties since it's a big enough number that it's very unlikely anyone will hit it.

The way the amount of points each difficulty has will be define through a simple formula that I arrived at through trial and error seeing what felt best. Again, this kind of stuff is more on the design side of things so I don't want to spend much time on my reasoning, but you should try your own ideas here if you feel like you can do something better.

The way I decided to do is was through this formula:

  • Difficulty 1 has 16 points;
  • From difficulty 2 onwards the following formula is followed on a 4 step basis:
    • Difficulty i has difficulty i-1 points + 8
    • Difficulty i+1 has difficulty i points
    • Difficulty i+2 has difficulty (i+1)/1.5
    • Difficulty i+3 has difficulty (i+2)*2

In code that looks like this:

function Director:new(...)
    ...
  
    self.difficulty_to_points = {}
    self.difficulty_to_points[1] = 16
    for i = 2, 1024, 4 do
        self.difficulty_to_points[i] = self.difficulty_to_points[i-1] + 8
        self.difficulty_to_points[i+1] = self.difficulty_to_points[i]
        self.difficulty_to_points[i+2] = math.floor(self.difficulty_to_points[i+1]/1.5)
        self.difficulty_to_points[i+3] = math.floor(self.difficulty_to_points[i+2]*2)
    end
end

And so, for instance, for the first 14 difficulties the amount of points they will have looks like this:

Difficulty - Points
1 - 16
2 - 24
3 - 24
4 - 16
5 - 32
6 - 40
7 - 40
8 - 26
9 - 56
10 - 64
11 - 64
12 - 42
13 - 84

And so what happens is that at first there's a certain level of points that lasts for about 3 rounds, then it goes down for 1 round, and then it spikes a lot on the next round that becomes the new plateau that lasts for ~3 rounds and then this repeats forever. This creates a nice "normalization -> relaxation -> intensification" loop that feels alright to play around.

The way points increase also follows a pretty harsh and fast rule, such that at difficulty 40 for instance a round will be composed of around 400 points. Since enemies spend a fixed amount of points and each round must spend all points its given, the game quickly becomes overwhelming and so at some point players won't be able to win anymore, but that's fine since it's how we're designing the game and it's a game about getting the highest score possible essentially given these circumstances.

Now that we have this sorted we can try to go for the second part of rule 2, which is the definition of how much each enemy should cost. For now we only have two enemies implemented so this is rather trivial, but we'll come back to fill this out more in another article after we've implemented more enemies. What it can look like now is this though:

function Director:new(...)
    ...
    self.enemy_to_points = {
        ['Rock'] = 1,
        ['Shooter'] = 2,
    }
end

This is a simple table where given an enemy name, we'll get the amount of points it costs to spawn it.

The last part of rule 2 has to do with the implementation of the setEnemySpawnsForThisRound function. But before we get to that I have to introduce a very important construct we'll use throughout the game whenever chances and probabilities are involved.


ChanceList

Let's say you want X to happen 25% of the time, Y to happen 25% of the time and Z to happen 50% of the time. The normal way you'd do this is just use a function like love.math.random, have it generate a value between 1 and 100 and then see where this number lands. If it lands below 25 we say that X event will happen, if it lands between 25 and 50 we say that Y event will happen, and if it lands above 50 then Z event will happen.

The big problem with doing things this way though is that we can't ensure that if we run love.math.random 100 times, X will happen actually 25 times, for instance. If we run it 10000 times maybe it will approach that 25% probability, but often times we want to have way more control over the situation than that. So a simple solution is to create what I call a chanceList.

The way chanceLists work is that you generate a list with values between 1 and 100. Then whenever you want to get a random value on this list you call a function called next. This function will give you a random number in it, let's say it gives you 28. This means that Y event happened. The difference is that once we call that function, we will also remove the random number chosen from the list. This essentially means that 28 can never happen again and that event Y now has a slightly lower chance of happening than the other 2 events. As we call next more and more, the list will get more and more empty and then when it gets completely empty we just regenerate the 100 numbers again.

In this way, we can ensure that event X will happen exactly 25 times, that event Y will happen exactly 25 times, and that event Z will happen exactly 50 times. We can also make it so that instead of it generating 100 numbers, it will generate 20 instead. And so in that case event X would happen 5 times, Y would happen 5 times, and Z would happen 10 times.

The way the interface for this idea works is rather simple looks like this:

events = chanceList({'X', 25}, {'Y', 25}, {'Z', 50})
for i = 1, 100 do
    print(events:next()) --> will print X 25 times, Y 25 times and Z 50 times
end
events = chanceList({'X', 5}, {'Y', 5}, {'Z', 10})
for i = 1, 20 do
    print(events:next()) --> will print X 5 times, Y 5 times and Z 10 times
end
events = chanceList({'X', 5}, {'Y', 5}, {'Z', 10})
for i = 1, 40 do
    print(events:next()) --> will print X 10 times, Y 10 times and Z 20 times
end

We will create the chanceList function in utils.lua and we will make use of some of Lua's features in this that we covered in tutorial 2. Make sure you're up to date on that!

The first thing we have to realize is that this function will return some kind of object that we should be able to call the next function on. The easiest way to achieve that is to just make that object a simple table that looks like this:

function chanceList(...)
    return {
        next = function(self)

        end
    }
end

Here we are receiving all the potential definitions for values and chances as ... and we'll handle those in more details soon. Then we're returning a table that has a function called next in it. This function receives self as its only argument, since as we know, calling a function using : passes itself as the first argument. So essentially, inside the next function, self refers to the table that chanceList is returning.

Before defining what's inside the next function, we can define a few attributes that this table will have. The first is the actual chance_list one, which will contain the values that should be returned by next:

function chanceList(...)
    return {
    	chance_list = {},
        next = function(self)

        end
    }
end

This table starts empty and will be filled in the next function. In this example, for instance:

events = chanceList({'X', 3}, {'Y', 3}, {'Z', 4})

The chance_list attribute would look something like this:

.chance_list = {'X', 'X', 'X', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z'}

The other attribute we'll need is one called chance_definitions, which will hold all the values and chances passed in to the chanceList function:

function chanceList(...)
    return {
    	chance_list = {},
    	chance_definitions = {...},
        next = function(self)

        end
    }
end

And that's all we'll need. Now we can move on to the next function. The two behaviors we want out of that function is that it returns us a random value according to the chances described in chance_definitions, and also that it regenerates the internal chance_list whenever it reaches 0 elements. Assuming that the list is filled with elements we can take care of the former behavior like this:

next = function(self)
    return table.remove(self.chance_list, love.math.random(1, #self.chance_list))
end

We simply pick a random element inside the chance_list table and then return it. Because of the way elements are laid out inside, all the constraints we had about how this should work are being followed.

Now for the most important part, how we'll actually build the chance_list table. It turns out that we can use the same piece of code to build this list initially as well as whenever it gets emptied after repeated uses. The way this looks is like this:

next = function(self)
    if #self.chance_list == 0 then
        for _, chance_definition in ipairs(self.chance_definitions) do
      	    for i = 1, chance_definition[2] do 
                table.insert(self.chance_list, chance_definition[1]) 
      	    end
    	end
    end
    return table.remove(self.chance_list, love.math.random(1, #self.chance_list))
end

And so what we're doing here is first figuring out if the size of chance_list is 0. This will be true whenever we call next for the first time as well as whenever the list gets emptied after we called it multiple times. If it is true, then we start going over the chance_definitions table, which contains tables that we call chance_definition with the values and chances for that value. So if we called the chanceList function like this:

events = chanceList({'X', 3}, {'Y', 3}, {'Z', 4})

The chance_definitions table looks like this:

.chance_definitions = {{'X', 3}, {'Y', 3}, {'Z', 4}}

And so whenever we go over this list, chance_definitions[1] refers to the value and chance_definitions[2] refers to the number of times that value appears in chance_list. Knowing that, to fill up the list we simply insert chance_definition[1] into chance_list chance_definition[2] times. And we do this for all tables in chance_definitions as well.

And so if we try this out now we can see that it works out:

events = chanceList({'X', 2}, {'Y', 2}, {'Z', 4})
for i = 1, 16 do
    print(events:next())
end

Director

Now back to the Director, we wanted to implement the last part of rule 2 which deals with the implementation of setEnemySpawnsForThisRound. The first thing we wanna do for this is to define the spawn chances of each enemy. Different difficulties will have different spawn chances and we'll want to define at least the first few difficulties manually. And then the following difficulties will be defined somewhat randomly since they'll have so many points that the player will get overwhelmed either way.

So this is what the first few difficulties could look like:

function Director:new(...)
    ...
    self.enemy_spawn_chances = {
        [1] = chanceList({'Rock', 1}),
        [2] = chanceList({'Rock', 8}, {'Shooter', 4}),
        [3] = chanceList({'Rock', 8}, {'Shooter', 8}),
        [4] = chanceList({'Rock', 4}, {'Shooter', 8}),
    }
end

These are not the final numbers but just an example. So in the first difficulty only rocks would be spawned, then in the second one shooters would also be spawned but at a lower amount than rocks, then in the third both would be spawned about the same, and finally in the fourth more shooters would be spawned than rocks.

For difficulties past 5 until 1024 we can just assign somewhat random probabilities to each enemy like this:

function Director:new(...)
    ...
    for i = 5, 1024 do
        self.enemy_spawn_chances[i] = chanceList(
      	    {'Rock', love.math.random(2, 12)}, 
      	    {'Shooter', love.math.random(2, 12)}
    	)
    end
end

When we implement more enemies we will do the first 16 difficulties manually and after difficulty 17 we'll do it somewhat randomly. In general, a player with a completely filled skill tree won't be able to go past difficulty 16 that often so it's a good place to stop.

Now for the setEnemySpawnsForThisRound function. The first thing we'll do is use create enemies in a list, according to the enemy_spawn_chances table, until we run out of points for this difficulty. This can look something like this:

function Director:setEnemySpawnsForThisRound()
    local points = self.difficulty_to_points[self.difficulty]

    -- Find enemies
    local enemy_list = {}
    while points > 0 do
        local enemy = self.enemy_spawn_chances[self.difficulty]:next()
        points = points - self.enemy_to_points[enemy]
        table.insert(enemy_list, enemy)
    end
end

And so with this, the local enemy_list table will be filled with Rock and Shooter strings according to the probabilities of the current difficulty. We put this inside a while loop that stops whenever the number of points left reaches 0.

After this, we need to decide when in the 22 second duration of this round each one of those enemies inside the enemy_list table will be spawned. That could look something like this:

function Director:setEnemySpawnsForThisRound()
    ...
  
    -- Find enemies spawn times
    local enemy_spawn_times = {}
    for i = 1, #enemy_list do 
    	enemy_spawn_times[i] = random(0, self.round_duration) 
    end
    table.sort(enemy_spawn_times, function(a, b) return a < b end)
end

Here we make it so that each enemy in enemy_list has a random number of between 0 and round_duration assigned to it and stored in the enemy_spawn_times table. We further sort this table so that the values are laid out in order. So if our enemy_list table looks like this:

.enemy_list = {'Rock', 'Shooter', 'Rock'}

Our enemy_spawn_times table would look like this:

.enemy_spawn_times = {2.5, 8.4, 14.8}

Which means that a Rock would be spawned 2.5 seconds in, a Shooter would be spawned 8.4 seconds in, and another Rock would be spawned 14.8 seconds in since the start of the round.

Finally, now we have to actually set enemies to be spawned using the timer:after call:

function Director:setEnemySpawnsForThisRound()
    ...

    -- Set spawn enemy timer
    for i = 1, #enemy_spawn_times do
        self.timer:after(enemy_spawn_times[i], function()
            self.stage.area:addGameObject(enemy_list[i])
        end)
    end
end

And this should be pretty straightforward. We go over the enemy_spawn_times list and set enemies from the enemy_list to be spawned according to the numbers in the former. The last thing to do is to call this function once for when the game starts:

function Director:new(...)
    ...
    self:setEnemySpawnsForThisRound()
end

If we don't do this then enemies will only start spawning after 22 seconds. We can also add an Attack resource spawn at the start so that the player has the chance to swap his attack from the get go as well, but that's not mandatory. In any case, if you run everything now it should work like we intended!

This is where we'll stop with the Director for now but we'll come back to it in a future article after we have added more content to the game!


Director Exercises

116. (CONTENT) Implement rule 3. It should work just like rule 1, except that instead of the difficulty going up, either one of the 3 resources listed will be spawned. The chances for each resource to be spawned should follow this definition:

function Director:new(...)
    ...
    self.resource_spawn_chances = chanceList({'Boost', 28}, {'HP', 14}, {'SkillPoint', 58})
end

117. (CONTENT) Implement rule 4. It should work just like rule 1, except that instead of the difficulty going up, a random attack is spawned.

118. The while loop that takes care of finding enemies to spawn has one big problem: it can get stuck indefinitely in an infinite loop. Consider the situation where there's only one point left, for instance, and enemies that cost 1 point (like a Rock) can't be spawned anymore because that difficulty doesn't spawn Rocks. Find a general fix for this problem without changing the cost of enemies, the number of points in a difficulty, or without assuming that the probabilities of enemies being spawned will take care of it (making all difficulties always spawn low cost enemies like Rocks).


Game Loop

Now for the game loop. What we'll do here is make sure that the player can play the game over and over by making it so that whenever the player dies it restarts another run from scratch. In the final game the loop will be a bit different, because after a playthrough you'll be thrown back into the Console room, but since we don't have the Console room ready now, we'll just restart a Stage one. This is also a good place to check for memory problems, since we'll be restarting the Stage room over and over after the game has been played thoroughly.

Because of the way we structured things it turns out that doing this is incredibly simple. We'll do it by defining a finish function in the Stage class, which will take care of using gotoRoom to change to another Stage room. This function looks like this:

function Stage:finish()
    timer:after(1, function()
        gotoRoom('Stage')
    end)
end

gotoRoom will take care of destroying the previous Stage instance and creating the new one, so we don't have to worry about manually destroying objects here or there. The only one we have worry about is setting the player attribute in the Stage class to nil in its destroy function, otherwise the Player object won't be collected properly.

The finish function can be called whenever the player dies from the Player object itself:

function Player:die()
    ...
    current_room:finish()
end

We know that current_room is a global variable that holds the currently active room, and whenever the die function is called on a player the only room that could be active is a Stage, so this works out well. If you run all this you'll see that it works as expected. Once the player dies, after 1 second a new Stage room will start and you can play right away.

Note that this was this simple because of how we structured our game with the idea of Rooms and Areas. If we had structured things differently it would have been considerably harder and this is (in my opinion) where a lot of people get lost when making games with LÖVE. Because you can structure things in whatever way you want, it's easy to do it in a way that doesn't make doing things like resetting gameplay simple. So it's important to understand the role that the way we architectured everything plays.


Score

The main goal of the game is to have the highest score possible, so we need to create a score system. This one is also fairly simple compared to everything else we've been doing. All we need to do for now is create a score attribute in the Stage class that will keep track of how well we're doing on this run. Once the game ends that score will get saved somewhere else and then we'll be able to compare it against our highest scores ever. For now we'll skip the second part of comparing scores and just focus on getting the basics of it down.

function Stage:new()
    ...
    self.score = 0
end

And then we can increase the score whenever something that should increase it happens. Here are all the score rules for now:

  1. Gathering an ammo resource adds 50 to score
  2. Gathering a boost resource adds 150 to score
  3. Gathering a skill point resource adds 250 to score
  4. Gathering an attack resource adds 500 to score
  5. Killing a Rock adds 100 to score
  6. Killing a Shooter adds 150 to score

So, the way we'd go about doing rule 1 would be like this:

function Player:addAmmo(amount)
    self.ammo = math.min(self.ammo + amount, self.max_ammo)
    current_room.score = current_room.score + 50
end

We simply go to the most obvious place where the event happens (in this case in the addAmmo function), and then just add the code that changes the score there. Like we did for the finish function, we can access the Stage room through current_room here because the Stage room is the only one that could be active in this case.

Score Exercises

119. (CONTENT) Implement rules 2 through 6. They are very simple implementations and should be just like the one given as an example.


UI

Now for the UI. In the final game it looks like this:

There's the number of skill points you have to the top-left, your score to the top-right, and then the fundamental player stats on the top and bottom middle of the screen. Let's start with the score. All we want to do here is print a number to the top-right of the screen. This could look like this:

function Stage:draw()
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        ...
  		
        love.graphics.setFont(self.font)

        -- Score
        love.graphics.setColor(default_color)
        love.graphics.print(self.score, gw - 20, 10, 0, 1, 1,
    	math.floor(self.font:getWidth(self.score)/2), self.font:getHeight()/2)
        love.graphics.setColor(255, 255, 255)
    love.graphics.setCanvas()
  
    ...
end

We want to draw the UI above everything else and there are essentially two ways to do this. We can either create an object named UI or something and set its depth attribute so that it will be drawn on top of everything, or we can just draw everything directly on top of the Area on the main_canvas that the Stage room uses. I decided to go for the latter but either way works.

In the code above we're just using love.graphics.setFont to set this font:

function Stage:new()
    ...
    self.font = fonts.m5x7_16
end

And then after that we're drawing the score at a reasonable position on the top-right of the screen. We offset it by half the width of the text so that the score is centered on that position, rather than starting in it, otherwise when numbers get too high (>10000) the text will go offscreen.

The skill point text follows a similarly simple setup so that will be left as an exercise.


Now for the other main part of the UI, which are the center elements. We'll start with the HP one. We want to draw 3 things: the word of the stat (in this case "HP"), a bar showing how filled the stat is, and then numbers showing that same information but more precisely.

First we'll start by drawing the bar:

function Stage:draw()
    ...
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        ...
  
        -- HP
        local r, g, b = unpack(hp_color)
        local hp, max_hp = self.player.hp, self.player.max_hp
        love.graphics.setColor(r, g, b)
        love.graphics.rectangle('fill', gw/2 - 52, gh - 16, 48*(hp/max_hp), 4)
        love.graphics.setColor(r - 32, g - 32, b - 32)
        love.graphics.rectangle('line', gw/2 - 52, gh - 16, 48, 4)
	love.graphics.setCanvas()
end

First, the position we'll draw this rectangle at is gw/2 - 52, gh - 16 and the width will be 48, which means that both bars will be drawn around the center of the screen with a small gap of around 8 pixels. From this we can also tell that the position of the bar to the right will be gw/2 + 4, gh - 16.

The way we draw this bar is that it will be a filled rectangle with hp_color as its color, and then an outline on that rectangle with hp_color - 32 as its color. Since we can't really subtract from a table, we have to separate the hp_color table into its separate components and subtract from each.

The only bar that will be changed in any way is the one that is filled, and it will be changed according to the ratio of hp/max_hp. For instance, if hp/max_hp is 1, it means that the HP is full. If it's 0.5, then it means hp is half the size of max_hp. If it's 0.25, then it means it's 1/4 the size. And so if we multiply this ratio by the width the bar is supposed to have, we'll have a decent visual on how filled the player's HP is or isn't. If you do that it should look like this:

And you'll notice here that as the player gets his the bar responds accordingly.

Now similarly to how we drew the score number, we can the draw the HP text:

function Stage:draw()
    ...
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        ...
  
        -- HP
        ...
        love.graphics.print('HP', gw/2 - 52 + 24, gh - 24, 0, 1, 1,
    	math.floor(self.font:getWidth('HP')/2), math.floor(self.font:getHeight()/2))
	love.graphics.setCanvas()
end

Again, similarly to how we did for the score, we want this text to be centered around gw/2 - 52 + 24, which is the center of the bar, and so we have to offset it by the width of this text while using this font (and we do that with the getWidth function).

Finally, we can also draw the HP numbers below the bar somewhat simply:

function Stage:draw()
    ...
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        ...
  
        -- HP
        ...
        love.graphics.print(hp .. '/' .. max_hp, gw/2 - 52 + 24, gh - 6, 0, 1, 1,
    	math.floor(self.font:getWidth(hp .. '/' .. max_hp)/2),
    	math.floor(self.font:getHeight()/2))
	love.graphics.setCanvas()
end

And here the same principle applies. We want the text to be centered to we have to offset it by its width. Most of these positions were arrived at through trial and error so you can try different spacings if you want.


UI Exercises

120. (CONTENT) Implement the UI for the Ammo stat. The position of the bar is gw/2 - 52, 16.

121. (CONTENT) Implement the UI for the Boost stat. The position of the bar is gw/2 + 4, 16.

122. (CONTENT) Implement the UI for the Cycle stat. The position of the bar is gw/2 + 4, gh - 16.


END

And with that we finished the first main part of the game. This is the basic skeleton of the entire game with a minimal amount of content. The second part (the next 5 or so articles) will focus entirely on adding content to the game. The structure of the articles will also start to become more like this article where I show how to do something once and then the exercises are just implementing that same idea for multiple other things.

The next article though will be a small intermission where I'll go over some thoughts on coding practices and where I'll try to justify some of the choices I've made on how to architecture things and how I chose to lay all this code out. You can skip it if you only care about making the game, since it's going to be a more opinionated article and not as directly related to the game itself as others.



BYTEPATH #2 - Libraries

Introduction

In this article we'll cover a few Lua/LÖVE libraries that are necessary for the project and we'll also explore some ideas unique to Lua that you should start to get comfortable with. There will be a total of 4 libraries used by the end of it, and part of the goal is to also get you used to the idea of downloading libraries built by other people, reading through the documentation of those and figuring out how they work and how you can use them in your game. Lua and LÖVE don't come with lots of features by themselves, so downloading code written by other people and using it is a very common and necessary thing to do.


Object Orientation

The first thing I'll cover here is object orientation. There are many many different ways to get object orientation working with Lua, but I'll just use a library. The OOP library I like the most is rxi/classic because of how small and effective it is. To install it just download it and drop the classic folder inside the project folder. Generally I create a libraries folder and drop all libraries there.

Once that's done you can import the library to the game at the top of the main.lua file by doing:

Object = require 'libraries/classic/classic'

As the github page states, you can do all the normal OOP stuff with this library and it should work fine. When creating a new class I usually do it in a separate file and place that file inside an objects folder. So, for instance, creating a Test class and instantiating it once would look like this:

-- in objects/Test.lua
Test = Object:extend()

function Test:new()

end

function Test:update(dt)

end

function Test:draw()

end
-- in main.lua
Object = require 'libraries/classic/classic'
require 'objects/Test'

function love.load()
    test_instance = Test()
end

So when require 'objects/Test' is called in main.lua, everything that is defined in the Test.lua file happens, which means that the Test global variable now contains the definition for the Test class. For this game, every class definition will be done like this, which means that class names must be unique since they are bound to a global variable. If you don't want to do things like this you can make the following changes:

-- in objects/Test.lua
local Test = Object:extend()
...
return Test
-- in main.lua
Test = require 'objects/Test'

By defining the Test variable as local in Test.lua it won't be bound to a global variable, which means you can bind it to whatever name you want when requiring it in main.lua. At the end of the Test.lua script the local variable is returned, and so in main.lua when Test = require 'objects/Test' is declared, the Test class definition is being assigned to the global variable Test.

Sometimes, like when writing libraries for other people, this is a better way of doing things so you don't pollute their global state with your library's variables. This is what classic does as well, which is why you have to initialize it by assigning it to the Object variable. One good result of this is that since we're assigning a library to a variable, if you wanted to you could have named Object as Class instead, and then your class definitions would look like Test = Class:extend().

One last thing that I do is to automate the require process for all classes. To add a class to the environment you need to type require 'objects/ClassName'. The problem with this is that there will be lots of classes and typing it for every class can be tiresome. So something like this can be done to automate that process:

function love.load()
    local object_files = {}
    recursiveEnumerate('objects', object_files)
end

function recursiveEnumerate(folder, file_list)
    local items = love.filesystem.getDirectoryItems(folder)
    for _, item in ipairs(items) do
        local file = folder .. '/' .. item
        if love.filesystem.isFile(file) then
            table.insert(file_list, file)
        elseif love.filesystem.isDirectory(file) then
            recursiveEnumerate(file, file_list)
        end
    end
end

So let's break this down. The recursiveEnumerate function recursively enumerates all files inside a given folder and adds them as strings to a table. It makes use of LÖVE's filesystem module, which contains lots of useful functions for doing stuff like this.

The first line inside the loop lists all files and folders in the given folder and returns them as a table of strings using love.filesystem.getDirectoryItems. Next, it iterates over all those and gets the full file path of each item by concatenating (concatenation of strings in Lua is done by using ..) the folder string and the item string.

Let's say that the folder string is 'objects' and that inside the objects folder there is a single file named GameObject.lua. And so the items list will look like items = {'GameObject.lua'}. When that list is iterated over, the local file = folder .. '/' .. item line will parse to local file = 'objects/GameObject.lua', which is the full path of the file in question.

Then, this full path is used to check if it is a file or a directory using the love.filesystem.isFile and love.filesystem.isDirectory functions. If it is a file then simply add it to the file_list table that was passed in from the caller, otherwise call recursiveEnumerate again, but now using this path as the folder variable. When this finishes running, the file_list table will be full of strings corresponding to the paths of all files inside folder. In our case, the object_files variable will be a table full of strings corresponding to all the classes in the objects folder.

There's still a step left, which is to take all those paths and require them:

function love.load()
    local object_files = {}
    recursiveEnumerate('objects', object_files)
    requireFiles(object_files)
end

function requireFiles(files)
    for _, file in ipairs(files) do
        local file = file:sub(1, -5)
        require(file)
    end
end

This is a lot more straightforward. It simply goes over the files and calls require on them. The only thing left to do is to remove the .lua from the end of the string, since the require function spits out an error if it's left in. The line that does that is local file = file:sub(1, -5) and it uses one of Lua's builtin string functions. So after this is done all classes defined inside the objects folder can be automatically loaded. The recursiveEnumerate function will also be used later to automatically load other resources like images, sounds and shaders.


OOP Exercises

6. Create a Circle class that receives x, y and radius arguments in its constructor, has x, y, radius and creation_time attributes and has update and draw methods. The x, y and radius attributes should be initialized to the values passed in from the constructor and the creation_time attribute should be initialized to the relative time the instance was created (see love.timer). The update method should receive a dt argument and the draw function should draw a white filled circle centered at x, y with radius radius (see love.graphics). An instance of this Circle class should be created at position 400, 300 with radius 50. It should also be updated and drawn to the screen. This is what the screen should look like:

7. Create an HyperCircle class that inherits from the Circle class. An HyperCircle is just like a Circle, except it also has an outer ring drawn around it. It should receive additional arguments line_width and outer_radius in its constructor. An instance of this HyperCircle class should be created at position 400, 300 with radius 50, line width 10 and outer radius 120. This is what the screen should look like:

8. What is the purpose of the : operator in Lua? How is it different from . and when should either be used?

9. Suppose we have the following code:

function createCounterTable()
    return {
        value = 1,
        increment = function(self) self.value = self.value + 1 end,
    }
end

function love.load()
    counter_table = createCounterTable()
    counter_table:increment()
end

What is the value of counter_table.value? Why does the increment function receive an argument named self? Could this argument be named something else? And what is the variable that self represents in this example?

10. Create a function that returns a table that contains the attributes a, b, c and sum. a, b and c should be initiated to 1, 2 and 3 respectively, and sum should be a function that adds a, b and c together. The final result of the sum should be stored in the c attribute of the table (meaning, after you do everything, the table should have an attribute c with the value 6 in it).

11. If a class has a method with the name of someMethod can there be an attribute of the same name? If not, why not?

12. What is the global table in Lua?

13. Based on the way we made classes be automatically loaded, whenever one class inherits from another we have code that looks like this:

SomeClass = ParentClass:extend()

Is there any guarantee that when this line is being processed the ParentClass variable is already defined? Or, to put it another way, is there any guarantee that ParentClass is required before SomeClass? If yes, what is that guarantee? If not, what could be done to fix this problem?

14. Suppose that all class files do not define the class globally but do so locally, like:

local ClassName = Object:extend()
...
return ClassName

How would the requireFiles function need to be changed so that we could still automatically load all classes?


Input

Now for how to handle input. The default way to do it in LÖVE is through a few callbacks. When defined, these callback functions will be called whenever the relevant event happens and then you can hook the game in there and do whatever you want with it:

function love.load()

end

function love.update(dt)

end

function love.draw()

end

function love.keypressed(key)
    print(key)
end

function love.keyreleased(key)
    print(key)
end

function love.mousepressed(x, y, button)
    print(x, y, button)
end

function love.mousereleased(x, y, button)
    print(x, y, button)
end

So in this case, whenever you press a key or click anywhere on the screen the information will be printed out to the console. One of the big problems I've always had with this way of doing things is that it forces you to structure everything you do that needs to receive input around these calls.

So, let's say you have a game object which has inside it a level object which has inside a player object. To get the player object receive keyboard input, all those 3 objects need to have the two keyboard related callbacks defined, because at the top level you only want to call game:keypressed inside love.keypressed, since you don't want the lower levels to know about the level or the player. So I created a library to deal with this problem. You can download it and install it like the other library that was covered. Here's a few examples of how it works:

function love.load()
    input = Input()
    input:bind('mouse1', 'test')
end

function love.update(dt)
    if input:pressed('test') then print('pressed') end
    if input:released('test') then print('released') end
    if input:down('test') then print('down') end
end

So what the library does is that instead of relying on callback functions for input, it simply asks if a certain key has been pressed on this frame and receives a response of true or false. In the example above on the frame that you press the mouse1 button, pressed will be printed to the screen, and on the frame that you release it, released will be printed. On all the other frames where the press didn't happen the input:pressed or input:released calls would have returned false and so whatever is inside of the conditional wouldn't be run. The same applies to the input:down function, except it returns true on every frame that the button is held down and false otherwise.

Often times you want behavior that repeats at a certain interval when a key is held down, instead of happening every frame. For that purpose you can use the down function like this:

function love.update(dt)
    if input:down('test', 0.5) then print('test event') end
end

So in this example, once the key bound to the test action is held down, every 0.5 seconds test event will be printed to the console.


Input Exercises

15. Suppose we have the following code:

function love.load()
    input = Input()
    input:bind('mouse1', function() print(love.math.random()) end)
end

Will anything happen when mouse1 is pressed? What about when it is released? And held down?

16. Bind the keypad + key to an action named add, then increment the value of a variable named sum (which starts at 0) by 1 every 0.25 seconds when the add action key is held down. Print the value of sum to the console every time it is incremented.

17. Can multiple keys be bound to the same action? If not, why not? And can multiple actions be bound to the same key? If not, why not?

18. If you have a gamepad, bind its DPAD buttons(fup, fdown...) to actions up, left, right and down and then print the name of the action to the console once each button is pressed.

19. If you have a gamepad, bind one of its trigger buttons (l2, r2) to an action named trigger. Trigger buttons return a value from 0 to 1 instead of a boolean saying if its pressed or not. How would you get this value?

20. Repeat the same as the previous exercise but for the left and right stick's horizontal and vertical position.


Timer

Now another crucial piece of code to have are general timing functions. For this I'll use hump, more especifically hump.timer.

Timer = require 'libraries/hump/timer'

function love.load()
    timer = Timer()
end

function love.update(dt)
    timer:update(dt)
end

According to the documentation it can be used directly through the Timer variable or it can be instantiated to a new one instead. I decided to do the latter. I'll use this global timer variable for global timers and then whenever timers inside objects are needed, like inside the Player class, it will have its own timer instantiated locally.

The most important timing functions used throughout the entire game are after, every and tween. And while I personally don't use the script function, some people might find it useful so it's worth a mention. So let's go through them:

function love.load()
    timer = Timer()
    timer:after(2, function() print(love.math.random()) end)
end

after is pretty straightfoward. It takes in a number and a function, and it executes the function after number seconds. In the example above, a random number would be printed to the console 2 seconds after the game is run. One of the cool things you can do with after is that you can chain multiple of those together, so for instance:

function love.load()
    timer = Timer()
    timer:after(2, function()
        print(love.math.random())
        timer:after(1, function()
            print(love.math.random())
            timer:after(1, function()
                print(love.math.random())
            end)
        end)
    end)
end

In this example, a random number would be printed 2 seconds after the start, then another one 1 second after that (3 seconds since the start), and finally another one another second after that (4 seconds since the start). This is somewhat similar to what the script function does, so you can choose which one you like best.

function love.load()
    timer = Timer()
    timer:every(1, function() print(love.math.random()) end)
end

In this example, a random number would be printed every 1 second. Like the after function it takes in a number and a function and executes the function after number seconds. Optionally it can also take a third argument which is the amount of times it should pulse for, so, for instance:

function love.load()
    timer = Timer()
    timer:every(1, function() print(love.math.random()) end, 5)
end

Would only print 5 numbers in the first 5 pulses. One way to get the every function to stop pulsing without specifying how many times it should be run for is by having it return false. This is useful for situations where the stop condition is not fixed or known at the time the every call was made.

Another way you can get the behavior of the every function is through the after function, like so:

function love.load()
    timer = Timer()
    timer:after(1, function(f)
        print(love.math.random())
        timer:after(1, f)
    end)
end

I never looked into how this works internally, but the creator of the library decided to do it this way and document it in the instructions so I'll just take it ^^. The usefulness of getting the funcionality of every in this way is that we can change the time taken between each pulse by changing the value of the second after call inside the first:

function love.load()
    timer = Timer()
    timer:after(1, function(f)
        print(love.math.random())
        timer:after(love.math.random(), f)
    end)
end

So in this example the time between each pulse is variable (between 0 and 1, since love.math.random returns values in that range by default), something that can't be achieved by default with the every function. Variable pulses are very useful in a number of situations so it's good to know how to do them. Now, on to the tween function:

function love.load()
    timer = Timer()
    circle = {radius = 24}
    timer:tween(6, circle, {radius = 96}, 'in-out-cubic')
end

function love.update(dt)
    timer:update(dt)
end

function love.draw()
    love.graphics.circle('fill', 400, 300, circle.radius)
end

The tween function is the hardest one to get used to because there are so many arguments, but it takes in a number of seconds, the subject table, the target table and a tween mode. Then it performs the tween on the subject table towards the values in the target table. So in the example above, the table circle has a key radius in it with the initial value of 24. Over the span of 6 seconds this value will changed to 96 using the in-out-cubic tween mode. (here's a useful list of all tweening modes) It sounds complicated but it looks like this:

The tween function can also take an additional argument after the tween mode which is a function to be called when the tween ends. This can be used for a number of purposes, but taking the previous example, we could use it to make the circle shrink back to normal after it finishes expanding:

function love.load()
    timer = Timer()
    circle = {radius = 24}
    timer:after(2, function()
        timer:tween(6, circle, {radius = 96}, 'in-out-cubic', function()
            timer:tween(6, circle, {radius = 24}, 'in-out-cubic')
        end)
    end)
end

And that looks like this:

These 3 functions - after, every and tween - are by far in the group of most useful functions in my code base. They are very versatile and they can achieve a lot of stuff. So make you sure you have some intuitive understanding of what they're doing!


One important thing about the timer library is that each one of those calls returns a handle. This handle can be used in conjunction with the cancel call to abort a specific timer:

function love.load()
    timer = Timer()
    local handle_1 = timer:after(2, function() print(love.math.random()) end)
    timer:cancel(handle_1)

So in this example what's happening is that first we call after to print a random number to the console after 2 seconds, and we store the handle of this timer in the handle_1 variable. Then we cancel that call by calling cancel with handle_1 as an argument. This is an extremely important thing to be able to do because often times we will get into a situation where we'll create timed calls based on certain events. Say, when someone presses the key r we want to print a random number to the console after 2 seconds:

function love.keypressed(key)
    if key == 'r' then
        timer:after(2, function() print(love.math.random()) end)
    end
end

If you add the code above to the main.lua file and run the project, after you press r a random number should appear on the screen with a delay. If you press r multiple times repeatedly, multiple numbers will appear with a delay in quick succession. But sometimes we want the behavior that if the event happens repeated times it should reset the timer and start counting from 0 again. This means that whenever we press r we want to cancel all previous timers created from when this event happened in the past. One way of doing this is to somehow store all handles created somewhere, bind them to an event identifier of some sort, and then call some cancel function on the event identifier itself which will cancel all timer handles associated with that event. This is what that solution looks like:

function love.keypressed(key)
    if key == 'r' then
        timer:after('r_key_press', 2, function() print(love.math.random()) end)
    end
end

I created an enhancement of the current timer module that supports the addition of event tags. So in this case, the event r_key_press is attached to the timer that is created whenever the r key is pressed. If the key is pressed multiple times repeatedly, the module will automatically see that this event has other timers registered to it and cancel those previous timers as a default behavior, which is what we wanted. If the tag is not used then it defaults to the normal behavior of the module.

You can download this enhanced version here and swap the timer import in main.lua from libraries/hump/timer to wherever you end up placing the EnhancedTimer.lua file, I personally placed it in libraries/enhanced_timer/EnhancedTimer. This also assumes that the hump library was placed inside the libraries folder. If you named your folders something different you must change the path at the top of the EnhancedTimer file. Additionally, you can also use this library I wrote which has the same functionality as hump.timer, but also handles event tags in the way I described.


Timer Exercises

21. Using only a for loop and one declaration of the after function inside that loop, print 10 random numbers to the screen with an interval of 0.5 seconds between each print.

22. Suppose we have the following code:

function love.load()
    timer = Timer()
    rect_1 = {x = 400, y = 300, w = 50, h = 200}
    rect_2 = {x = 400, y = 300, w = 200, h = 50}
end

function love.update(dt)
    timer:update(dt)
end

function love.draw()
    love.graphics.rectangle('fill', rect_1.x - rect_1.w/2, rect_1.y - rect_1.h/2, rect_1.w, rect_1.h)
    love.graphics.rectangle('fill', rect_2.x - rect_2.w/2, rect_2.y - rect_2.h/2, rect_2.w, rect_2.h)
end

Using only the tween function, tween the w attribute of the first rectangle over 1 second using the in-out-cubic tween mode. After that is done, tween the h attribute of the second rectangle over 1 second using the in-out-cubic tween mode. After that is done, tween both rectangles back to their original attributes over 2 seconds using the in-out-cubic tween mode. It should look like this:

23. For this exercise you should create an HP bar. Whenever the user presses the d key the HP bar should simulate damage taken. It should look like this:

As you can see there are two layers to this HP bar, and whenever damage is taken the top layer moves faster while the background one lags behind for a while.

24. Taking the previous example of the expanding and shrinking circle, it expands once and then shrinks once. How would you change that code so that it expands and shrinks continually forever?

25. Accomplish the results of the previous exercise using only the after function.

26. Bind the e key to expand the circle when pressed and the s to shrink the circle when pressed. Each new key press should cancel any expansion/shrinking that is still happening.

27. Suppose we have the following code:

function love.load()
    timer = Timer()
    a = 10  
end

function love.update(dt)
    timer:update(dt)
end

Using only the tween function and without placing the a variable inside another table, how would you tween its value to 20 over 1 second using the linear tween mode?


Table Functions

Now for the final library I'll go over Yonaba/Moses which contains a bunch of functions to handle tables more easily in Lua. The documentation for it can be found here. By now you should be able to read through it and figure out how to install it and use it yourself.

But before going straight to exercises you should know how to print a table to the console and verify its values:

for k, v in pairs(some_table) do
    print(k, v)
end

Table Exercises

For all exercises assume you have the following tables defined:

a = {1, 2, '3', 4, '5', 6, 7, true, 9, 10, 11, a = 1, b = 2, c = 3, {1, 2, 3}}
b = {1, 1, 3, 4, 5, 6, 7, false}
c = {'1', '2', '3', 4, 5, 6}
d = {1, 4, 3, 4, 5, 6}

You are also required to use only one function from the library per exercise unless explicitly told otherwise.

28. Print the contents of the a table to the console using the each function.

29. Count the number of 1 values inside the b table.

30. Add 1 to all the values of the d table using the map function.

31. Using the map function, apply the following transformations to the a table: if the value is a number, it should be doubled; if the value is a string, it should have 'xD' concatenated to it; if the value is a boolean, it should have its value flipped; and finally, if the value is a table it should be omitted.

32. Sum all the values of the d list. The result should be 23.

33. Suppose you have the following code:

if _______ then
    print('table contains the value 9')
end

Which function from the library should be used in the underscored spot to verify if the b table contains or doesn't contain the value 9?

34. Find the first index in which the value 7 is found in the c table.

35. Filter the d table so that only numbers lower than 5 remain.

36. Filter the c table so that only strings remain.

37. Check if all values of the c and d tables are numbers or not. It should return false for the first and true for the second.

38. Shuffle the d table randomly.

39. Reverse the d table.

40. Remove all occurrences of the values 1 and 4 from the d table.

41. Create a combination of the b, c and d tables that doesn't have any duplicates.

42. Find the common values between b and d tables.

43. Append the b table to the d table.



BYTEPATH #5 - Game Basics

Introduction

In this part we'll start going over the game itself. First we'll go over an overview of how the game is structured in terms of gameplay, then we'll focus on a few basics that are common to all parts of the game, like its pixelated look, the camera, as well as the physics simulation. After that we'll go over basic player movement and lastly we'll take a look at garbage collection and how we should look out for possible object leaks.


Gameplay Structure

The game itself is divided in only 3 different Rooms: Stage, Console and SkillTree.

The Stage room is where all the actual gameplay will take place and it will have objects such as the player, enemies, projectiles, resources, powerups and so on. The gameplay is very similar to that of Bit Blaster XL and is actually quite simple. I chose something this simple because it would allow me to focus on the other aspect of the game (the huge skill tree) more thoroughly than if the gameplay was more complicated.

The Console room is where all the "menu" kind of stuff happens: changing sound and video settings, seeing achievements, choosing which ship you want to play with, accessing the skill tree, and so on. Instead of creating various different menus it makes more sense for a game that has this sort of computery look to it (also known as lazy programmer art xD) to go for this, since the console emulates a terminal and the idea is that you (the player) are just playing the game through some terminal somewhere.

The SkillTree room is where all the passive skills can be acquired. In the Stage room you can get SP (skill points) that spawn randomly or after you kill enemies, and then once you die you can use those skill points to buy passive skills. The idea is to try something massive like Path of Exile's Passive Skill Tree and I think I was mildly successful at that. The skill tree I built has between 600-800 nodes and I think that's good enough.

I'll go over the creation of each of those rooms in detail, including all skills in the skill tree. However, I highly encourage you to deviate from what I'm writing as much as possible. A lot of the decisions I'm making when it comes to gameplay are pretty much just my own preference, and you might prefer something different.

For instance, instead of a huge skill tree you could prefer a huge class system that allows tons of combinations like Tree of Savior's. So instead of building the passive skill tree like I am, you could follow along on the implementation of all passive skills, but then build your own class system that uses those passive skills instead of building a skill tree.

This is just one idea and there are many different areas in which you could deviate in a similar way. One of the reasons I'm writing these tutorials with exercises is to encourage people to engage with the material by themselves instead of just following along because I think that that way people learn better. So whenever you see an opportunity to do something differently I highly recommend trying to do it.


Game Size

Now let's start with the Stage. The first thing we want (and this will be true for all rooms, not just the Stage) is for it to have a sort low resolution pixelated look to it. For instance, look at this circle:

And then look at this:

I want the second one. The reason for this is purely aesthetic and my own personal preference. There are a number of games that don't go for the pixelated look but still use simple shapes and colors to get a really nice look, like this one. So it just depends on which style you prefer and how much you can polish it. But for this game I'll go with the pixelated look.

The way to achieve that is by defining a very small default resolution first, preferably something that scales up exactly to a target resolution of 1920x1080. For this game I'll go with 480x270, since that's the target 1920x1080 divided by 4. To set the game's size to be this by default we need to use the file conf.lua, which as I explained in a previous article is a configuration file that defines a bunch of default settings about a LÖVE project, including the resolution that the window will start with.

On top of that, in that file I also define two global variables gw and gh, corresponding to width and height of the base resolution, and sx and sy ones, corresponding to the scale that should be applied to the base resolution. The conf.lua file should be placed in the same folder as the main.lua file and this is what it should look like:

gw = 480 
gh = 270 
sx = 1
sy = 1

function love.conf(t)
    t.identity = nil                   -- The name of the save directory (string)
    t.version = "0.10.2"                -- The LÖVE version this game was made for (string)
    t.console = false                  -- Attach a console (boolean, Windows only)
 
    t.window.title = "BYTEPATH" -- The window title (string)
    t.window.icon = nil                -- Filepath to an image to use as the window's icon (string)
    t.window.width = gw -- The window width (number)
    t.window.height = gh -- The window height (number)
    t.window.borderless = false        -- Remove all border visuals from the window (boolean)
    t.window.resizable = true          -- Let the window be user-resizable (boolean)
    t.window.minwidth = 1              -- Minimum window width if the window is resizable (number)
    t.window.minheight = 1             -- Minimum window height if the window is resizable (number)
    t.window.fullscreen = false        -- Enable fullscreen (boolean)
    t.window.fullscreentype = "exclusive" -- Standard fullscreen or desktop fullscreen mode (string)
    t.window.vsync = true              -- Enable vertical sync (boolean)
    t.window.fsaa = 0                  -- The number of samples to use with multi-sampled antialiasing (number)
    t.window.display = 1               -- Index of the monitor to show the window in (number)
    t.window.highdpi = false           -- Enable high-dpi mode for the window on a Retina display (boolean)
    t.window.srgb = false              -- Enable sRGB gamma correction when drawing to the screen (boolean)
    t.window.x = nil                   -- The x-coordinate of the window's position in the specified display (number)
    t.window.y = nil                   -- The y-coordinate of the window's position in the specified display (number)
 
    t.modules.audio = true -- Enable the audio module (boolean)
    t.modules.event = true             -- Enable the event module (boolean)
    t.modules.graphics = true          -- Enable the graphics module (boolean)
    t.modules.image = true             -- Enable the image module (boolean)
    t.modules.joystick = true -- Enable the joystick module (boolean)
    t.modules.keyboard = true          -- Enable the keyboard module (boolean)
    t.modules.math = true              -- Enable the math module (boolean)
    t.modules.mouse = true             -- Enable the mouse module (boolean)
    t.modules.physics = true -- Enable the physics module (boolean)
    t.modules.sound = true -- Enable the sound module (boolean)
    t.modules.system = true            -- Enable the system module (boolean)
    t.modules.timer = true             -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update
    t.modules.window = true            -- Enable the window module (boolean)
    t.modules.thread = true            -- Enable the thread module (boolean)
end

If you run the game now you should see a smaller window than you had before.

Now, to achieve the pixelated look when we scale the window up we need to do some extra work. If you were to draw a circle at the center of the screen (gw/2, gh/2) right now, like this:

And scale the screen up directly by calling love.window.setMode with width 3*gw and height 3*gh, for instance, you'd get something like this:

And as you can see, the circle didn't scale up with the screen and it just stayed a small circle. And it also didn't stay centered on the screen, because gw/2 and gh/2 isn't the center of the screen anymore when it's scaled up by 3. What we want is to be able to draw a small circle at the base resolution of 480x270, but then when the screen is scaled up to fit a normal monitor, the circle is also scaled up proportionally (and in a pixelated manner) and its position also remains proportionally the same. The easiest way to do that is by using a Canvas, which also goes by the name of framebuffer or render target in other engines. First, we'll create a canvas with the base resolution in the constructor of the Stage class:

function Stage:new()
    self.area = Area(self)
    self.main_canvas = love.graphics.newCanvas(gw, gh)
end

This creates a canvas with size 480x270 that we can draw to:

function Stage:draw()
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        love.graphics.circle('line', gw/2, gh/2, 50)
        self.area:draw()
    love.graphics.setCanvas()
end

The way the canvas is being drawn to is simply following the example on the Canvas page. According to the page, when we want to draw something to a canvas we need to call love.graphics.setCanvas, which will redirect all drawing operations to the currently set canvas. Then, we call love.graphics.clear, which will clear the contents of this canvas on this frame, since it was also drawn to in the last frame and every frame we want to draw everything from scratch. Then after that we draw what we want to draw and use setCanvas again, but passing nothing this time, so that our target canvas is unset and drawing operations aren't redirected to it anymore.

If we stopped here then nothing would appear on the screen. This happens because everything we drew went to the canvas but we're not actually drawing the canvas itself. So now we need to draw that canvas itself to the screen, and that looks like this:

function Stage:draw()
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
        love.graphics.circle('line', gw/2, gh/2, 50)
        self.area:draw()
    love.graphics.setCanvas()

    love.graphics.setColor(255, 255, 255, 255)
    love.graphics.setBlendMode('alpha', 'premultiplied')
    love.graphics.draw(self.main_canvas, 0, 0, 0, sx, sy)
    love.graphics.setBlendMode('alpha')
end

We simply use love.graphics.draw to draw the canvas to the screen, and then we also wrap that with some love.graphics.setBlendMode calls that according to the Canvas page on the LÖVE wiki are used to prevent improper blending. If you run this now you should see the circle being drawn.

Note that we used sx and sy to scale the Canvas up. Those variables are set to 1 right now, but if you change those variables to 3, for instance, this is what would happen:

You can't see anything! But this is the because the circle that was now in the middle of the 480x270 canvas, is now in the middle of a 1440x810 canvas. Since the screen itself is only 480x270, you can't see the entire Canvas that is bigger than the screen. To fix this we can create a function named resize in main.lua that will change both sx and sy as well as the screen size itself whenever it's called:

function resize(s)
    love.window.setMode(s*gw, s*gh) 
    sx, sy = s, s
end

And so if we call resize(3) in love.load, this should happen:

And this is roughly what we wanted. There's only one problem though: the circle looks kinda blurry instead of being properly pixelated.

The reason for this is that whenever things are scaled up or down in LÖVE, they use a FilterMode and this filter mode is set to 'linear' by default. Since we want the game to have a pixelated look we should change this to 'nearest'. Calling love.graphics.setDefaultFilter with the 'nearest' argument at the start of love.load should fix the issue. Another thing to do is to set the LineStyle to 'rough'. Because it's set to 'smooth' by default, LÖVE primitives will be drawn with some aliasing to them, and this doesn't work for a pixelated look. If you do all that and run the code again, it should look like this:

And it looks crispy and pixelated like we wanted it to! Most importantly, now we can use one resolution to build the entire game around. If we want to spawn an object at the center of the screen then we can say that it's x, y position should be gw/2, gh/2, and no matter what the resolution that we need to serve, that object will always be at the center of the screen. This significantly simplifies the process and it means we only have to worry about how the game looks and how things are distributed around the screen once.


Game Size Exercises

65. Take a look at Steam's Hardware Survey in the primary resolution section. The most popular resolution, used by almost half the users on Steam is 1920x1080. This game's base resolution neatly multiplies to that. But the second most popular resolution is 1366x768. 480x270 does not multiply into that at all. What are some options available for dealing with odd resolutions once the game is fullscreened into the player's monitor?

66. Pick a game you own that uses the same or a similar technique to what we're doing here (scaling a small base resolution up). Usually games that use pixel art will do that. What is that game's base resolution? How does the game deal with odd resolutions that don't fit neatly into its base resolution? Change the resolution of your desktop and run the game various times with different resolutions to see what changes and how it handles the variance.


Camera

All three rooms will make use of a camera so it makes sense to go through it now. From the second article in this series we used a library named hump for timers. This library also has a useful camera module that we'll also use. However, I use a slightly modified version of it that also has screen shake functionality. You can download the files here. Place the camera.lua file directory of the hump library (and overwrite the already existing camera.lua) and then require the camera module in main.lua. And place the Shake.lua file in the objects folder.

(Additionally, you can also use this library I wrote which has all this functionality already. I wrote this library after I wrote the entire tutorial, so the tutorial will go on as if the library didn't exist. If you do choose to use this library then you can follow along on the tutorial but sort of translating things to use the functions in this library instead.)

One function you'll need after adding the camera is this:

function random(min, max)
    local min, max = min or 0, max or 1
    return (min > max and (love.math.random()*(min - max) + max)) or (love.math.random()*(max - min) + min)
end

This function will allow you to get a random number between any two numbers. It's necessary because the Shake.lua file uses it. After defining that function in utils.lua try something like this:

function love.load()
    ...
    camera = Camera()
    input:bind('f3', function() camera:shake(4, 60, 1) end)
    ...
end

function love.update(dt)
    ...
    camera:update(dt)
    ...
end

And then on the Stage class:

function Stage:draw()
    love.graphics.setCanvas(self.main_canvas)
    love.graphics.clear()
  	camera:attach(0, 0, gw, gh)
        love.graphics.circle('line', gw/2, gh/2, 50)
        self.area:draw()
  	camera:detach()
    love.graphics.setCanvas()

    love.graphics.setColor(255, 255, 255, 255)
    love.graphics.setBlendMode('alpha', 'premultiplied')
    love.graphics.draw(self.main_canvas, 0, 0, 0, sx, sy)
    love.graphics.setBlendMode('alpha')
end

And you'll see that the screen shakes like this when you press f3:

The shake function is based on the one described on this article and it takes in an amplitude (in pixels), a frequency and a duration. The screen will shake with a decay, starting from the amplitude, for duration seconds with a certain frequency. Higher frequencies means that the screen will oscillate more violently between extremes (amplitude, -amplitude), while lower frequencies will do the contrary.

Another important thing to notice about the camera is that it's not anchored to a certain spot right now, and so when it shakes it will be thrown in all directions, making it be positioned elsewhere by the time the shaking ends, as you could see in the previous gif.

One way to fix this is to center it and this can be achieved with the camera:lockPosition function. In the modified version of the camera module I changed all camera movement functions to take in a dt argument first. And so that would look like this:

function Stage:update(dt)
    camera.smoother = Camera.smooth.damped(5)
    camera:lockPosition(dt, gw/2, gh/2)

    self.area:update(dt)
end

The camera smoother is set to damped with a value of 5. This was reached through trial and error but basically it makes the camera focus on the target point in a smooth and nice way. And the reason I placed this code inside the Stage room is that right now we're working with the Stage room and that room happens to be the one where the camera will need to be centered in the middle and never really move (other than screen shakes). And so that results in this:

We will use a single global camera for the entire game since there's no real need to instantiate a separate camera for each room. The Stage room will not use the camera in any way other than screen shakes, so that's where I'll stop for now. Both the Console and SkillTree rooms will use the camera more extensively but we'll get to that when we get to it.


Player Physics

Now we have everything needed to start with the actual game and we'll start with the Player object. Create a new file in the objects folder named Player.lua that looks like this:

Player = GameObject:extend()

function Player:new(area, x, y, opts)
    Player.super.new(self, area, x, y, opts)
end

function Player:update(dt)
    Player.super.update(self, dt)
end

function Player:draw()

end

This is the default way a new game object class in the game should be created. All of them will inherit from GameObject and will have the same structure to its constructor, update and draw functions. Now we can instantiate this Player object in the Stage room like this:

function Stage:new()
    ...
    self.area:addGameObject('Player', gw/2, gh/2)
end

To test that the instantiation worked and that the Player object is being updated and drawn by the Area, we can simply have it draw a circle in its position:

function Player:draw()
    love.graphics.circle('line', self.x, self.y, 25)
end

And that should give you a circle at the center of the screen. It's interesting to note that the addGameObject call returns the created object, so we could keep a reference to the player inside Stage's self.player, and then if we wanted we could trigger the Player object's death with a keybind:

function Stage:new()
    ...
    self.player = self.area:addGameObject('Player', gw/2, gh/2)
    input:bind('f3', function() self.player.dead = true end)
end

And if you press the f3 key then the Player object should be killed, which means that the circle will stop being drawn. This happens as a result of how we set up our Area object code from the previous article. It's also important to note that if you decide to hold references returned by addGameObject like this, if you don't set the variable holding the reference to nil that object will never be collected. And so it's important to keep in mind to always nil references (in this case by saying self.player = nil) if you want an object to truly be removed from memory (on top of settings its dead attribute to true).


Now for the physics. The Player (as well as enemies, projectiles and various resources) will be a physics objects. I'll use LÖVE's box2d integration for this, but this is something that is genuinely not necessary for this game, since it benefits in no way from using a full physics engine like box2d. The reason I'm using it is because I'm used to it. But I highly recommend you to try either rolling you own collision routines (which for a game like this is very easy to do), or using a library that handles that for you.

What I'll use and what the tutorial will follow is a library called windfield that I created which makes using box2d with LÖVE a lot easier than it would otherwise be. Other libraries that also handle collisions in LÖVE are HardonCollider or bump.lua.

I highly recommend for you to either do collisions on your own or use one of these two other libraries instead of the one the tutorial will follow. This is because this will make you exercise a bunch of abilities that you'll constantly have to exercise, like picking between various distinct solutions and seeing which one fits your needs and the way you think best, as well as coming up with your own solutions to problems that will likely arise instead of just following a tutorial.

To repeat this again, one of the main reasons why the tutorial has exercises is so that people actively engage with the material so that they actually learn, and this is another opportunity to do that. If you just follow the tutorial along and don't learn to confront things you don't know by yourself then you'll never truly learn. So I seriously recommend deviating from the tutorial here and doing the physics/collision part of the game on your own.

In any case, you can download the windfield library and require it in the main.lua file. According to its documentation there are the two main concepts of a World and a Collider. The World is the physics world that the simulation happens in, and the Colliders are the physics objects that are being simulated inside that world. So our game will need to have a physics world like and the player will be a collider inside that world.

We'll create a world inside the Area class by adding an addPhysicsWorld call:

function Area:addPhysicsWorld()
    self.world = Physics.newWorld(0, 0, true)
end

This will set the area's .world attribute to contain the physics world. We also need to update that world (and optionally draw it for debugging purposes) if it exists:

function Area:update(dt)
    if self.world then self.world:update(dt) end

    for i = #self.game_objects, 1, -1 do
        ...
    end
end

function Area:draw()
    if self.world then self.world:draw() end
    for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end

We update the physics world before updating all the game objects because we want to use up to date information for our game objects, and that will happen only after the physics simulation is done for this frame. If we were updating the game objects first then they would be using physics information from the last frame and this sort of breaks the frame boundary. It doesn't really change the way things work much as far as I can tell but it's conceptually more confusing.

The reason we add the world through the addPhysicsWorld call instead of just adding it directly to the Area constructor is because we don't want all Areas to have physics worlds. For instance, the Console room will also use an Area object to handle its entities, but it will not need a physics world attached to that Area. So making it optional through the call of one function makes sense. We can instantiate the physics world in Stage's Area like this:

function Stage:new()
    self.area = Area(self)
    self.area:addPhysicsWorld()
    ...
end

And so now that we have a world we can add the Player's collider to it:

function Player:new(area, x, y, opts)
    Player.super.new(self, area, x, y, opts)

    self.x, self.y = x, y
    self.w, self.h = 12, 12
    self.collider = self.area.world:newCircleCollider(self.x, self.y, self.w)
    self.collider:setObject(self)
end

Note how the player having a reference to the Area comes in handy here, because that way we can access the Area's World to add new colliders to it. This pattern (of accessing things inside the Area) repeats itself a lot, which is I made it so that all GameObject objects have this same constructor where they receive a reference to the Area object they belong to.

In any case, in the Player's constructor we defined its width and height to be 12 via the w and h attributes. Then we add a new CircleCollider with the radius set to the width. It doesn't make much sense now to make the collider a circle while having width and height defined but it will in the future, because as we add different types of ships that the player can be, visually the ships will have different widths and heights, but physically the collider will always be a circle for fairness between different ships as well as predictability to how they feel.

After the collider is added we call the setObject function which binds the Player object to the Collider we just created. This is useful because when two Colliders collide, we can get information in terms of Colliders but not in terms of objects. So, for instance, if the Player collides with a Projectile we will have in our hands two colliders that represent the Player and the Projectile but we might not have the objects themselves. Using setObject (and getObject) allows us to set and then extract the object that a Collider belongs to.

Finally now we can draw the Player according to its size:

function Player:draw()
    love.graphics.circle('line', self.x, self.y, self.w)
end

If you run the game now you should see a small circle that is the Player:


Player Physics Exercises

If you chose to do collisions yourself or decided to use one of the alternative libraries for collisions/physics then you don't need to do these exercises.

67. Change the physics world's y gravity to 512. What happens to the Player object?

68. What does the third argument of the .newWorld call do and what happens if it's set to false? Are there advantages/disadvantages to setting it to true/false? What are those?


Player Movement

The way movement for the Player works in this game is that there's a constant velocity that you move at and an angle that can be changed by holding left or right. To get that to work we need a few variables:

function Player:new(area, x, y, opts)
    Player.super.new(self, area, x, y, opts)

    ...

    self.r = -math.pi/2
    self.rv = 1.66*math.pi
    self.v = 0
    self.max_v = 100
    self.a = 100
end

Here I define r as the angle the player is moving towards. It starts as -math.pi/2, which is pointing up. In LÖVE angles work in a clockwise way, meaning math.pi/2 is down and -math.pi/2 is up (and 0 is right). Next, the rv variable represents the velocity of angle change when the user presses left or right. Then we have v, which represents the player's velocity, and then max_v, which represents the maximum velocity possible. The last attribute is a, which represents the player's acceleration. These were all arrived at by trial and error.

To update the player's position using all these variables we can do something like this:

function Player:update(dt)
    Player.super.update(self, dt)

    if input:down('left') then self.r = self.r - self.rv*dt end
    if input:down('right') then self.r = self.r + self.rv*dt end

    self.v = math.min(self.v + self.a*dt, self.max_v)
    self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r))
end

The first two lines define what happens when the user presses the left or right keys. It's important to note that according to the Input library we're using those bindings had to be defined beforehand, and I did so in main.lua (since we'll use a global Input object for everything):

function love.load()
    ...
    input:bind('left', 'left')
    input:bind('right', 'right')
    ...
end

And so whenever the user pressed left or right, the r attribute, which corresponds to the player's angle, will be changed by 1.66*math.pi radians in the appropriate direction. One important thing to note here is that this value is being multiplied by dt, which essentially means that this value is operating on a per second basis. So the angle at which the angle change happens is 1.66*math.pi radians per second. This is a result of how the game loop that we went over in the first article works.

After this, we set the v attribute. This one is a bit more involved, but if you've done this in other languages it should be familiar. The original calculation is self.v = self.v + self.a*dt, which is just increasing the velocity by the acceleration. In this case, we increase it by 100 per second. But we also defined the max_v attribute, which should cap the maximum velocity allowed. If we don't cap the maximum velocity allowed then self.v = self.v + self.a*dt will keep increasing v forever with no end and the Player will become Sonic. We don't want that! And so one way to prevent that from happening would be like this:

function Player:update(dt)
    ...

    self.v = self.v + self.a*dt
    if self.v >= self.max_v then
        self.v = self.max_v
    end

    ...
end

In this way, whenever v went over max_v it would be capped at that value instead of going over it. Another shorthand way of writing this is by using the math.min function, which returns the minimum value of all arguments that are passed to it. In this case we're passing in the result of self.v + self.a*dt and self.max_v, which means that if the result of the addition goes over max_v, math.min will return max_v, since its smaller than the addition. This is a very common and useful pattern in Lua (and in other languages as well).

Finally, we set the Collider's x and y velocity to the v attribute multiplied by the appropriate amount given the angle of the object using setLinearVelocity. In general whenever you want to move something in a direction and you have an angle to work with, you want to use cos to move it along the x axis and sin to move it along the y axis. This is also a very common pattern in 2D gamedev in general. I'm going to assume that you learned why this makes sense in school (and if you haven't then look up basic trigonometry on Google).

The final change we can make is one to the GameObject class and it's a simple one. Because we're using a physics engine we essentially have two representations of some variables, like position and velocity. We have the player's position and velocity through x, y and v attributes, and we have the Collider's position and velocity through getPosition and getLinearVelocity. It's a good idea to keep both of those representations synced, and one way to achieve that sort of automatically is by changing the parent class of all game objects:

function GameObject:update(dt)
    if self.timer then self.timer:update(dt) end
    if self.collider then self.x, self.y = self.collider:getPosition() end
end

And so here we simply that if the object has a collider attribute defined, then x and y will be set to the position of that collider. And so whenever the collider's position changes, the representation of that position in the object itself will also change accordingly.

If you run the program now you should see this:

And so you can see that the Player object moves around normally and changes its direction when left or right arrow keys are pressed. One detail that's important here is that what is being drawn is the Collider via the world:draw() call in the Area object. We don't really want to draw colliders only, so it makes sense to comment that line out and draw the Player object directly:

function Player:draw()
    love.graphics.circle('line', self.x, self.y, self.w)
end

One last useful thing we can do is visualize the direction that the player is heading towards. And this can be done by just drawing a line from the player's position that points in the direction he's heading:

function Player:draw()
    love.graphics.circle('line', self.x, self.y, self.w)
    love.graphics.line(self.x, self.y, self.x + 2*self.w*math.cos(self.r), self.y + 2*self.w*math.sin(self.r))
end

And that looks like this:

This again is basic trigonometry and uses the same idea as I explained a while ago. Generally whenever you want to get a position B that is distance units away from position A such that position B is positioned at a specific angle in relation to position A, the pattern is something like: bx = ax + distance*math.cos(angle) and by = ay + distance*math.sin(angle). Doing this is a very very common occurence in 2D gamedev (in my experience at least) and so getting an instinctive handle on how this works is useful.


Player Movement Exercises

69. Convert the following angles to degrees (in your head) and also say which quadrant they belong to (top-left, top-right, bottom-left or bottom-right). Notice that in LÖVE the angles are counted in a clockwise manner instead of an anti-clockwise one like you learned in school.

math.pi/2
math.pi/4
3*math.pi/4
-5*math.pi/6
0
11*math.pi/12
-math.pi/6
-math.pi/2 + math.pi/4
3*math.pi/4 + math.pi/3
math.pi

70. Does the acceleration attribute a need to exist? How could would the player's update function look like if it didn't exist? Are there any benefits to it being there at all?

71. Get the (x, y) position of point B from position A if the angle to be used is -math.pi/4, and the distance is 100.

72. Get the (x, y) position of point C from position B if the angle to be used is math.pi/4, and the distance if 50. The position A and B and the distance and angle between them are the same as the previous exercise.

73. Based on the previous two exercises, what's the general pattern involved when you want to get from point A to some point C when you all have to use are multiple points in between that all can be reached at through angles and distances?

74. The syncing of both representations of Player attributes and Collider attributes mentioned positions and velocities, but what about rotation? A collider has a rotation that can be accessed through getAngle. Why not also sync that to the r attribute?


Garbage Collection

Now that we've added the physics engine code and some movement code we can focus on something important that I've been ignoring until now, which is dealing with memory leaks. One of the things that can happen in any programming environment is that you'll leak memory and this can have all sorts of bad effects. In a managed language like Lua this can be an even more annoying problem to deal with because things are more hidden behind black boxes than if you had full control of memory yourself.

The way the garbage collector works is that when there are no more references pointing to an object it will eventually be collected. So if you have a table that is only being referenced to by variable a, once you say a = nil, the garbage collector will understand that the table that was being referenced isn't being referenced by anything and so that table will be removed from memory in a future garbage collection cycle. The problem happens when a single object is being referenced to multiple times and you forget to dereference it in all those locations.

For instance, when we create a new object with addGameObject what happens is that the object gets added to a .game_objects list. This counts as one reference pointing to that object. What we also do in that function, though, is return the object itself. So previously we did something like self.player = self.area:addGameObject('Player', ...), which means that on top of holding a reference to the object in the list inside the Area object, we're also holding a reference to it in the self.player variable. Which means that when we say self.player.dead and the Player object gets removed from the game objects list in the Area object, it will still not be collected because self.player is still pointing to it. So in this instance, to truly remove the Player object from memory we have to both set dead to true and then say self.player = nil.

This is just one example of how it could happen but this is a problem that can happen everywhere, and you should be especially careful about it when using other people's libraries. For instance, the physics library I built has a setObject function in which you pass in the object so that the Collider holds a reference to it. If the object dies will it be removed from memory? No, because the Collider is still holding a reference to it. Same problem, just in a different setting. One way of solving this problem is being explicit about the destruction of objects by having a destroy function for them, which will take care of dereferencing things.

So, one thing we can add to all objects this:

function GameObject:destroy()
    self.timer:destroy()
    if self.collider then self.collider:destroy() end
    self.collider = nil
end

So now all objects have this default destroy function. This function calls the destroy functions of the EnhancedTimer object as well as the Collider's one. What these functions do is essentially dereference things that the user will probably want removed from memory. For instance, inside Collider:destroy, one of the things that happens is that self:setObject(nil) is called, since if we want to destroy this object we don't want the Collider holding a reference to it anymore.

And then we can also change our Area update function like this:

function Area:update(dt)
    if self.world then self.world:update(dt) end

    for i = #self.game_objects, 1, -1 do
        local game_object = self.game_objects[i]
        game_object:update(dt)
        if game_object.dead then 
            game_object:destroy()
            table.remove(self.game_objects, i) 
        end
    end
end

If an object's dead attribute is set to true, then on top of removing it from the game objects list, we also call its destroy function, which will get rid of most references to it. We can expand this concept further and realize that the physics world itself also has a World:destroy, and so we might want to use it when destroying an Area object:

function Area:destroy()
    for i = #self.game_objects, 1, -1 do
        local game_object = self.game_objects[i]
        game_object:destroy()
        table.remove(self.game_objects, i)
    end
    self.game_objects = {}

    if self.world then
        self.world:destroy()
        self.world = nil
    end
end

When destroying an Area we first destroy all objects in it and then we destroy the physics world if it exists. We can now change the Stage room to accomodate for this:

function Stage:destroy()
    self.area:destroy()
    self.area = nil
end

And then we can also change the gotoRoom function:

function gotoRoom(room_type, ...)
    if current_room and current_room.destroy then current_room:destroy() end
    current_room = _G[room_type](...)
end

We check to see if current_room is a variable that exists and if it contains a destroy attribute (basically we ask if its holding an actual room), and if it does then we call the destroy function. And then we proceed with changing to the target room.

It's important to also remember that now with the addition of the destroy function, all objects have to follow the following template:

NewGameObject = GameObject:extend()

function NewGameObject:new(area, x, y, opts)
    NewGameObject.super.new(self, area, x, y, opts)
end

function NewGameObject:update(dt)
    NewGameObject.super.update(self, dt)
end

function NewGameObject:draw()

end

function NewGameObject:destroy()
    NewGameObject.super.destroy(self)
end

Now, this is all well and good, but how do we test to see if we're actually removing things from memory or not? One blog post I like that answers that question is this one, and it offers a relatively simple solution to track leaks:

function count_all(f)
    local seen = {}
    local count_table
    count_table = function(t)
        if seen[t] then return end
            f(t)
	    seen[t] = true
	    for k,v in pairs(t) do
	        if type(v) == "table" then
		    count_table(v)
	        elseif type(v) == "userdata" then
		    f(v)
	        end
	end
    end
    count_table(_G)
end

function type_count()
    local counts = {}
    local enumerate = function (o)
        local t = type_name(o)
        counts[t] = (counts[t] or 0) + 1
    end
    count_all(enumerate)
    return counts
end

global_type_table = nil
function type_name(o)
    if global_type_table == nil then
        global_type_table = {}
            for k,v in pairs(_G) do
	        global_type_table[v] = k
	    end
	global_type_table[0] = "table"
    end
    return global_type_table[getmetatable(o) or 0] or "Unknown"
end

I'm not going to over what this code does because its explained in the article, but add it to main.lua and then add this inside love.load:

function love.load()
    ...
    input:bind('f1', function()
        print("Before collection: " .. collectgarbage("count")/1024)
        collectgarbage()
        print("After collection: " .. collectgarbage("count")/1024)
        print("Object count: ")
        local counts = type_count()
        for k, v in pairs(counts) do print(k, v) end
        print("-------------------------------------")
    end)
    ...
end

And so what this does is that whenever you press f1, it will show you the amount of memory before a garbage collection cycle and the amount of memory after it, as well as showing you what object types are in memory. This is useful because now we can, for instance, create a new Stage full of objects, delete it, and then see if the memory remains the same (or acceptably the same hehexD) as it was before the Stage was created. If it remains the same then we aren't leaking memory, if it doesn't then it means we are and we need to track down the cause of it.


Garbage Collection Exercises

75. Bind the f2 key to create and activate a new Stage with a gotoRoom call.

76. Bind the f3 key to destroy the current room.

77. Check the amount of memory used by pressing f1 a few times. After that spam the f2 and f3 keys a few times to create and destroy new rooms. Now check the amount of memory used again by pressing f1 a few times again. Is it the same as the amount of memory as it was first or is it more?

78. Set the Stage room to spawn 100 Player objects instead of only 1 by doing something like this:

function Stage:new()
    ...
    for i = 1, 100 do 
        self.area:addGameObject('Player', gw/2 + random(-4, 4), gh/2 + random(-4, 4))
    end
end

Also change the Player's update function so that Player objects don't move anymore (comment out the movement code). Now repeat the process of the previous exercise. Is the amount of memory used different? And do the overall results change?



The Power of Lua and Mixins

2014-01-07 06:30

This post deals with OOP and Lua, and since Lua has no OOP built-in by default I'll make use of a library. kikito has a great OOP library made for Lua that supports mixins. rxi also has one (it's the one I use now). Either way, as stated in one of those links: "Mixins can be used for sharing methods between classes, without requiring them to inherit from the same father". If you've read anything about component based system design, then you've read something like that quote but probably without the word mixin anywhere near it. Since I've been using mixins as the foundation of the entity code in Kara, in this post I'll explain their advantages and what you can possibly get out of them.


Objects, attributes and methods in Lua

The one thing to remember about Lua is that everything in it is a table, except the things that aren't. But pretty much all objects are going to be made out of tables (essentially hash tables) that have attribute and method names as keys and attribute values and functions as values. So, for instance, this:

Entity = class('Entity')
  
-- This is the constructor!
function Entity:init(x, y)
    self.x = x
    self.y = y
end
  
object = Entity(300, 400)

... is, for all that matters to this post, the same as having a table named object with keys init, x and y:

-- in Lua, table.something is the same as table["something"]
-- so basically accessing a key in a table looks like accessing 
-- an attribute of an object
  
object = {}
object["init"] = function(x, y)
    object["x"] = x
    object.y = y
end
  
object.init(300, 400)

Mixins

If you've looked at this mixins explanation it should be pretty clear what's going on. In case it isn't: an include call simply adds the defined functions of a certain mixin to a certain class, which means adding a new key to the table that is the object. In a similar fashion, a function that changes an object's attributes can be defined:

Mixin = {
    mixinFunctionInit = function(self, x, y)
        self.x = x
        self.y = y
    end
}
  
Entity = class('Entity')
Entity:include(Mixin)
  
function Entity:init(x, y)
    self.mixinFunctionInit(self, x, y)
    -- here self:mixinFunctionInit(x, y) could also have been used
    -- since in Lua : is shorthand for calling a function while 
    -- passing self as the first argument
end
  
object = Entity(300, 400)

This example is exactly the same as the first one up there, except that instead of directly setting the x and y attributes, the mixin does it. The include call adds the mixinFunctionInit function to the class, then calling that function makes changing the object's attributes possible (by passing self, a reference to the object being modified). It's a very easy and cheap way of getting some flexibility into your objects. That flexibility can get you the following:


Reusability

Component based systems were mentioned, and mixins are a way of getting there. For instance, these are the initial calls for two classes in Kara, the Hammer and a Blaster (an enemy):

Hammer = class('Hammer', Entity)
Hammer:include(PhysicsRectangle)
Hammer:include(Timer)
Hammer:include(Visual)
Hammer:include(Steerable)
...
  
Blaster = class('Blaster', Entity)
Blaster:include(Timer)
Blaster:include(PhysicsRectangle)
Blaster:include(Steerable)
Blaster:include(Stats)
Blaster:include(HittableRed)
Blaster:include(EnemyExploder)
...

Notice how the PhysicsRectangle, Timer and Steerable mixins repeat themselves? That's reusability in its purest form. PhysicsRectangle is a simple wrapper mixin for LÖVE's box2d implementation, and, as you may guess, makes it so that the object becomes a physics object of rectangular shape. The Timer mixin implements a wrapper for the main Timer class, containing calls like after (do something after n seconds) or every (do something every n seconds). A bit off-topic, but since you can pass functions as arguments in Lua, timers are pretty nifty and have pretty simple calls that look like this:

-- somewhere in the code of a class that has the Timer mixin
-- after 2 seconds explode this object by doing everything needed in an explosion
self.timer:tween(2, self, {tint_color = {255, 255, 255}}, 'in-out-cubic')
self.timer:after(2, function()
    self.dead = true
    self:createExplosionParticles()
    self:createDeathParticles()
    self:dealAreaDamage({radius = 10, damage = 50})
    self.world:cameraShake({intensity = 5, duration = 1})
end)

Anyway, lastly, the Steerable mixin implements steering behaviors, which are used to control how far and in which way something moves towards a target. With a bit of work steering behaviors can be used in all sorts of ways to achieve basic entity behaviors while allowing for the possibility of more complex ones later on. In any case, those three mixins were reused with changes only to the variables passed in on their initialization and sometimes on their update function. Otherwise, it's all the same code that can be used by multiple classes. Consider the alternative, where reuse would have to rely on inheritance and somehow Hammer and Blaster would have to be connected in some weird way, even though they're completely different things (one is something the Player uses as a tool of attack, the other is an Enemy).

And consider the other alternative, which is the normal component based system. It usually uses some sort of message passing or another mechanism to get variables from other components in
the same object (kinda yucky!). While the mixin based system simply changes those attributes directly, because when you're coding a mixin you always have a reference to the object you're
coding to via the self parameter.


Object Creation

The mutability offered by mixins (in this case Lua is also important) also helps when you're creating objects. For instance, this is the single call that I need to create new objects in my game:

create = function(self, type, x, y, settings)
    table.insert(self.to_be_created, 
                {type = type, x = x, y = y, settings = settings})
end
  
-- Example:
enemy = create('Enemy', 400, 300, {velocity = 200, hp = 5, damage = 2}) 

And then this gets called every frame to create the objects inside the to_be_created list (this must be done because I can't create objects while box2d runs its own update cycle,
and it usually happens that the create function is called precisely while box2d is running):

createPostWorldStep = function(self)
    for _, o in ipairs(self.to_be_created) do
        local entity = nil
        if o.type then entity = _G[o.type](self, o.x, o.y, o.settings) end
        self:add(entity)
    end
    self.to_be_created = {}
end

Essentially, whenever I create a class like this: "Entity = class('Entity')" I'm actually creating a global variable called Entity that holds the Entity class definition (it's the table that is used as a prototype for creating Entity instances). Since Lua's global state is inside a table (and that table is called _G), I can access the class by going through that global table and then just calling its constructor with the appropriate parameters passed. And the clevererest part of this is actually the settings table. All entities in Kara derive from an Entity class, which looks like this:

Entity = class('Entity')
  
function Entity:init(world, x, y, settings)
    self.id = getUID()
    self.dead = false
    self.world = world
    self.x = x
    self.y = y
    if settings then
        for k, v in pairs(settings) do self[k] = v end
    end
end

The last few lines being the most important ones, as the attributes of the class are changed according to the settings table. So whenever an object is created you get a choice to add whatever attributes (or even functions) you want to an object, which adds a lot of flexibility, since otherwise you'd have to create many different classes (or add various attributes to the same class) for all different sorts of random properties that can happen in a game.


Readability

Reusable code is locked inside mixins and entity specific code is clearly laid out on that entity's file. This is a huge advantage whenever you're reading some entity's code because you never get lost into what is relevant and what isn't. For example, look at the code for the smoke/dust class:

Dust = class('Dust', Entity)
Dust:include(PhysicsRectangle)
Dust:include(Timer)
Dust:include(Fader)
Dust:include(Visual)
  
function Dust:init(world, x, y, settings)
    Entity.init(self, world, x, y, settings)
    self:physicsRectangleInit(self.world.world, x, y, 'dynamic', 16, 16)
    self:timerInit()
    self:faderInit(self.fade_in_time, 0, 160)
    self:visualInit(dust, Vector(0, 0)) 
  
    self.scale = math.prandom(0.5, 1)
    self.timer:tween(2, self, {scale = 0.25}, 'in-out-cubic')
    self.body:setGravityScale(math.prandom(-0.025, 0.025))
    self.body:setAngularVelocity(math.prandom(math.pi/8, math.pi/2))
    local r = math.prandom(0, 2*math.pi)
    local v = math.prandom(5, 10)
    self.body:setLinearVelocity(v*math.cos(r), v*math.sin(r))
    self:fadeIn(0, self.fade_in_time or 1, 'in-out-cubic')
    self:fadeOut(self.fade_in_time or 1, 1, 'in-out-cubic')
end
  
function Dust:update(dt)
    self.x, self.y = self.body:getPosition()
    self:timerUpdate(dt)
end
  
function Dust:draw()
    if debug_draw then self:physicsRectangleDraw() end
    self:faderDraw()
    self:visualDraw()
end

The entity specific code here happens in the constructor after the visualInit call. If I ever look back at this in another month, year or decade I will know that if I want to change the smoke's behavior I need to change something in that block of entity specific code. If I want to change how to smoke looks I have to change the first parameter in the visualInit call. If I have to change something in how the fading in or out of the effect works, I have to change something in the faderInit call. Everything is very properly divided and hidden and there's a clear way of knowing where everything is and what everything does.


ENDING

Hopefully if you could understand any of that you can see why Lua is great and why mixins are the best. There are certainly drawbacks to using this, but if you're coding by yourself or with only another programmer then the rules don't matter because you can do anything you want. You are a shining star and the world is only waiting for you to shine your bright beautiful light upon it.

Psychedelics and Finishing Games

14/07/19 - Psychedelics and Finishing Games

Indie game developers are already selected for high openness, given that openness is the personality trait most related to creativity and interest in aesthetics and ideas. The video above mentions that taking psychedelics has the effect of increasing someone's levels of openness by one standard deviation permanently after one year of taking the drug once, which is an absolutely huge increase. And so if you have an indie game developer, and you have them take psychedelics, chances are that you'll have a person who is now in the very top percentiles for openness compared to the rest of the population.

The way this manifests itself in real life is that indie developers who are higher in openness will be more likely to be hyper interested in the discovery part of making a game, but once the game is established enough, their inherent need to be working on something new (driven by high openness) will override everything else and they'll find themselves unable to see any projects through completion and will be stuck in a loop of always starting a new game. This is a well known problem among indie developers and it happens with many people who are just naturally high in openness without the involvement of drugs.

So my idea is that if indie developers want to be more likely to finish games, they should avoid taking psychedelics, especially if they can already notice in themselves this tendency to keep trying out new things all the time and never committing to a single one.

BYTEPATH #0 - Introduction

Introduction

This tutorial series will cover the creation of a complete game with Lua and LÖVE. It's aimed at programmers who have some experience but are just starting out with game development, or game developers who already have some experience with other languages or frameworks but want to figure out Lua or LÖVE better.

The game that will be created is a mix of Bit Blaster XL and Path of Exile's Passive Skill Tree. It's simple enough that it can be covered in a number of articles without extending for too long, but with enough content that a beginner would feel uncomfortable with the code and end up giving up before finishing.

It's also at a level of complexity that most game development tutorials don't cover. Most of the problems beginners have when starting out with game development have to do with scope. The usual advice is to start small and work your way up, and while that might be a good idea, if the types of projects you're interested in cannot be made any smaller then there are very few resources out there that attempt to guide you through the problems that come up.

In my case, I've always been interested in making games with lots and lots of items/passives/skills and so when I was starting out it was really hard to figure out a good way to structure my code so that I wouldn't get lost. Hopefully these tutorials can help someone with that.


Requirements

Before you start there are some programming knowledge requirements:

  • The basics of programming, like variables, loops, conditionals, basic data structures and so on;

  • The basics of OOP, like knowing what classes, instances, attributes and methods are;

  • And the very basics of Lua, this quick tutorial should be good enough.

Essentially this is not for people who are just getting started with programming in general. Also, this tutorial series will have exercises. If you've ever been in the situation where you finish a tutorial and you don't know what to do next it's probably because it had no exercises, so if you don't want that to happen here then I recommend at least trying to do them.


Contents

1. Game Loop

2. Libraries

3. Rooms and Areas

4. Exercises

5. Game Basics

6. Player Basics

7. Player Stats and Attacks

8. Enemies

9. Director and Gameplay Loop

10. Coding Practices

11. Passives

12. More Passives

13. Skill Tree

14. Console

15. Final



Comments on /r/programming

Comments on Hacker News

Comments on /r/gamedev

Automatic Game Updates with LÖVE

2015-08-21 17:30

This post will cover how to create an automatic game updater for your LÖVE game. The main benefit of having an automatic game updater is only having to send players an executable once and then from there they'll always be up to date without having to manually download anything else. I was surprised at how simple this was to get working with LÖVE so I thought I'd share.


Intro

Before we get into the meat of this there's some base knowledge required:

  1. LÖVE games can be distributed in two ways, either as a .love file or as an executable. An executable is an executable and a .love file is simply a renamed zip with its contents arranged in a certain way: main.lua, which is the entry point of LÖVE programs, has to be a top level file on the zip (see more about this here).

  2. There's a function called love.filesystem.mount which lets you mount a zip file or folder from the game's save directory into the current environment. So say you have a bunch of zipped assets in the save directory and you want to access them. You'd do something like this:

    love.filesystem.mount('assets.zip', 'assets')
    image = love.graphics.newImage('assets/foo.png')
  3. .love files are renamed zips and love.filesystem.mount mounts zips, so it stands to reason that .love files can be mounted into the current environment.

  4. When you use require to load a file in a Lua program, it's cached in the package.loaded table. To reload a file while the game is running we can just nil its reference in package.loaded and then require it again. This is the main way in which you can achieve live coding features with Lua/LÖVE (see lurker/lume). For instance, say the programmer changed the Player.lua file and you want to reload it while the game is running so that you don't have to close + rerun the whole program again. What you'd do is create a reload function and then call this function whenever needed:

    function reload()
      package.loaded.Player = nil
      Player = require('Player')
    end

Updater

With all that super hot info in mind we can start connecting the dots and getting ideas about maybe how the auto-updater might work. But I'll give you a hint, it has to do with:

  1. Code your game;
  2. Make it a .love file and upload it somewhere;
  3. Create a new LÖVE program (the updater) that:
    • Downloads the .love file
    • Mounts the .love file
    • package.loaded.main = nil, package.loaded.conf = nil
    • Reloads everything by calling love.init() and love.load()

And there you have it! The game was mounted into the updater's environment, the current main.lua (and conf.lua, which is LÖVE's configuration file) was unloaded and then everything was reloaded again with love.init() and love.load(), but since the updater's code was just unloaded, all that's left is the mounted code that was inside the .love file, which means that instead of reloading the updater's code we reload the game's code (since main.lua is a top level file).

So now whenever the user runs the updater it will first do all checks it needs to do to see if there's a new version available, download it if there is and then it will load the game, which is just a .love file in the user's save directory.


Code

We'll use two libraries to do this: async so we can make an HTTP request without locking the LÖVE thread and luajit-request so we can make an HTTP request.

One thing I failed to mention about how the updater works is the version checking logic. The way I'm doing it is that I have a version file that is automatically updated and uploaded somewhere as I commit and push the game to version control. This file will then be used so that the updater can check which is the most up to date version, so that if the current version on the user's computer is lower, the new one will be downloaded.


Version checks

Anyway, after getting those libraries you can create the first HTTP request for the version file:

local async = require('async')

function love.load(args)
  -- Define asynchronous version request
  local version_request = async.define('version_request', function()
    local request = require('luajit-request')
    local response = request.send(link to the version file)
    return response.body, response.code
  end)

  -- Request the version
  local version = nil
  version_request(function(result, status)
    if status == 200 then
      version = getVersion(result) -- define getVersion however you want based on your version file   
    end
  end)
end

function love.update(dt)
  async.update()
end

So now if everything went right we should have to most up to date version number in the version variable. Now what we need to do is check to see if the version that exists on the user's save directory matches the one in the version variable or not. The way I do this is based on the .love file's name. After downloading the game, I always save it like this:

love.filesystem.write('game_' .. version .. '.love', result)

This writes the result of the HTTP request that we haven't written yet (the one that downloads the game) as the file game_1.0.2.love, for instance. So assuming that this is the case, for us to check versions all we have to do is:

-- Request the version
version_request(function(result, status)
  if status == 200 then
    version = getVersion(result) 
    if not love.filesystem.isFile('game_' .. version .. '.love') then
      -- download the new version, mount and run the game
    else 
      -- mount the existing version and run the game
    end
  end
end)

Downloading the game

Now for the game HTTP request. This is very similar to the previous one, with the only difference that we have to pass in the version variable (at least the way I'm doing it, but you could be doing version checks in a way that doesn't require this, either way, you get the general idea):

-- Define asynchronous game request
local game_request = async.define('game_request', function(version)
  local request = require('luajit-request')
  local response = request.send(link to the version file using the version variable)
  return response.body, response.code
end)

And

-- Request the version
version_request(function(result, status)
  if status == 200 then
    version = getVersion(result) 
    if not love.filesystem.isFile('game_' .. version .. '.love') then
      -- Request the game
      game_request(function(result, status)
        if status == 200 then
          love.filesystem.write('game_' .. version .. '.love', result)
          -- mount and run
      end, version)
    else 
      -- mount and run
    end
  end
end)

Now all there's left is the part where we mount and run the game.

Mount and run

This one is rather straight forward. All we have to do is, as previously stated, mount the .love file, unload main.lua and conf.lua, then call love.init() and love.load():

-- Request the version
version_request(function(result, status)
  if status == 200 then
    version = getVersion(result) 
    if not love.filesystem.isFile('game_' .. version .. '.love') then
      -- Request the game
      game_request(function(result, status)
        if status == 200 then
          love.filesystem.write('game_' .. version .. '.love', result)
          -- Mount and run
          love.filesystem.mount('game_' .. version .. '.love', '')
          package.loaded.main = nil
          package.loaded.conf = nil
          love.conf = nil
          love.init()
          love.load(args)
      end, version)
    else 
      -- Mount and run
      love.filesystem.mount('game_' .. version .. '.love', '')
      package.loaded.main = nil
      package.loaded.conf = nil
      love.conf = nil
      love.init()
      love.load(args)
    end
  end
end)

END

And after this everything should work fine. Of course there's a lot of stuff you can and should add, like error checking. Or some super cool animation with your game's title being engulfed in the fiery flames of hell. Or a mini-game if the download is big. Whatever, you can do anything because the updater is a LÖVE game as well so you can code whatever you want in it, and since the HTTP request is asynchronous it will run in the background while the main LÖVE thread does it's own stuff. You can find the full file here and credit goes to Billiam for creating a gist that does the same thing which was what I used to guide me through the shadowy paths of confusion

The Value of Copypasting

2016-02-05 13:46

Yesterday I read a blog post, this one, about code reuse patterns. I think it's mostly correct in its assessments but IMO it doesn't do justice to the Copy and Paste technique. So in this post I'll explain why I think copypasting your own code instead of properly making it reusable is a good idea more often than you'd think, especially if you're an indie developer working on an indie game. Also, if you're surprised like this guy at how "bad" game code can be, hopefully this article will shed some light into why that's often true.


Reuse

My main argument is that the costs of copypasting are often lower than the ongoing costs of properly making things reusable. So first let's try an example showing the process of making reusable code. This example will use functions but the same thought process applies to classes, mixins, components, inheritance and what have you that you decide to use to reuse code.

So assume we want to add functionality X to the codebase and it's made up of three logical parts, A, B and C. I'd do that like this:

-- A, explains what A does
block 1
block 2
block 3

-- B, explains what B does
block 4
block 5

-- C, explains what C does
block 6
block 7
block 8

If I'm only using X in one place I'd probably write it like I wrote it up there, with simple comments (-- are comments) dividing the different logical sections and explaining what each section does when necessary. If I'm calling X from multiple places then I'd wrap the above code in a function named X.

This is all good and simple so far. But now suppose we want to add functionality Y. And this new functionality is really similar to X, in that it uses A, B and C, except A has to be a bit different, maybe block 2 has to be changed for block 9, and after block 3 there should be an additional block 10.

So now first what we can do is take B and C out into their own functions, since they're going to be reused by X and Y:

function B()
  block 4
  block 5
end

function C()
  block 6
  block 7
  block 8
end

And then what we can do is either create a new function D, which will be the modified A, or modify A such that it takes into account the possibilities that functionality Y needs. The first solution would look like this:

...
function A()
  block 1
  block 2
  block 3
end

function D()
  block 1
  block 9
  block 3
  block 10
end

function X()
  A()
  B()
  C()
end

function Y()
  D()
  B()
  C()
end

The problem with this solution is that now block 1 and block 3 are repeated on A and D and since we want to minimize code repetition this won't work.

The second solution avoids code duplication by doing this:

...
function A(y_flag)
  block 1

  if d_flag
    block 9
  else
    block 2

  block 3

  if d_flag
    block 10
end

function X()
  A()
  B()
  C()
end

function Y()
  A(y_flag = true)
  B()
  C()
end

This solution is better from a code duplication perspective because it gets rid of it, but it also has its own problems in that as the needs of A grow with more and more functionalities, it will become a confusing land of random conditionals and settings that is going to be really hard to logically follow.

Anyway, either solution chosen, suppose now that we want to add functionality Z, which, again, is really similar to both X and Y but slightly different. Now A is also a little different where block 3 needs to be block 12. And again, we can go through the same exercise of either choosing to create a new function E that will be exactly what Z needs:

function A()
  block 1
  block 2
  block 3
end

function D()
  block 1
  block 9
  block 3
  block 10
end

function E()
  block 1
  block 2
  block 12
end

function X()
  A()
  B()
  C()
end

function Y()
  D()
  B()
  C()
end

function Z()
  E()
  B()
  C()
end

Or changing A to suit Z's needs:

...
function A(y_flag, z_flag)
  block 1

  if y_flag
    block 9
  else
    block 2

  if z_flag
    block 12
  else
    block 3

  if y_flag
    block 10
end

function X()
  A()
  B()
  C()
end

function Y()
  A(y_flag = true)
  B()
  C()
end

function Z()
  A(z_flag = true)
  B()
  C()
end

Either way we choose, we lose. This is because this exercise will keep going on and on forever as we add new functionality to our codebase, and as the codebase grows, whatever we did to minimize code duplication will make it fundamentally more complex. The solution where you add a new function adds a new node to your code, and with a new node now you have more dependencies to think about and worry about. The solution where you add flags and settings adds logical complexity to the flow of the affected function, which makes the code a lot harder to reason about. Any other solution out of this problem will do either of those things and complexity will grow.

The point here is not that code reuse is bad, but that there are ongoing costs to making things reusable. The cost here is that at each point you have to add a new functionality, you'll have to think about which solution to choose to minimize duplication and which previous components/functions that exist in the codebase that you should look at to change and/or use. The process of building reusable code is one where you have to think about how to integrate new functionality against existing code instead of just adding new functionality. This is a huge mental resource drain that most people forget to take into account.


Copypasting

Now let's look at the same example through a copypasting lens. We add functionality X:

function X()
  -- A
  block 1
  block 2
  block 3

  -- B
  block 4
  block 5

  -- C
  block 6
  block 7
  block 8
end

Now we want to add functionality Y, which is a bit different from X in the ways described earlier. Instead of worrying about what can and can't be reused, we'll just copy paste X and change what needs to be changed:

function Y()
  -- A
  block 1
  block 9
  block 3
  block 10

  -- B
  block 4
  block 5

  -- C
  block 6
  block 7
  block 8
end

Now we want to add functionality Z, which is similar to X and Y. Again, instead of worrying about what can and can't be reused, we'll just copy paste X again and change what needs to be changed:

function Z()
  -- A
  block 1
  block 2
  block 11

  -- B
  block 4
  block 5

  -- C
  block 6
  block 7
  block 8
end

Isn't this process much much simpler? This takes no mental effort at all. You literally just copypaste and change what needs to be changed. Yes, there are very obvious problems with copypasting. If we want to change how B works for instance now we have 3 places where we have to make that change. And that's a bad thing! But my point is that people should know when the costs of doing this are higher than the ongoing costs of making things reusable and when they aren't.

The upfront costs of copypasting are usually a lot lower than the upfront costs of making things reusable. This is a fact. The long term costs of copypasting are usually a lot higher than the long term costs of making things reusable. This is another fact. But people often forget about the first fact and that they can use both of them to make reasonable and responsible trades in their codebase. The lower upfront costs of copypasting can be extremely useful in a huge number of situations, but a lot of people default to reuse always, and that's a huge waste of mental resources and a huge unnecessary increase in complexity.


Frequency of Change

One of the ways you can use to figure out when using copypasting actually makes sense is looking at the frequency of change of some part of your codebase. If in the example above we know (through experience or instinct) that C is a piece of code that has a very low frequency of change, then it makes a lot of sense to not care about it from a code duplication perspective, because most of the time it won't be changed.

For instance, suppose that for every 20 functionalities we add that are similar enough to X, only once does C change. In this case it makes sense to put C away as a function, call it from X and then just copypaste X each time for each new functionality. When C changes (which doesn't happen often), we'll either have to change the function itself or add a new function similar enough to C, which is exactly what we did for A in the example above. The difference here is that instead of defaulting to reuse, we default to copypaste and use reuse where it actually makes sense to use it. And we do this because the situation called for it, not because we just decided that this was going to be a rule that we always follow.


Example

So now for a real example. This is an attack in one of the games I'm working on:

It's a 3 part melee attack, light -> medium -> heavy punches as the player presses the same key over and over. All that's happening to make this happen is: player presses key, character plays some animation, a hitbox is created to see if the attack hit anyone, then if it hit someone a series of events happen, like the enemy gets pushed back and loses some HP, and the camera shakes, and some particles fly around and so on. To get this attack going I need 3 main files.

The first file is an attack object. This gets attached to the player and then input is directed to the current attack attached to the key pressed. The attack object has state and logic needed to make the player side of this attack work, which is logic needed for which animation to play (if the sequence is in light, medium or heavy punch stage), cooldowns, pushing the player forward or backwards a bit and so on. This is also where the melee hitbox gets created.

The second file is the melee hitbox. This is just a normal hitbox that collides with relevant enemies to see if the attack hit them or not. If it collides, this calls the appropriate function for this attack and passes as arguments to it the hitbox, the player, the enemy hit and some other additional necessary data.

The third file is the function that gets called by the melee hitbox. I call these functions events. Basically they're functions that will receive all interested parties of this attack and do things to them, like deal damage to enemies, give HP back to the player if it has some life leech passive, shake the screen, add some effects, create particles and so on.

Out of these files, the first and third ones are the ones I'm constantly copy pasting. If I want to add another 3 stage attack for instance, but with kick animations instead of punches, I'll just copy paste the 3 stage punch and change what's needed to change. The same goes for the third file. If some event is similar enough to the event of another attack I'll just copy paste it. And this is the optimal course of action for attacks. Because I'm constantly adding attacks to the game and very rarely changing how attacks in general work. The times where I need to change something for all attacks happen, but they're really rare compared to the times where I have to add a new attack. So it makes the most sense to minimize costs of adding new attacks by levering the low upfront costs of copypasting.


The general rule here is that a lot of the times it's better to optimize for process simplification than it is to optimize for code reuse. And this is especially true with indie game development. Simple processes for doing things to your game require less mental effort, which means you can work for longer and you're less likely to get demotivated by high complexity.

At the same time, when you do need to do something dumb like changing the general case in 20 places because you didn't abstract things properly, this is dumb work that is also very easy to do and that you don't need to think about, which also minimizes mental effort spent. And on top of this, since gameplay code is often changing in unexpected ways, it's usually better to start from dumb code and generalize from there than it is to start from generalized code and working your way backwards.


END

Hopefully we've all learned something from this. One thing I noticed is that often times I see indie developers with amazing games, but by all normal standards if you were to look at their code they would be considered bad coders who just don't know any good practices.

I think intuitively people realize that doing things like what I outlined in this article is a good idea, and so you get lots of "dumb" code that works. It's important to understand that if you're able to analyze these tradeoffs properly you can make informed decisions about how to engage in those dumb activities while decreasing the overall costs of making whatever you want to make.

Also don't let other people bully you into "proper" code and tell you your code is dumb. Tell them they're dumb instead and that their code is even dumber and that they should be ashamed of trying to shame you. And tell them Undertale has dumb code so it's okay to have dumb code too. Look:

The Indiepocalypse Isn't Real

This talk on YouTube by Tom Giardino from Valve goes over the state of Steam in 2018. He gives a lot of good information but in regards to Valve's decision to "open the floodgates" he shows these graphs:

From these graphs we can tell that Steam has been getting a higher amount of games succeeding over the years. Another interesting graph from the talk is this one:

And so based on these we know that more games have been succeeding on Steam than ever before, and that Steam users are buying more new games than ever before. The argument Indiepocalypsers always make is that as Steam opened up, it became harder to succeed. And even with those graphs you could still make that argument.

If in year 1 50 games made $100K in the first 30 days, and in year 5 100 games made $100K in the first 30 days, that seems like pretty good progress. But if the total number of games released in year 1 was 500 and the total number of games released in year 5 was 5000, we have a pretty drastic decrease in the percentage of successful games (from 10% to 2%).

So all things being equal, your chances of succeeding are lower in year 5 than they were in year 1. But this makes one big assumption, which is that all things are equal. If you're willing to concede that you're an average indie developer making average games then yes, based on this logic the Indiepocalypse is very real. If you don't consider that to be the case though then things are looking increasingly better :-)

For more on this I've written two relevant articles before: Luck Isn't Real and Hidden Gems Don't Exist

Behavior Trees #2

2014-08-17 03:46

This tutorial features a full implementation of behavior for an enemy using behavior trees. The previous part went over the basics of behavior trees as well as their implementation. So look there first if you somehow got here randomly. The enemy behavior we're going to implement is actually quite simple from a player/viewer standpoint, but it's a nice way to introduce most concepts behind behavior trees.

The enemy is going to have three (3) main modes of behaving:

  • Idle
  • Suspicious
  • Threatened

In the idle mode the enemy will be able to either: wander around its spawn point, talk to another enemy or practice his TK (telekinesis) on a nearby object. The suspicious mode is triggered whenever the player gets close enough to an enemy. When it is triggered the enemy spawns a question mark above his head and turns towards the player. If the player leaves this trigger area then the enemy goes back to the idle mode, otherwise he stays here. The threatened mode happens when the player gets really really close to the enemy, in which case the enemy will spawn an angry face above his head, chase the player around and attempt to hit him.


Wander

Using the findWanderPoint and moveToPoint actions from the last article, we can start building the idle wander behavior. A small addition to that particular sequence is adding an action that makes the entity wait around for a few seconds before choosing the next point, otherwise he moves around too much.

wait = Action:extend()
  
function wait:new(duration)
    wait.super.new(self, 'wait')
  
    self.wait_duration = duration
    self.done = false
end
  
function wait:update(dt, context)
    return wait.super.update(self, dt, context)
end
  
function wait:run(dt, context)
    if self.done then return 'success'
    else return 'running' end
end
  
function wait:start(context)
    context.object.timer:after(self.wait_duration, function() self.done = true end)
end
  
function wait:finish(status, context)
    self.done = false
end

Here, like with the Timer decorator, we just set a timer so that after wait_duration seconds this action returns success. Before that happens it will just return running and the sequence won't be able to move on, which will make the entity do nothing. The tree for the behavior looks like this:

We use a sequence to bind everything together. If at any point moveToPoint fails, then wait will not run and the tree will just start over by finding a new point. If the tree succeeds then the entity will have done what we wanted it to do: move to a point and then wait around a bit. After that the tree will just restart and so on...

    ...
    self.behavior_tree = Root(self,
        Sequence('wander', {
            findWanderPoint(self.x, self.y, 100),
            moveToPoint(),
            wait(5),
        })
    )
    ...


TK Practice

As you can see in that gif there's a box lying around. To add some more personality to each enemy we're going to make it so that sometimes they practice their TK on objects around the map. The idea of this game I'm working on is that it's a school full of people with TK, so it makes sense that sometimes they'd practice it!

To do this we're going to need two actions: one to check if there's any object that can be TKed around, and another to do the actual lifting. First, let's do the one that finds an object:

anyAround = Action:extend()
  
function anyAround:new(object_types, radius)
    anyAround.super.new(self, 'anyAround')
  
    self.object_types = object_types
    self.radius = radius
end
  
function anyAround:update(dt, context)
    return anyAround.super.update(self, dt, context)
end
  
function anyAround:run(dt, context)
    -- Query for entities of object_types around a circle of 
    -- radius radius centered in x, y
    local x, y = context.object.body:getPosition()
    local entities = context.object.world:queryAreaCircle(x, y, 
                     self.radius, self.object_types)
    -- If no entities then fail
    if #entities == 0 then 
        return 'failure'
    else
        -- Pick a random entity that isn't the entity this tree is attached to,
        -- set context.any_around_entity to point it and then succeed
        local entity = entities[math.random(1, #entities)]
        while entity.id == context.object.id do 
            entity = entities[math.random(1, #entities)] 
        end
        context.any_around_entity = entity 
        return 'success'
    end
end
  
function anyAround:start(context)
  
end
  
function anyAround:finish(status, context)
  
end

anyAround is a conditional type of action that will ask a question to the game's world and return the answer. In this case, the side effect is changing context.any_around_entity to point to some entity of some type around a certain point with some radius. The idea is that before the NPC can move to the object he wants to lift he needs to first find it. Separating finding and moving makes it so that both can be reused later and, as we'll see, anyAround will be reused a lot.

There's one problem so far, though. Ideally we want this subtree to look like this:

But anyAround sets context.any_around_entity to point to the entity, while moveToPoint looks for context.move_to_point_target to move to the target. There are two solutions to this: either add an additional node that always succeeds and translates any_around_entity to move_to_point_target, or change moveToPoint so that it also looks for any_around_entity and then translates that into a point it can use. I'll use the second and this small refactor will be omitted.

Finally, the TKLift action, which does all the lifting work, uses any_around_entity and changes its z velocity:

tkLift = Action:extend()
  
function tkLift:new()
    tkLift.super.new(self, 'tkLift')
  
    self.done = false
end
  
function tkLift:update(dt, context)
    return tkLift.super.update(self, dt, context)
end
  
function tkLift:run(dt, context)
    if self.done then return 'success' end
    if context.any_around_entity then
        -- Change z velocity so that the targetted entity goes up a bit
        context.any_around_entity.v_z = -mg.utils.math.random(10, 30)
        return 'running'
    else return 'failure' end
end
  
function tkLift:start(context)
    context.object.tk_charging = true
    -- This action lasts between 0.5 and 1.5 seconds
    context.object.timer:after({0.5, 1.5}, function() self.done = true end)
end
  
function tkLift:finish(status, context)
    -- Clean everything up, including removing the reference to the 
    -- targetted entity from the context. Since Lua's GC uses 
    -- reference counting to remove its objects from memory this 
    -- is super important!!!
    context.any_around_entity = nil
    context.object.tk_charging = false
    self.done = false
end

And then after that we tie it all to a sequence. And since we want the enemy to do one thing or the other, either try to lift objects or wander around, we use a selector on top of that. The order in which we place each subtree also matters a lot. Suppose we do selector -> wander, lift. The wander sequence rarely fails, which means that the selector would succeed whenever wander succeeded, which means that tkLift would never really be picked. tkLift fails whenever there isn't a particular object around, which means that placing it before the wander subtree makes more sense.

There's still a problem, though. As you can see in the gif, whenever the NPC finds a box once he'll be addicted to lifting it. That's because there are no checks in place to keep this from happening. The anyAround query will always return success after the first time, because there will be a box around, and so the whole lifting sequence will be performed and it will repeat again forever. To prevent this from happening we can either try creating a decorator or performing an additional check by creating another action. I'll choose to go with the decorator one:

DontSucceedInARow = Decorator:extend()
  
function DontSucceedInARow:new(behavior)
    DontSucceedInARow.super.new(self, 'DontSucceedInARow', behavior)
  
    self.past_status = 'invalid'
end
  
function DontSucceedInARow:update(dt, context)
    return DontSucceedInARow.super.update(self, dt, context)
end
  
function DontSucceedInARow:run(dt, context)
    local status = self.behavior:update(dt, context)
    -- If success now and the last run was successful as well then fail!
    if status == 'success' and self.past_status == 'success' then
        -- Dirty hack: since moveToPoint now also reads in 
        -- context.any_around_entity, we remove the reference here 
        -- to avoid adding a node that does this. If this reference 
        -- isn't removed then the NPC will still be addicted to 
        -- following the same box around, it'll just never lift 
        -- the box twice in a row. And this reference can't 
        -- be removed in anyAround either because there's nowhere 
        -- to do it there where it won't affect behaviors that might 
        -- need to use it.
        context.any_around_entity = nil
        self.past_status = 'failure'
        return 'failure'
    else 
        self.past_status = status
        return status 
    end
end
  
function DontSucceedInARow:start(context)
  
end
  
function DontSucceedInARow:finish(status, context)
  
end

Now whenever anyAround succeeds twice in a row a failure will be forced. That failure existing, the tree will be able to move on to try out the wander subtree, which means the NPC won't get stuck in the same box again.

And so the whole tree looks like this:

And the code to do that like this:

    ...
    self.behavior_tree = Root(self,
        Selector('idle', {
            Sequence('TK practice', {
                DontSucceedInARow(anyAround({'Box'}, 50)),
                moveToPoint(),
                tkLift(),
            }),
  
            Sequence('wander', {
                findWanderPoint(self.x, self.y, 100),
                moveToPoint(),
                wait(5),
            })
        })
    )
    ...

Talking to Friends 1

This is the first kinda complex behavior and there are multiple ways of going about doing it. The goal is to get the NPCs to talk to each other somehow. The way I'll do it is by creating FriendMeetingPoint objects, to which NPCs will be able to attach themselves to and wait around for other NPCs to attach themselves to those points and then they'll be able to talk. The first step to doing that is finding and moving to one of those FriendMeetingPoints. Similarly to how we move to a box in the tkLift behavior, we can use anyAround and moveToPoint:

And this works fine. However, there's some repetition going on there. The DontSucceedInARow, anyAround -> moveToPoint subtree looks exactly the same in TK practice as it does in friend talk. Luckily, there's a way of removing this repetition by storing subtrees!


Storing Subtrees

Storing a subtree simply means that you'll take the way in which those nodes are arranged and you'll store it so you don't have to type it all again whenever you wanna reuse it. Think of it like a function that you call and that builds that whole subtree with the arguments you pass to it.

And the way to achieve that, at least in Lua, is exactly like thinking of it as a function. We simply create a file that returns a function that returns the built subtree:

return function(object_types, radius)
    return Sequence('findAndMoveTo', {
        DontSucceedInARow(anyAround(object_types, radius)),
        moveToPoint(),
    })
end

And then to call it:

    ...
    self.behavior_tree = Root(self,
        Selector('idle', {
            Sequence('TK practice', {
                findAndMoveTo({'Box'}, 50),
                tkLift(),
            }),
  
            Sequence('friend talk', {
                findAndMoveTo({'FriendMeetingPoint'}, 100),
            }),
  
            Sequence('wander', {
                findWanderPoint(self.x, self.y, 100),
                moveToPoint(),
                wait(5),
            })
        })
    )
    ...

Talking to Friends 2

Back to friends, I'll omit two actions because they're extremely dependent on how I've coded the FriendMeetingPoint entity: attachTo, which attaches the NPC to the FriendMeetingPoint entity and anyAroundAttached, which looks for a friendly NPC that is also attached to a FriendMeetingPoint entity. The subtree now looks like this:

The WaitUntil decorator waits until the child node returns success before it can return success. This means that once an entity is attached to a FriendMeetingPoint, it'll stay there until some other NPC comes along to talk to it.

WaitUntil = Decorator:extend()
  
function WaitUntil:new(behavior)
    WaitUntil.super.new(self, 'WaitUntil', behavior)
end
  
function WaitUntil:update(dt, context)
    return WaitUntil.super.update(self, dt, context)
end
  
function WaitUntil:run(dt, context)
    local status = self.behavior:update(dt, context)
    if status == 'success' then return 'success'
    else return 'running' end
end
  
function WaitUntil:start(context)
  
end
  
function WaitUntil:finish(status, context)
  
end

After this we add one more action before the actual talk action, which is separate. In case two NPCs attach themselves to the same FriendMeetingPoint we want to separate them a bit before they start talking, otherwise it looks a little too weird. The separate action uses the separation steering behavior that the entities in my game have, so it's pretty simple code wise:

separate = Action:extend()
  
function separate:new(radius)
    separate.super.new(self, 'separate')
  
    self.radius = radius
end
  
function separate:update(dt, context)
    return separate.super.update(self, dt, context)
end
  
function separate:run(dt, context)
    return 'success'   
end
  
function separate:start(context)
    context.object:separationOn(self.radius)
end
  
function separate:finish(status, context)
  
end

And finally we add the talk action, which contains most of the work:

talk = Action:extend()
  
function talk:new(talk_duration)
    talk.super.new(self, 'talk')
  
    self.talk_duration = talk_duration
    self.done = false
end
  
function talk:update(dt, context)
    return talk.super.update(self, dt, context)
end
  
function talk:run(dt, context)
    if self.done then return 'success'
    else 
        if context.any_around_attached_entity then
            context.object:turnTowards(context.any_around_attached_entity)
        end
        return 'running' 
    end
end
  
function talk:start(context)
    -- Timers so that they start talking slightly after they turn towards 
    -- each other, more precisely 0.5 seconds after. Similarly, set a 
    -- timer so that this entity stops talking after the talk duration 
    -- and so that this node returns success (self.done).
    context.object.timer:after(0.5, function() 
        context.object:setTalking(self.talk_duration - 1) 
    end)
    context.object.timer:after(0.5 + self.talk_duration - 1, function() 
        context.object:stopTalking() 
    end)
    context.object.timer:after(self.talk_duration, function() 
        self.done = true 
    end)
end
  
function talk:finish(status, context)
    -- Normal clean up...
    self.done = false
    context.any_around_attached_entity = nil
    context.object.attached_to = nil
    -- Turn the separation behavior off here!
    context.object:separationOff()
end

With this, NPCs should move around randomly, find boxes and lift them with their TK sometimes, and whenever they get close to a FriendMeetingPoint they'll go there to try to talk to someone. There's the possibility they'll be stuck there forever if no one shows up, but that's a detail that can be fixed by adding a time limit to the WaitUntil decorator for instance. Other than that we have a working implementation of an idle behavior for a particular NPC of this game. It wasn't super simple but to me at least it beats the approach I was taking before! (which was just hardcoding everything)

The whole tree now looks like this:

And the code:

    ...
    self.behavior_tree = Root(self,
        Selector('idle', {
            Sequence('TK practice', {
                findAndMoveTo({'Box'}, 50),
                tkLift(),
            }),
  
            Sequence('friend talk', {
                findAndMoveTo({'FriendMeetingPoint'}, 100),
                attachTo('FriendMeetingPoint', 10),
                WaitUntil(anyAroundAttached('Student', 'FriendMeetingPoint', 80)),
                separate(10),
                talk(5),
            }),
  
            Sequence('wander', {
                findWanderPoint(self.x, self.y, 100),
                moveToPoint(),
                wait(5),
            })
        })
    )
    ...

Suspicious

Now that the idle subtree is done we can move on to the suspicious behavior. This behavior will check to see if the player is around, if it is then it will set the NPC to be suspicious, which will turn him towards the player, change his animation to combat mode and spawn a little question mark above his head. This lasts a few seconds, after which we perform another check to see if the player is still inside the suspicious trigger area, if he is, then we bail out of the sequence and fail, and if he isn't then we set the NPC back to normal using an unsuspicious action.

The subtree should look like this:

I'm going to omit the specifis of each behavior since by now you should have a good idea of how I'm coding them and how you're coding them. If you're following along and trying this out with your game your tree may look slightly different based on how you've coded each behavior, but that just happens, since there are multiple ways of achieving the same thing. In any case, the whole tree should look like this:

We tie everything up with a selector at the top and by placing the suspicious behavior to the left. We want the tree to first check for any suspicious activity around, and then if that fails, be able to go to idle stuff. The code looks like this:

    ...
    self.behavior_tree = Root(self,
        Selector('meleeEnemy', {
            Sequence('suspicious', {
                anyAround({'Player'}, 150),
                suspicious(),
                Inverter(anyAround({'Player'}, 150)),
                unsuspicious(),
            }),
  
            Selector('idle', {
                Sequence('TK practice', {
                    findAndMoveTo({'Box'}, 50),
                    tkLift(),
                }),
  
                Sequence('friend talk', {
                    findAndMoveTo({'FriendMeetingPoint'}, 100),
                    attachTo('FriendMeetingPoint', 10),
                    WaitUntil(anyAroundAttached('Student', 'FriendMeetingPoint', 80)),
                    separate(10),
                    talk(5),
                }),
  
                Sequence('wander', {
                    findWanderPoint(self.x, self.y, 100),
                    moveToPoint(),
                    wait(5),
                })
            })
        })
    )
    ...

As you can see on that gif though, there's a problem here. Whenever the player enters the suspicious trigger area (yellow circle), the NPC doesn't become suspicious immediately. This is because he's still waiting for the wait action to be finished on the idle subtree, which means that he won't check the suspicious subtree until it ends. We want it to check this immediately, as soon as it happens, but using a normal selector that won't be possible.


Active Selector

An active selector behaves like a selector, except instead of returning to the node that is still running, it always checks all children. So in our case whenever wait returns running, on the next frame, instead of just going directly to that node and running it again, it will run the suspicious subtree first, see if it returned running or failure, and then move on to the idle subtree where it will pick up from the running wait node. If suspicious succeeds, however, since it behaves like a selector it won't run the idle subtree.

ActiveSelector = Behavior:extend()
  
function ActiveSelector:new(name, behaviors)
    ActiveSelector.super.new(self)
    self.name = name
    self.behaviors = behaviors
end
  
function ActiveSelector:update(dt, context)
    return ActiveSelector.super.update(self, dt, context)
end
  
function ActiveSelector:run(dt, context)
    -- Logic is exactly the same as a normal selector, except instead 
    -- of keeping track of which behavior we're in by using current_behavior, 
    -- we just don't do it at all and go through them all every frame.
    for _, behavior in ipairs(self.behaviors) do
        local status = behavior:update(dt, context)
        if status ~= 'failure' then return status end
    end
    return 'failure'
end
  
function ActiveSelector:start(context)
  
end
  
function ActiveSelector:finish(status, context)
    
end

And now it should behave accordingly because it will always first check the suspicious subtree (since it's first on the list). Note that this is, in a way, parallelism. Although isn't real because it's doing each in sequence, for the purposes of the tree it is real, since what matters is what happens over multiple frames, and this makes it so that things happen in one frame simultaneously. For the threatened subtree I'll introduce yet another node called the Parallel, which does something similar to the active selector but has different success/failure logic.


Threatened

For the threatened behavior the same active selector logic will apply. We need the NPC to be threatened immediately when the Player comes too close. Similarly to suspicious, we'll use anyAround for this:

Now what we wanna do is make it so that when the NPC is threatened, it chases the player around and tries to hit him if it gets close enough. One way of doing that is, after the threatened node, add a subtree that repeatedly chases and tries to hit the player while doing so:

repeatUntilFail will make it so that this whole sequence of checking to see if the player is still close and then chasing/punching is repeated forever, which means the NPC will chase the player until he distances himself enough. After that, the only node we don't really know anything about (assuming the implementation of chase and meleeAttack) is the Parallel one.

The implementation of the parallel node is probably one of the most complex ones, but it's similar to the active selector in the sense that it always goes through all nodes. The only difference is that on top of receiving behaviors as arguments it can also receive failure and success policies. A failure policy defines when the parallel node will fail, same for the success one. Both types of policies have two possible values: one or all. An one failure policy means that the parallel node will fail whenever one of its nodes fail. An all success policy means that the parallel node will succeed only when all of its children succeed, and so on...

Parallel = Behavior:extend()
  
function Parallel:new(name, success_policy, failure_policy, behaviors)
    Parallel.super.new(self)
    self.name = name
    self.success_policy = success_policy
    self.failure_policy = failure_policy
    self.behaviors = behaviors
end
  
function Parallel:update(dt, context)
    return Parallel.super.update(self, dt, context)
end
  
function Parallel:run(dt, context)
    local success_count, failure_count = 0, 0 
    for _, behavior in ipairs(self.behaviors) do
        local status = nil
        status = behavior:update(dt, context)
   
        if status == 'success' then
            success_count = success_count + 1
            behavior:finish(status, context)
            -- Got one success and the success policy only requires one, so succeed
            -- same for failure in the next if statement.
            if self.success_policy == 'one' then return 'success' end
        end
  
        if status == 'failure' then
            failure_count = failure_count + 1
            behavior:finish(status, context)
            if self.failure_policy == 'one' then return 'failure' end
        end
    end
  
    -- Has been through all behaviors, the failure/success policy is 'all' 
    -- and the number ofbehaviors matches the failure/success count? 
    -- Then fail/succeed!
    if self.failure_policy == 'all' and failure_count == #self.behaviors then 
        return 'failure' 
    end
    if self.success_policy == 'all' and success_count == #self.behaviors then 
        return 'success' 
    end
    return 'running'
end
  
function Parallel:start(context)
      
end
  
function Parallel:finish(status, context)
    -- If the parallel node is done, finish all currently running behaviors so that 
    -- the next time the node is run it starts over instead of continuing 
    -- the previous run.
    for _, behavior in ipairs(self.behaviors) do
        if behavior.status == 'running' then
            behavior:finish(status, context)
        end
    end
end

For the attack subtree we used ('all', 'all'), which means that we want it to fail or succeed whenever both behaviors fail or succeed, which is rare. chase always succeeds. anyAround can fail if the player is far away enough from the NPC. meleeAttack always succeeds. Whenever they all succeed, the parallel node will succeed, which means the sequence will succeed, which will return success to repeatUntilFail, meaning that the whole subtree will be repeated again. The only way out of this subtree is if the first anyAround outside of the parallel node fails, which is the intended behavior.

And with that we have a working attacking enemy! The code for the whole tree now looks like this:

    ...
    self.behavior_tree = Root(self,
        ActiveSelector('meleeEnemy', {
            Sequence('attack', {
                anyAround({'Player'}, 75),
                threatened(),
                RepeatUntilFail(Sequence('chase', {
                    anyAround({'Player'}, 75),
                    Parallel('chaseAttack', 'all', 'all', {
                        chase(),
                        Sequence('attack', {
                            anyAround({'Player'}, 30),
                            meleeAttack(),
                        }),
                    }),
                })),
                Inverter(anyAround({'Player'}, 75)),
                unthreatened(),
            }),
  
            Sequence('suspicious', {
                anyAround({'Player'}, 150),
                suspicious(),
                Inverter(anyAround({'Player'}, 150)),
                unsuspicious(),
            }),
  
            Selector('idle', {
                Sequence('TK practice', {
                    findAndMoveTo({'Box'}, 50),
                    tkLift(),
                }),
  
                Sequence('friend talk', {
                    findAndMoveTo({'FriendMeetingPoint'}, 100),
                    attachTo('FriendMeetingPoint', 10),
                    WaitUntil(anyAroundAttached('Student', 'FriendMeetingPoint', 80)),
                    separate(10),
                    talk(5),
                }),
  
                Sequence('wander', {
                    findWanderPoint(self.x, self.y, 100),
                    moveToPoint(),
                    wait(5),
                })
            })
        })
    )
    ...

And the final tree looks like this:


END

And that's it! If you've followed through all this, even if just reading the explanations and code, you should have a nice idea of what behavior trees can do and how they do it. Hopefully this helped you in ways that I wanted to be helped when I was trying to understand all this. There are tons of issues with the way I'm doing it and there are tons of resources that go beyond what my implementation does, but since my game isn't really memory nor performance intensive I can get away with the simple solution. The resources on the first paragraph of the first part mostly cover these issues.

Why do you host a blog on GitHub Issues?

Just asking. Ther are literally countless more common and convenient ways to make a blog, like Blogspot, Wordpress, or even a static GitHub Pages site powered by something like Jekyll.

BYTEPATH #4 - Exercises

@SSYGEA

In the previous three tutorials we went over a lot of code that didn't have anything to do directly with the game. All of that code can be used independently of the game you're making which is why I call it the engine code in my head, even though I guess it's not really an engine. As we make more progress in the game I'll constantly be adding more and more code that falls into that category and that can be used across multiple games. If you take anything out of these tutorials that code should definitely be it and it has been extremely useful to me over time.

Before moving on to the next part where we'll start with the game itself you need to be comfortable with some of the concepts taught in the previous tutorials, so here are some more exercises. If you can't do some of them it's fine, but it's important to at least try!


Exercises

1. Create a getGameObjects function inside the Area class that works as follows:

-- Get all game objects of the Enemy class
all_enemies = area:getGameObjects(function(e)
    if e:is(Enemy) then
        return true
    end
end)

-- Get all game objects with over 50 HP
healthy_objects = area:getGameObjects(function(e)
    if e.hp and e.hp >= 50 then
        return true
    end
end)

It receives a function that receives a game object and performs some test on it. If the result of the test is true then the game object will be added to the table that is returned once getGameObjects is fully run.

2. What is the value in a, b, c, d, e, f and g?

a = 1 and 2
b = nil and 2
c = 3 or 4
d = 4 or false
e = nil or 4
f = (4 > 3) and 1 or 2
g = (3 > 4) and 1 or 2

3. Create a function named printAll that receives an unknown number of arguments and prints them all to the console. printAll(1, 2, 3) will print 1, 2 and 3 to the console and printAll(1, 2, 3, 4, 5, 6, 7, 8, 9) will print from 1 to 9 to the console, for instance. The number of arguments passed in is unknown and may vary.

4. Similarly to the previous exercise, create a function named printText that receives an unknown number of strings, concatenates them all into a single string and then prints that single string to the console.

5. How can you trigger a garbage collection cycle?

6. How can you show how much memory is currently being used up by your Lua program?

7. How can you trigger an error that halts the execution of the program and prints out a custom error message?

8. Create a class named Rectangle that draws a rectangle with some width and height at the position it was created. Create 10 instances of this class at random positions of the screen and with random widths and heights. When the d key is pressed a random instance should be deleted from the environment. When the number of instances left reaches 0, another 10 new instances should be created at random positions of the screen and with random widths and heights.

9. Create a class named Circle that draws a circle with some radius at the position it was created. Create 10 instances of this class at random positions of the screen with random radius, and also with an interval of 0.25 seconds between the creation of each instance. After all instances are created (so after 2.5 seconds) start deleting once random instance every [0.5, 1] second (a random number between 0.5 and 1). After all instances are deleted, repeat the entire process of recreation of the 10 instances and their eventual deletion. This process should repeat forever.

10. Create a queryCircleArea function inside the Area class that works as follows:

-- Get all objects of class 'Enemy' and 'Projectile' in a circle of 50 radius around point 100, 100
objects = area:queryCircleArea(100, 100, 50, {'Enemy', 'Projectile'})

It receives an x, y position, a radius and a list of strings containing names of target classes. Then it returns all objects belonging to those classes inside the circle of radius radius centered in position x, y.

11. Create a getClosestGameObject function inside the Area class that works follows:

-- Get the closest object of class 'Enemy' in a circle of 50 radius around point 100, 100
closest_object = area:getClosestObject(100, 100, 50, {'Enemy'})

It receives the same arguments as the queryCircleArea function but returns only one object (the closest one) instead.

12. How would you check if a method exists on an object before calling it? And how would you check if an attribute exists before using its value?

13. Using only one for loop, how can you write the contents of one table to another?

14. Like I did for Nuclear Throne and The Binding of Isaac, break down the game Recettear in terms of rooms. Watch this gameplay video up to to 2 minutes or so (when the player moves on to the next level of the dungeon) to get an idea of most of the screens involved.

15. Break down the game Shovel Knight in terms of rooms. Watch this video up to the end when the overworld is shown (you can skip most of the gameplay in the middle) to get an idea of most of the screens involved.

Unfinished Thoughts

07/06/19 - Battle Royales, Plans and The Role of Video Games

The part of the video linked above touches upon something I found out from playing BR games a few years ago, especially PUBG when it was more popular.

One thing that happens in those games is that they have down periods where you're not really doing anything and you're just sort of waiting for the next circle to be shown. In those periods I often found myself either literally waiting (camping) somewhere, or just going around aimlessly trying to find people to fight.

But I started noticing that doing this made me feel somewhat uncomfortable for multiple reasons. One of them is that often times I would find someone to fight but I'd be in an awkward spot since I hadn't thought much about what I was doing. But another was likely what was pointed out in the video, which is that I was in "prey mode". meaning I didn't have a plan of attack and I was just aimlessly going around and waiting for something, which likely made me more stressed and would explain why I felt uncomfortable.

So what I instead instinctively decided to do more was to always have a very well defined plan of what I was going to do next, and then I would act out that plan until I had new information to create a new plan and so on. This seems obvious but actively deciding to do this changed how I approached the game drastically and the uncomfortable feeling I had before vanished completely.

The plan could literally be just waiting in a house until the next circle came, but because it was an actual plan I was doing that while paying attention to all the things that could go wrong, so I'd pick a good house, for instance. And similarly, if I decided to move instead I would decide to move somewhere very specific for some specific reason, like I saw someone going in that direction and I'm going to try to find them, because if you see that someone is going somewhere with a specific car and then you see the specific car somewhere you just know that they're around and then you won't really be surprised by someone showing up randomly. You'll choose ways to move around that always put you into an advantageous if the person you were following was to show up in a general direction.

Anyway, the point being, always having a plan was just the better decision and it's line with what was said in that video. You want to attack problems actively and be the predator rather than just wait around and be the prey.

Now, another thing about this is that this process happened over the 400 hours or so that I played PUBG. It wasn't that I just suddenly decided to do this and did it, it was something that developed over time as I learned to play the game more and more and got quite good at it. And one of the things that is interesting to me about games is that you can actually go through this kind of subtle (in the action to action, short term sense) but significant (in the week to week, month to month long term sense) process that sort of teaches you something in an embodied way. Before going through this process in PUBG, I would never really get what that portion in that video really meant, but because my body has physically gone through this process what was said in that video actually makes real sense to me. And everyone sort of has had this experience in one way or another, where someone says something and you really get it because you've been through the same thing, right?

So I think that this is a very important role that video games can play in society and bridge the gap in understanding between people. Because initially, before I was like 20 or so, the way I thought about how people are went something like: "everyone is basically good and rational, and if everyone has access to the same facts and has similar mental capacities they would reach the same conclusion", but this is seriously wrong and naive, because that's not how people are at all. People are highly motivated by internal and biological things that they have little control over and that they mostly don't understand, and it's reasonable to make the argument that in a lot of cases what people say is mostly informed by those internal drives and not by their higher order processes of reason and logic. And you can easily see that this is the case in things like politics or even discussions over things like programming languages.

And so what games can do, like they did for me with PUBG, is to actually have people embody another way of looking at the world and making them really understand what it's like to be more like that. The only problem is that this has to be done at a very fundamental level that's very hard to break down logically. For instance, I would say that a game like PUBG, on top of being a shooting game and exercising all the skills that come with that, essentially is training some people (like me) to both be more assertive and to also come up with plans and execute them better.

But I guess this is a subtle effect and it's hard to really know how much being better at PUBG helped me in those domains generally (assertiveness and planning) and not just in that one game. I don't know how transferable the skills are, is what I mean. And perhaps there are people out there who were already good at planning and the way they approached PUBG and got better at it was to plan less and learn to be more flexible, right? It's totally possible.

Then the question becomes, is PUBG normalizing people towards some optimal level of assertiveness and planning that the game itself requires, or are people just playing mostly in the way that their personalities allow them to and there's lots of variance to how you can win, and therefore lots of variance to the types of temperaments that can be successful in it? Or maybe all these points I just talked about are completely overshadowed by just being more skilled with a mouse, which could totally be the case...

BYTEPATH #1 - Game Loop

Start

To start off you need to install LÖVE on your system and then figure out how to run LÖVE projects. The LÖVE version we'll be using is 0.10.2 and it can be downloaded here. If you're in the future and a new version of LÖVE has been released you can get 0.10.2 here. You can follow the steps from this page for further details. Once that's done you should create a main.lua file in your project folder with the following contents:

function love.load()

end

function love.update(dt)

end

function love.draw()

end

If you run this you should see a window popup and it should show a black screen. In the code above, once your LÖVE project is run the love.load function is run once at the start of the program and love.update and love.draw are run every frame. So, for instance, if you wanted to load an image and draw it, you'd do something like this:

function love.load()
    image = love.graphics.newImage('image.png')
end

function love.update(dt)

end

function love.draw()
    love.graphics.draw(image, 0, 0)
end

love.graphics.newImage loads the image texture to the image variable and then every frame it's drawn at position 0, 0. To see that love.draw actually draws the image on every frame, try this:

love.graphics.draw(image, love.math.random(0, 800), love.math.random(0, 600))

The default size of the window is 800x600, so what this should do is randomly draw the image around the screen really fast:

Note that between every frame the screen is cleared, otherwise the image you're drawing randomly would slowly fill the entire screen as it is drawn in random positions. This happens because LÖVE provides a default game loop for its projects that clears the screen at the end of every frame. I'll go over this game loop and how you can change it now.


Game Loop

The default game loop LÖVE uses can be found in the love.run page, and it looks like this:

function love.run()
    if love.math then
	love.math.setRandomSeed(os.time())
    end

    if love.load then love.load(arg) end

    -- We don't want the first frame's dt to include time taken by love.load.
    if love.timer then love.timer.step() end

    local dt = 0

    -- Main loop time.
    while true do
        -- Process events.
        if love.event then
	    love.event.pump()
	    for name, a,b,c,d,e,f in love.event.poll() do
	        if name == "quit" then
		    if not love.quit or not love.quit() then
		        return a
		    end
	        end
		love.handlers[name](a,b,c,d,e,f)
	    end
        end

	-- Update dt, as we'll be passing it to update
	if love.timer then
	    love.timer.step()
	    dt = love.timer.getDelta()
	end

	-- Call update and draw
	if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled

	if love.graphics and love.graphics.isActive() then
	    love.graphics.clear(love.graphics.getBackgroundColor())
	    love.graphics.origin()
            if love.draw then love.draw() end
	    love.graphics.present()
	end

	if love.timer then love.timer.sleep(0.001) end
    end
end

When the program starts love.run is run and then from there everything happens. The function is fairly well commented and you can find out what each function does on the LÖVE wiki. But I'll go over the basics:

if love.math then
    love.math.setRandomSeed(os.time())
end

In the first line we're checking to see if love.math is not nil. In Lua all values are true, except for false and nil, so the if love.math condition will be true if love.math is defined as anything at all. In the case of LÖVE these variables are set to be enabled or not in the conf.lua file. You don't need to worry about this file for now, but I'm just mentioning it because it's in that file that you can enable or disable individual systems like love.math, and so that's why there's a check to see if it's enabled or not before anything is done with one of its functions.

In general, if a variable is not defined in Lua and you refer to it in any way, it will return a nil value. So if you ask if random_variable then this will be false unless you defined it before, like random_variable = 1.

In any case, if the love.math module is enabled (which it is by default) then its seed is set based on the current time. See love.math.setRandomSeed and os.time. After doing this, the love.load function is called:

if love.load then love.load(arg) end

arg are the command line arguments passed to the LÖVE executable when it runs the project. And as you can see, the reason why love.load only runs once is because it's only called once, while the update and draw functions are called multiple times inside a loop (and each iteration of that loop corresponds to a frame).

-- We don't want the first frame's dt to include time taken by love.load.
if love.timer then love.timer.step() end

local dt = 0

After calling love.load and after that function does all its work, we verify that love.timer is defined and call love.timer.step, which measures the time taken between the two last frames. As the comment explains, love.load might take a long time to process (because it might load all sorts of things like images and sounds) and that time shouldn't be the first thing returned by love.timer.getDelta on the first frame of the game.

dt is also initialized to 0 here. Variables in Lua are global by default, so by saying local dt it's being defined only to the local scope of the current block, which in this case is the love.run function. See more on blocks here.

-- Main loop time.
while true do
    -- Process events.
    if love.event then
        love.event.pump()
        for name, a,b,c,d,e,f in love.event.poll() do
            if name == "quit" then
                if not love.quit or not love.quit() then
                    return a
                end
            end
            love.handlers[name](a,b,c,d,e,f)
        end
    end
end

This is where the main loop starts. The first thing that is done on each frame is the processing of events. love.event.pump pushes events to the event queue and according to its description those events are generated by the user in some way, so think key presses, mouse clicks, window resizes, window focus lost/gained and stuff like that. The loop using love.event.poll goes over the event queue and handles each event. love.handlers is a table of functions that calls the relevant callbacks. So, for instance, love.handlers.quit will call the love.quit function if it exists.

One of the things about LÖVE is that you can define callbacks in the main.lua file that will get called when an event happens. A full list of all callbacks is available here. I'll go over callbacks in more detail later, but this is how all that happens. The a, b, c, d, e, f arguments you can see passed to love.handlers[name] are all the possible arguments that can be used by the relevant functions. For instance, love.keypressed receives as arguments the key pressed, its scancode and if the key press event is a repeat. So in the case of love.keypressed the a, b, c values would be defined as something while d, e, f would be nil.

-- Update dt, as we'll be passing it to update
if love.timer then
    love.timer.step()
    dt = love.timer.getDelta()
end

-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled

love.timer.step measures the time between the two last frames and changes the value returned by love.timer.getDelta. So in this case dt will contain the time taken for the last frame to run. This is useful because then this value is passed to the love.update function, and from there it can be used in the game to define things with constant speeds, despite frame rate changes.

if love.graphics and love.graphics.isActive() then
    love.graphics.clear(love.graphics.getBackgroundColor())
    love.graphics.origin()
    if love.draw then love.draw() end
    love.graphics.present()
end

After calling love.update, love.draw is called. But before that we verify that the love.graphics module exists and that we can draw to the screen via love.graphics.isActive. The screen is cleared to the defined background color (initially black) via love.graphics.clear, transformations are reset via love.graphics.origin, love.draw is finally called and then love.graphics.present is used to push everything drawn in love.draw to the screen. And then finally:

if love.timer then love.timer.sleep(0.001) end

I never understood why love.timer.sleep needs to be here at the end of the frame, but the explanation given by a LÖVE developer here seems reasonable enough.

And with that the love.run function ends. Everything that happens inside the while true loop is referred to as a frame, which means that love.update and love.draw are called once per frame. The entire game is basically repeating the contents of that loop really fast (like at 60 frames per second), so get used to that idea. I remember when I was starting it took me a while to get an instinctive handle on how this worked for some reason.

There's a helpful discussion on this function on the LÖVE forums if you want to read more about it.

Anyway, if you don't want to you don't need to understand all of this at the start, but it's helpful to be somewhat comfortable with editing how your game loop works and to figure out how you want it to work exactly. There's an excellent article that goes over different game loop techniques and does a good job of explaining each. You can find it here.


Game Loop Exercises

1. What is the role that Vsync plays in the game loop? It is enabled by default and you can disable it by calling love.window.setMode with the vsync attribute set to false.

2. Implement the Fixed Delta Time loop from the Fix Your Timestep article by changing love.run.

3. Implement the Variable Delta Time loop from the Fix Your Timestep article by changing love.run.

4. Implement the Semi-Fixed Timestep loop from the Fix Your Timestep article by changing love.run.

5. Implement the Free the Physics loop from the Fix Your Timestep article by changing love.run.



BYTEPATH #10 - Coding Practices

Introduction

In this article I'll talk about some "best coding practices" and how they apply or not to what we're doing in this series. If you followed along until now and did most of the exercises (especially the ones marked as content) then you've probably encountered some possibly questionable decisions in terms of coding practices: huge if/elseif chains, global variables, huge functions, huge classes that do a lot of things, copypasting and repeating code around instead of properly abstracting it, and so on.

If you're a somewhat experienced programmer in another domain then that must have set off some red flags, so this article is meant to explain some of those decisions more clearly. In contrast to all other previous articles, this one is very opinionated and possibly wrong, so if you want to skip it there's no problem. We won't cover anything directly related to the game, even though I'll use examples from the game we're coding to give context to what I'm talking about. The article will talk about two main things: global variables and abstractions. The first will just be about when/how to use global variables, and the second will be a more general look at when/how to abstract/generalize things or not.

Also, if you've bought the tutorial then in the codebase for this article I've added some code that was previously marked as content in exercises, namely visuals for all the player ships, all attacks, as well as objects for all resources, since I'll use those as examples here.


Global Variables

The main advice people give each other when it comes to global variables is that you should generally avoid using them. There's lots of discussion about this and the reasoning behind this advice is generally fair. In a general sense, the main problem that comes with using global variables is that it makes things more unpredictable than they need to be. As the last link above states:

To elaborate, imagine you have a couple of objects that both use the same global variable. Assuming you're not using a source of randomness anywhere within either module, then the output of a particular method can be predicted (and therefore tested) if the state of the system is known before you execute the method.

However, if a method in one of the objects triggers a side effect which changes the value of the shared global state, then you no longer know what the starting state is when you execute a method in the other object. You can now no longer predict what output you'll get when you execute the method, and therefore you can't test it.

And this is all very good and reasonable. But one of the things that these discussions always forget is context. The advice given above is reasonable as a general guideline, but as you get more into the details of whatever situation you find yourself in you need to think clearly if it applies to what you're doing or not.

And this is something I'll repeat throughout this article because it's something I really believe in: advice that works for teams of people and for software that needs to be maintained for years/decades does not work as well for solo indie game development. When you're coding something mostly by yourself you can afford to cut corners that teams can't cut, and when you're coding video games you can afford to cut even more corners that other types of software can't because games need to be maintained for a lower amount of time.

The way this difference in context manifests itself when it comes to global variables is that, in my opinion, we can use global variables as much as we want as long as we're selective about when and how to use them. We want to gain most of the benefits we can gain from them, while avoiding the drawbacks that do exist. And in this sense we also want to take into account the advantages we have, namely, that we're coding by ourselves and that we're coding video games.


Types of Global Variables

In my view there are three types of global variables: those that are mostly read from, those are that are mostly written to, and those that are read from and written to a lot.

Type 1

The first type are global variables that are read from a lot and rarely written to. Variables like these are harmless because they don't really make the program any more unpredictable, as they're just values that are there and will be the same always or almost always. They can also be seen as constants.

An example of a variable like this in our game is the variable all_colors that holds the list of all colors. Those colors will never change and that table will never be written to, but it's read from various objects whenever we need to get a random color, for instance.

Type 2

The second type are global variables that are written to a lot and rarely read from. Variables like these are mostly harmless because they also don't really make the program any more unpredictable as they're just stores of values that will be used in very specific and manageable circumstances.

In our game so far we don't really have any variable that fits this definition, but an example would be some table that holds data about how the player plays the game and then sends all that data to a server whenever the game is exited. At all times and from many different places in our codebase we would be writing all sorts of information to this table, but it would only be read and changed slightly perhaps once we decide to send it to the server.

Type 3

The third type are global variables that are written to a lot and read from a lot. These are the real danger and they do in fact increase unpredictability and make things harder for us in a number of different ways. When people say "don't use global variables" they mean to not use this type of global variable.

In our game we have a few of these, but I guess the most prominent one would be current_room. Its name already implies some uncertainty, since the current room could be a Stage object, or a Console object, or a SkillTree object, or any other sort of Room object. For the purposes of our game I decided that this would be a reasonable hit in clarity to take over trying to fix this, but it's important to not overdo it.


The main point behind separating global variables into types like these is to go a bit deeper into the issue and to separate the wheat from the chaff, let's say. Our productivity would be harmed quite a bit if we tried to be extremely dogmatic about this and avoid global variables at all costs. While avoiding them at all costs works for teams and for people working on software that needs to be maintained for a long time, it's very unlikely that the all_colors variable will harm us in the long run. And as long as we keep an eye on variables like current_room and make sure that they aren't too numerous or too confusing (for instance, current_room is only changed whenever the gotoRoom function is called), we'll be able to keep most things under control.

Whenever you see or want to use a global variable, think about what type of global variable it is first. If it's a type 1 or 2 then it probably isn't a problem. If it's a type 3 then it's important to think about when and how frequently it gets written to and read from. If you're writing to it from random objects all over the codebase very frequently and reading it from random objects all over the codebase then it's probably not a good idea to make it a global. If you're writing to it from a very small set of objects very infrequently, and reading it from random objects all over the codebase, then it's still not good, but maybe it's manageable depending on the details. The point is to think critically about these issues and not just follow some dogmatic rule.


Abstracting vs. Copypasting

When talking about abstractions what I mean by it is a layer of code that is extracted out of repeated or similar code underneath it in order to be used and reused in a more constrained and well defined manner. So, for instance, in our game we have these lines:

local direction = table.random({-1, 1})
self.x = gw/2 + direction*(gw/2 + 48)
self.y = random(16, gh - 16)

And they are the same on all objects that need to be spawned from either left or right of the screen at a random y position. I think so far about 6-7 objects have these 3 lines at their start. The argument for abstraction here would say that since these lines are being repeated on multiple objects we should consider abstracting it up somehow and have those objects enjoy that abstraction instead of having to have those repeated lines of code all over. We could implement this abstraction either through inheritance, components, a function, or some other mechanism. For the purposes of this discussion all those different ways will be treated as the same thing because they show the same problems.

Now that we're on the same page as to what we're talking about, let's get into it. The main discussion in my view around these issues is one of adding new code against existing abstractions versus adding new code freely. What I mean by this is that whenever we have abstractions that help us in one way, they also have (often hidden) costs that slow us down in other ways.


Abstracting

In our example above we could create some function/component/parent class that would encapsulate those 3 lines and then we wouldn't have to repeat them everywhere. Since components are all the rage these days, let's go with that and call it SpawnerComponent (but again, remember that this applies to functions/inheritance/mixins and other similar methods of abstraction/reuse that we have available). We would initialize it like spawner_component = SpawnerComponent() and magically it would handle all the spawning logic for us. In this example it's just 3 lines but the same logic applies to more complex behaviors as well.

The benefits of doing this is that now everything that deals with the spawning logic of objects in our game is constrained to one place under one interface. This means that whenever we want to make some change to the spawning behavior, we have to change it in one place only and not go over multiple files changing everything manually. These are well defined benefits and I'm definitely not questioning them.

However, doing this also has costs, and these are largely ignored whenever people are "selling" you some solution. The costs here make themselves apparent whenever we want to add some new behavior that is kinda like the old behavior, but not exactly that. And in games this happens a lot.

So, for instance, say now that we want to add objects that will spawn exactly in the middle of the screen. We have two options here: either we change SpawnerComponent to accept this new behavior, or we make a new component that will implement this new behavior. In this case the obvious option is to change SpawnerComponent, but in more complex examples what you should do isn't that obvious. The point here being that now, because we have to add new code against the existing code (in this case the SpawnerComponent), it takes more mental effort to do it given that we have to consider where and how to add the functionality rather than just adding it freely.


Copypasting

The alternative option, which is what we have in our codebase now, is that these 3 lines are just copypasted everywhere we want the behavior to exist. The drawbacks of doing this is that whenever we want to change the spawning behavior we'll have to go over all files tediously and change them all. On top of that, the spawning behavior is not properly encapsulated in a separate environment, which means that as we add more and more behavior to the game, it could be harder to separate it from something else (it probably won't remain as just those 3 lines forever).

The benefits of doing this, however, also exist. In the case where we want to add objects that will spawn exactly in the middle of the screen, all we have to do is copypaste those 3 lines from a previous object and change the last one:

local direction = table.random({-1, 1})
self.x = gw/2 + direction*(gw/2 + 48)
self.y = gh/2

In this case, the addition of new behavior that was similar to previous behavior but not exactly the same is completely trivial and doesn't take any amount of mental effort at all (unlike with the SpawnerComponent solution).

So now the question becomes, as both methods have benefits and drawbacks, which method should we default to using? The answer that people generally talk about is that we should default to the first method. We shouldn't let code that is repeated stay like that for too long because it's a "bad smell". But in my opinion we should do the contrary. We should default to repeating code around and only abstract when it's absolutely necessary. The reason for that is...


Frequency and Types of Changes

One good way I've found of figuring out if some piece of code should be abstracted or not is to look at how frequently it changes and in what kind of way it changes. There are two main types of changes I've identified: unpredictable and predictable changes.

Unpredictable Changes

Unpredictable changes are changes that fundamentally modify the behavior in question in ways that go beyond simple small changes. In our spawning behavior example above, an unpredictable change would be to say that instead of enemies spawning from left and right of the screen randomly, they would be spawned based on a position given by a procedural generator algorithm. This is the kind of fundamental change that you can't really predict.

These changes are very common at the very early stages of development when we have some faint idea of what the game will be like but we're light on the details. The way to deal with those changes is to default to the copypasting method, since the more abstractions we have to deal with, the harder it will be to apply these overarching changes to our codebase.

Predictable Changes

Predictable changes are changes that modify the behavior in small and well defined ways. In our spawning behavior example above, a predictable change would be the example used where we'd have to spawn objects exactly in the middle y position. It's a change that actually changes the spawning behavior, but it's small enough that it doesn't completely break the fundamentals of how the spawning behavior works.

These changes become more and more common as the game matures, since by then we'll have most of the systems in place and it's just a matter of doing small variations or additions on the same fundamental thing. The way to deal with those changes is to analyze how often the code in question changes. If it changes often and those changes are predictable, then we should consider abstracting. If it doesn't change often then we should default to the copypasting idea.


The main point behind separating changes into these two different types is that it lets us analyze the situation more clearly and make more informed decisions. Our productivity would be harmed if all we did was default to abstracting things dogmatically and avoiding repeated code at all costs. While avoiding that at all costs works for teams and for people working on software that needs to be maintained for al ong time, it's not the case for indie games being written by one person.

Whenever you get the urge to generalize something, think really hard about if it's actually necessary to do that. If it's a piece of code that is not changing often then worrying about it at all is unnecessary. If it is changing often then is it changing in a predictable or unpredictable manner? If it's changing in an unpredictable manner then worrying about it too much and trying to encapsulate it in any way is probably a waste of effort, since that encapsulation will just get in the way whenever you have to change the whole thing in a big way. If it's changing in a predictable manner, though, then we have potential for real abstraction that will benefit us. The point is to think critically about these issues and not just follow some dogmatic rule.


Examples

We have a few more examples in the game that we can use to further discuss these issues:


Left/Right Movement

This is something that is very similar to the spawning code, which is the behavior of all entities that just move either left or right in a straight line. So this applies to a few enemies and most resources. The code that directs this behavior generally looks something like this and it's repeated across all these entities:

function Rock:new(area, x, y, opts)
    ...

    self.w, self.h = 8, 8
    self.collider = self.area.world:newPolygonCollider(createIrregularPolygon(8))
    self.collider:setPosition(self.x, self.y)
    self.collider:setObject(self)
    self.collider:setCollisionClass('Enemy')
    self.collider:setFixedRotation(false)
    self.v = -direction*random(20, 40)
    self.collider:setLinearVelocity(self.v, 0)
    self.collider:applyAngularImpulse(random(-100, 100))
  
  	...
end

function Rock:update(dt)
    ...

    self.collider:setLinearVelocity(self.v, 0) 
end

Depending on the entity there are very small differences in the way the collider is set up, but it's really mostly the same. Like with the spawning code, we could make the argument that abstracting this into something else, like maybe a LineMovementComponent or something would be a good idea.

The analysis here is exactly as before. We need to think about how often this behavior is changed across all these entities. The answer to that is almost never. The behavior that some of those entities have to move left/right is already decided and won't change, so it doesn't make sense to worry about it at all, which means that it's alright to repeat it around the codebase.


Player Ship Visuals and Trails

If you did most of the exercises, there's a piece of code in the Player class that looks something like this:

It's basically two huge if/elseifs, one to handle the visuals for all possible ships, and another to handle the trails for those ships as well. One of the things you might think when looking at something like this is that it needs to be PURIFIED. But again, is it necessary? Unlike our previous examples this is not code that is repeating itself over multiple places, it's just a lot of code being displayed in sequence.

One thing you might think to do is to abstract all those different ship types into different files, define their differences in those files and in the Player class we just read data from those files and it would be all clean and nice. And that's definitely something you could do, but in my opinion it falls under unnecessary abstraction. I personally prefer to just have straight code that shows itself clearly rather than have it spread over multiple layers of abstraction. If you're really bothered by this big piece of code right at the start of the Player class, you can put this into a function and place it at the bottom of the class. Or you can use folds, which is something your editor should support. Folds look like this in my editor, for instance:


Player Class Size

Similarly, the Player class now has about 500 lines. In the next article where we'll add passives this will blow up to probably over 2000 lines. And when you look at it the natural reaction will be to want to make it neater and cleaner. And again, the question to be asked is if it's really necessary to do that. In most games the Player class is the one that has the most functionality and often times people go through great lengths to prevent it from becoming this huge class where everything happens.

But for the same reasons as why I decided to not abstract away the ship visuals and trails in the previous example, it wouldn't make sense to me to abstract away all the various different logical parts that make up the player class. So instead of having a different file for player movement, one for player collision, another for player attacks, and so on, I think it's better to just put it all in one file and end up with a 2000 Player class. The benefit-cost ratio that comes from having everything in one place and without layers of abstraction between things is higher than the benefit-cost ratio that comes from properly abstracting things away (in my opinion!).


Entity Component Systems

Finally, the biggest meme of all that I've seen take hold of solo indie developers in the last few years is the ECS one. I guess by now you can kind of guess my position on this, but I'll explain it anyway. The benefits of ECSs are very clear and I think everyone understands them. What people don't understand are the drawbacks.

By definition ECS are a more complicated system to start with in a game. The point is that as you add more functionality to your game you'll be able to reuse components and build new entities out of them. But the obvious cost (that people often ignore) is that at the start of development you're wasting way more time than needed building out your reusable components in the first place. And like I mentioned in the abstracting/copypasting section, when you build things out and your default behavior is to abstract, it becomes a lot more taxing to add code to the codebase, since you have to add it against the existing abstractions and structures. And this manifests itself massively in a game based around components.

Furthermore, I think that most indie games actually never get to the point where the ECS architecture actually starts paying off. If you take a look at this very scientific graph that I drew what I mean should become clear:

So the idea is that at the start, "yolo coding" (what I'm arguing for in this article) requires less effort to get things done when compared to ECS. As time passes and the project gets further along, the effort required for yolo coding increases while the effort required for ECS decreases, until a point is reached where ECS becomes more efficient than yolo coding. The point I wanna make is that most indie games, with very few exceptions (in my view at least) ever reach that intersection point between both lines.

And so if this is the case, and in my view it is, then it makes no sense to use something like an ECS. This also applies to a number of other programming techniques and practices that you see people promote. This entire article has been about that, essentially. There are things that pay off in the long run that are not good for indie game development because the long run never actually manifests itself.


END

Anyway, I think I've given enough of my opinions on these issues. If you take anything away from this article just consider that most programming advice you'll find on the Internet is suited for teams of people working on software that needs to be maintained for a long time. Your context as a developer of indie video games is completely different, and so you should always think critically about if the advice given by other people suits you or not. Lots of times it will suit you, because there are things about programming that are of benefit in every context (like, say, naming variables properly), but sometimes it won't. And if you're not paying attention to the times when it doesn't you'll be slower and less productive than you otherwise could be.

At the same time, if at your day job you work in a big team on software that needs to be maintained for a long time and you've incorporated the practices and styles that come with that, if you can't come home and code your game with a different mindset then trying to do the things I'm outlining in this article would be disastrous. So you also need to consider what your "natural" coding environment is, how far away it is from what I'm saying is the natural coding environment of solo indie programmers, and how easily you can switch between the two on a daily basis. The point is, think critically about your programming practices, how well they're suited to your specific context and how comfortable you are with each one of them.



BYTEPATH #7 - Player Stats and Attacks

Introduction

In this tutorial we'll focus on getting more basics of gameplay down on the Player side of things. First we'll add the most fundamental stats: ammo, boost, HP and skill points. These stats will be used throughout the entire game and they're the main resources the player will use to do everything he can do. After that we'll focus on the creation of Resource objects, which are objects that the player can gather that contain the stats just mentioned. And finally after that we'll add the attack system as well as a few different attacks to the Player.


Draw Order

Before we go over to the main parts of this article, setting the game's draw order is something important that I forgot to mention in the previous article so we'll go over it now.

The draw order decides which objects will be drawn on top and which will be drawn behind which. For instance, right now we have a bunch of effects that are drawn when something happens. If the effects are drawn behind other objects like the Player then they either won't be visible or they will look wrong. Because of this we need to make sure that they are always drawn on top of everything and for that we need to define some sort of drawing order between objects.

The way we'll set this up is somewhat straight forward. In the GameObject class we'll define a depth attribute which will start at 50 for all entities. Then, in the definition of each class' constructors, we will be able to define the depth attribute ourselves for that class of objects if we want to. The idea is that objects with a higher depth will be drawn in front, while objects with a lower depth will be drawn behind. So, for instance, if we want to make sure that all effects are drawn in front of everything else, we can just set their depth attribute to something like 75.

function TickEffect:new(area, x, y, opts)
    TickEffect.super.new(self, area, x, y, opts)
    self.depth = 75

    ...
end

Now, the way that this works internally is that every frame we'll be sorting the game_objects list according to the depth attribute of each object:

function Area:draw()
    table.sort(self.game_objects, function(a, b) 
        return a.depth < b.depth
    end)

    for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end

Here, before drawing we simply use table.sort to sort the entities based on their depth attribute. Entities that have lower depth will be sorted to the front of the table and so will be drawn first (behind everything), and entities that have a higher depth will be sorted to the back of the table and so they will be drawn last (in front of everything). If you try setting different depth values to different types of objects you should see that this works out well.

One small problem that this approach has though is that some objects will have the same depth, and when this happens, depending on how the game_objects table is being sorted over time, flickering can occur. Flickering occurs because if objects have the same depth, in one frame one object might be sorted to be in front of another, but in another frame it might be sorted to be behind another. It's unlikely to happen but it can happen and we should take precautions against that.

One way to solve it is to define another sorting parameter in case objects have the same depth. In this case the other parameter I chose was the object's time of creation:

function Area:draw()
    table.sort(self.game_objects, function(a, b) 
        if a.depth == b.depth then return a.creation_time < b.creation_time
        else return a.depth < b.depth end
    end)

    for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end

So if the depths are equal then objects that were created earlier will be drawn earlier, and objects that were created later will be drawn later. This is a reasonable solution and if you test it out you'll see that it also works!


Draw Order Exercises

93. Order objects so that ones with higher depth are drawn behind and ones with lower depth are drawn in front of others. In case the objects have the same depth, they should be ordered by their creation time. Objects that were created earlier should be drawn last, and objects that were created later should be drawn first.

94. In a top-downish 2.5D game like in the gif below, one of the things you have to do to make sure that the entities are drawn in the appropriate order is sort them by their y position. Entities that have a higher y position (meaning that they are closer to the bottom of the screen) should be drawn last, while entities that have a lower y position should be drawn first. What would the sorting function look like in that case?


Basic Stats

Now we'll start with stat building. The first stat we'll focus on is the boost one. The way it works now is that whenever the player presses up or down the ship will take a different speed based on the key pressed. The way it should work is that on top of this basic functionality, it should also be a resource that depletes with use and regenerates over time when not being used. The specific numbers and rules that will be used are these:

  1. The player will have 100 boost initially
  2. Whenever the player is boosting 50 boost will be removed per second
  3. At all times 10 boost is generated per second
  4. Whenever boost reaches 0 a 2 second cooldown is applied before it can be used again
  5. Boosts can only happen when the cooldown is off and when the boost resource is above 0

These sound a little complicated but they're not. The first three are just number specifications, and the last two are to prevent boosts from never ending. When the resource reaches 0 it will regenerate to 1 pretty consistently and this can lead to a scenario where you can essentially use the boost forever, so a cooldown has to be added to prevent this from happening.

Now to add this as code:

function Player:new(...)
    ...
    self.max_boost = 100
    self.boost = self.max_boost
end

function Player:update(dt)
    ...
    self.boost = math.min(self.boost + 10*dt, self.max_boost)
    ...
end

So with this we take care of rules 1 and 3. We start boost with max_boost, which is 100, and then we add 10 per second to boost while making sure that it doesn't go over max_boost. We can easily also get rule 2 done by simply decreasing 50 boost per second whenever the player is boosting:

function Player:update(dt)
    ...
    if input:down('up') then
    	self.boosting = true
    	self.max_v = 1.5*self.base_max_v
    	self.boost = self.boost - 50*dt
    end
  	if input:down('down') then
    	self.boosting = true
    	self.max_v = 0.5*self.base_max_v
    	self.boost = self.boost - 50*dt
    end
    ...
end

Part of this code was already here from before, so the only lines we really added were the self.boost -= 50*dt ones. Now to check rule 4 we need to make sure that whenever boost reaches 0 a 2 second cooldown is started before the player can boost again. This is a bit more complicated because if involves more moving parts, but it looks like this:

function Player:new(...)
    ...
    self.can_boost = true
    self.boost_timer = 0
    self.boost_cooldown = 2
end

At first we'll introduce 3 variables. can_boost will be used to tell when a boost can happen. By default it's set to true because the player should be able to boost at the start. It will be set to false once boost reaches 0 and then it will be set to true again boost_cooldown seconds after. The boost_timer variable will take care of tracking how much time it has been since boost reached 0, and if this variable goes above boost_cooldown then we will set can_boost to true.

function Player:update(dt)
    ...
    self.boost = math.min(self.boost + 10*dt, self.max_boost)
    self.boost_timer = self.boost_timer + dt
    if self.boost_timer > self.boost_cooldown then self.can_boost = true end
    self.max_v = self.base_max_v
    self.boosting = false
    if input:down('up') and self.boost > 1 and self.can_boost then 
        self.boosting = true
        self.max_v = 1.5*self.base_max_v 
        self.boost = self.boost - 50*dt
        if self.boost <= 1 then
            self.boosting = false
            self.can_boost = false
            self.boost_timer = 0
        end
    end
    if input:down('down') and self.boost > 1 and self.can_boost then 
        self.boosting = true
        self.max_v = 0.5*self.base_max_v 
        self.boost = self.boost - 50*dt
        if self.boost <= 1 then
            self.boosting = false
            self.can_boost = false
            self.boost_timer = 0
        end
    end
    self.trail_color = skill_point_color 
    if self.boosting then self.trail_color = boost_color end
end

This looks complicated but it just follows from what we wanted to achieve. Instead of just checking to see if a key is being pressed with input:down, we additionally also make sure that boost is above 1 (rule 5) and that can_boost is true (rule 5). Whenever boost reaches 0 we set boosting and can_boost to false, and then we reset boost_timer to 0. Since boost_timer is being added dt every frame, after 2 seconds it will set can_boost to true and we'll be able to boost again (rule 4).

The code above is also the way the boost mechanism should look like now in its complete state. One thing to note is that you might think that this looks ugly or unorganized or any number of combination of bad things. But this is just what a lot of code that takes care of certain aspects of gameplay looks like. It's multiple rules that are being followed and they sort of have to be followed all in the same place. It's important to get used to code like this, in my opinion.

In any case, out of the basic stats, boost was the only one that had some more involved logic to it. There are two more important stats: ammo and HP, but both are way simpler. Ammo will just get depleted whenever the player attacks and regained whenever a resource is collected in gameplay, and HP will get depleted whenever the player is hit and also regained whenever a resource is collected in gameplay. For now, we can just add them as basic stats like we did for the boost:

function Player:new(...)
    ...
    self.max_hp = 100
    self.hp = self.max_hp

    self.max_ammo = 100
    self.ammo = self.max_ammo
end

Resources

What I call resources are small objects that affect one of the main basic stats that we just went over. The game will have a total of 5 of these types of objects and they'll work like this:

  • Ammo resource restores 5 ammo to the player and is spawned on enemy death
  • Boost resource restores 25 boost to the player and is spawned randomly by the Director
  • HP resource restores 25 HP to the player and is spawned randomly by the Director
  • SkillPoint resource adds 1 skill point to the player and is spawned randomly by the Director
  • Attack resource changes the current player attack and is spawned randomly by the Director

The Director is a piece of code that handles the spawning of enemies as well as resources. I called it that because other games (like L4D) call it that and it just seems to fit. Because we're not going to work on that piece of code yet, for now we'll bind the creation of each resource to a key just to test that it works.


Ammo Resource

So let's get started on the ammo. The final result should be like this:

The little green rectangles are the ammo resource. When the player hits one of them with its body the resource is destroyed and the player gets 5 ammo. We can create a new class named Ammo and start with some definitions:

function Ammo:new(...)
    ... 
    self.w, self.h = 8, 8
    self.collider = self.area.world:newRectangleCollider(self.x, self.y, self.w, self.h)
    self.collider:setObject(self)
    self.collider:setFixedRotation(false)
    self.r = random(0, 2*math.pi)
    self.v = random(10, 20)
    self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r))
    self.collider:applyAngularImpulse(random(-24, 24))
end

function Ammo:draw()
    love.graphics.setColor(ammo_color)
    pushRotate(self.x, self.y, self.collider:getAngle())
    draft:rhombus(self.x, self.y, self.w, self.h, 'line')
    love.graphics.pop()
    love.graphics.setColor(default_color)
end

Ammo resources will be physics rectangles that start with some random and small velocity and rotation, set initially by setLinearVelocity and applyAngularImpulse. This object is also drawn using the draft library. This is a small library that lets you draw all sorts of shapes more easily than if you had to do it yourself. For this case you can just draw the resource as a rectangle if you want, but I'll choose to go with this. I'll also assume that you can already install the library yourself and read the documentation to figure out what it can and can't do. Additionally, we're also taking into account the rotation of the physics object by using the result from getAngle in pushRotate.

To test this all out, we can bind the creation of one of these objects to a key like this:

function Stage:new()
    ...
    input:bind('p', function() 
        self.area:addGameObject('Ammo', random(0, gw), random(0, gh)) 
    end)
end

And if you run the game now and press p a bunch of times you should see these objects spawning and moving/rotating around.

The next thing we should do is create the collision interaction between player and resource. This interaction will hold true for all resources and will be mostly the same always. The first thing we want to do is make sure that we can capture an event when the player physics object collides with the ammo physics object. The easiest way to do this is through the use of collision classes. To start with we can define 3 collision classes for objects that are already exist: the Player, the projectiles and the resources.

function Stage:new()
    ...
    self.area = Area(self)
    self.area:addPhysicsWorld()
    self.area.world:addCollisionClass('Player')
    self.area.world:addCollisionClass('Projectile')
    self.area.world:addCollisionClass('Collectable')
    ...
end

And then in each one of those files (Player, Projectile and Ammo) we can set the collider's collision class using setCollisionClass (repeat the code below for the other files):

function Player:new(...)
    ...
    self.collider:setCollisionClass('Player')
    ...
end

By itself this doesn't change anything, but it gives us a base to work with and to capture collision events between physics objects. For instance, if we change the Collectable collision class to ignore the Player like this:

self.area.world:addCollisionClass('Collectable', {ignores = {'Player'}})

Then if you run the game again you'll notice that the player is now physically ignoring the ammo resource objects. This isn't what we want to do in the end but it serves as a nice example of what we can do with collision classes. The rules we actually want these 3 collision classes to follow are the following:

  1. Projectile will ignore Projectile
  2. Collectable will ignore Collectable
  3. Collectable will ignore Projectile
  4. Player will generate collision events with Collectable

Rules 1, 2 and 3 can be satisfied by making small changes to the addCollisionClass calls:

function Stage:new()
    ...
    self.area.world:addCollisionClass('Player')
    self.area.world:addCollisionClass('Projectile', {ignores = {'Projectile'}})
    self.area.world:addCollisionClass('Collectable', {ignores = {'Collectable', 'Projectile'}})
    ...
end

It's important to note that the order of the declaration of collision classes matters. For instance, if we swapped the order of the Projectile and Collectable declarations a bug would happen because the Collectable collision class makes reference to the Projectile collision class, but since the Projectile collision class isn't yet defined it bugs out.

The fourth rule can be satisfied by using the enter call:

function Player:update(dt)
    ...
    if self.collider:enter('Collectable') then
        print(1)
    end
end

And if you run this you'll see that 1 will be printed to the console every time the player collides with an ammo resource.

Another piece of behavior we need to add to the Ammo class is that it needs to move towards the player slightly. An easy way to do this is to add the Seek Behavior to it. My version of the seek behavior was based on the book Programming Game AI by Example which has a very nice section on steering behaviors in general. I'm not going to explain it in detail because I honestly don't remember how it works, so I'll just assume if you're curious about it you'll figure it out :D

function Ammo:update(dt)
    ...
    local target = current_room.player
    if target then
        local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized()
        local angle = math.atan2(target.y - self.y, target.x - self.x)
        local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized()
        local final_heading = (projectile_heading + 0.1*to_target_heading):normalized()
        self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y)
    else self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end
end

So here the ammo resource will head towards the target if it exists, otherwise it will just move towards the direction it was initially set to move towards. target contains a reference to the player, which was set in Stage like this:

function Stage:new()
    ...
    self.player = self.area:addGameObject('Player', gw/2, gh/2)
end

Finally, the only thing left to do is what happens when an ammo resource is collected. From the gif above you can see that a little effect plays (like the one for when a projectile dies), along with some particles, and then the player also gets +5 ammo.

Let's start with the effect. This effect follows the exact same logic as the ProjectileDeathEffect object, in that there's a little white flash and then the actual color of the effect comes on. The only difference here is that instead of drawing a square we will be drawing a rhombus, which is the same shape that we used to draw the ammo resource itself. I'll call this new object AmmoEffect and I won't really go over it in detail since it's the same as ProjectileDeathEffect. The way we call it though is like this:

function Ammo:die()
    self.dead = true
    self.area:addGameObject('AmmoEffect', self.x, self.y, 
    {color = ammo_color, w = self.w, h = self.h})
    for i = 1, love.math.random(4, 8) do 
    	self.area:addGameObject('ExplodeParticle', self.x, self.y, {s = 3, color = ammo_color}) 
    end
end

Here we are creating one AmmoEffect object and then between 4 and 8 ExplodeParticle objects, which we already used before for the Player's death effect. The die function on an Ammo object will get called whenever it collides with the Player:

function Player:update(dt)
    ...
    if self.collider:enter('Collectable') then
        local collision_data = self.collider:getEnterCollisionData('Collectable')
        local object = collision_data.collider:getObject()
        if object:is(Ammo) then
            object:die()
        end
    end
end

Here first we use getEnterCollisionData to get the collision data generated by the last enter collision event for the specified tag. After this we use getObject to get access to the object attached to the collider involved in this collision event, which could be any object of the Collectable collision class. In this case we only have the Ammo object to worry about, but if we had others here's where we'd place the code to separate between them. And that's what we do, to really check that the object we get from getObject is the of the Ammo class we use classic's is function. If it is really is an object of the Ammo class then we call its die function. All that should look like this:

One final thing we forgot to do is actually add +5 ammo to the player whenever an ammo resource is gathered. For this we'll define an addAmmo function which simply adds a certain amount to the ammo variable and checks that it doesn't go over max_ammo:

function Player:addAmmo(amount)
    self.ammo = math.min(self.ammo + amount, self.max_ammo)
end

And then we can just call this after object:die() in the collision code we just added.


Boost Resource

Now for the boost. The final result should look like this:

As you can see, the idea is almost the same as the ammo resource, except that the boost resource's movement is a little different, it looks a little different and the visual effect that happens when one is gathered is different as well.

So let's start with the basics. For every resource other than the ammo one, they'll be spawned either on the left or right of the screen and they'll move really slowly in a straight line to the other side. The same applies to the enemies. This gives the player enough time to move towards the resource to pick it up if he wants to.

The basic starting setup of the Boost class is about the same as the Ammo one and looks like this:

function Boost:new(...)
    ...

    local direction = table.random({-1, 1})
    self.x = gw/2 + direction*(gw/2 + 48)
    self.y = random(48, gh - 48)

    self.w, self.h = 12, 12
    self.collider = self.area.world:newRectangleCollider(self.x, self.y, self.w, self.h)
    self.collider:setObject(self)
    self.collider:setCollisionClass('Collectable')
    self.collider:setFixedRotation(false)
    self.v = -direction*random(20, 40)
    self.collider:setLinearVelocity(self.v, 0)
    self.collider:applyAngularImpulse(random(-24, 24))
end

function Boost:update(dt)
    ...

    self.collider:setLinearVelocity(self.v, 0) 
end

There are a few differences though. The 3 first lines in the constructor are picking the initial position of this object. The table.random function is defined in utils.lua as follows:

function table.random(t)
    return t[love.math.random(1, #t)]
end

And as you can see what it does is just pick a random element from a table. In this case, we're picking either -1 or 1 to signify the direction in which the object will be spawned. If -1 is picked then the object will be spawned to the left of the screen, and if 1 is picked then it will be spawned to the right. More precisely, the exact positions chosen for its chosen position will be either -48 or gw+48, so slightly offscreen but close enough to the edge.

After this we define the object mostly like the Ammo one, with a few differences again only when it comes to its velocity. If this object was spawned to the right then we want it to move left, and if it was spawned to the left then we want it to move right. So its velocity is set to a random value between 20 and 40, but also multiplied by -direction, since if the object was to the right, direction was 1, and if we want to move it to the left then the velocity has to be negative (and the opposite for the other side). The object's velocity is always set to the v attribute on the x component and set to 0 on the y component. We want the object to remain moving in a horizontal line no matter what so setting its y velocity to 0 will achieve that.

The final main difference is in the way its drawn:

function Boost:draw()
    love.graphics.setColor(boost_color)
    pushRotate(self.x, self.y, self.collider:getAngle())
    draft:rhombus(self.x, self.y, 1.5*self.w, 1.5*self.h, 'line')
    draft:rhombus(self.x, self.y, 0.5*self.w, 0.5*self.h, 'fill')
    love.graphics.pop()
    love.graphics.setColor(default_color)
end

Here instead of just drawing a single rhombus we draw one inner and one outer to be used as sort of an outline. You can obviously draw all these objects in whatever way you want, but this is what I personally decided to do.

Now for the effects. There are two effects being used here: one that is similar to AmmoEffect (although a bit more involved) and the one that is used for the +BOOST text. We'll start with the one that's similar to AmmoEffect and call it BoostEffect.

This effect has two parts to it, the center with its white flash and the blinking effect before it disappears. The center works in the same way as the AmmoEffect, the only difference is that the timing of each phase is different, from 0.1 to 0.2 in the first phase and from 0.15 to 0.35 in the second:

function BoostEffect:new(...)
    ...
    self.current_color = default_color
    self.timer:after(0.2, function() 
        self.current_color = self.color 
        self.timer:after(0.35, function()
            self.dead = true
        end)
    end)
end

The other part of the effect is the blinking before it dies. This can be achieved by creating a variable named visible, which when set to true will draw the effect and when set to false will not draw the effect. By changing this variable from false to true really fast we can achieve the desired effect:

function BoostEffect:new(...)
    ...
    self.visible = true
    self.timer:after(0.2, function()
        self.timer:every(0.05, function() self.visible = not self.visible end, 6)
        self.timer:after(0.35, function() self.visible = true end)
    end)
end

Here we use the every call to switch between visible and not visible six times, each with a 0.05 seconds delay in between, and after that's done we set it to be visible in the end. The effect will die after 0.55 seconds anyway (since we set dead to true after 0.55 seconds when setting the current color) so setting it to be visible in the end isn't super important to do. In any case, then we can draw it like this:

function BoostEffect:draw()
    if not self.visible then return end

    love.graphics.setColor(self.current_color)
    draft:rhombus(self.x, self.y, 1.34*self.w, 1.34*self.h, 'fill')
    draft:rhombus(self.x, self.y, 2*self.w, 2*self.h, 'line')
    love.graphics.setColor(default_color)
end

We're simply drawing both the inner and outer rhombus at different sizes. The exact numbers (1.34, 2) were reached through pretty much trial and error based on what looked best.

The final thing we need to do for this effect is to make the outer rhombus outline expand over the life of the object. We can do that like this:

function BoostEffect:new(...)
    ...
    self.sx, self.sy = 1, 1
    self.timer:tween(0.35, self, {sx = 2, sy = 2}, 'in-out-cubic')
end

And then update the draw function like this:

function BoostEffect:draw()
    ...
    draft:rhombus(self.x, self.y, self.sx*2*self.w, self.sy*2*self.h, 'line')
    ...
end

With this, the sx and sy variables will grow to 2 over 0.35 seconds, which means that the outline rhombus will also grow to double its previous value over those 0.35 seconds. In the end the result looks like this (I'm assuming you already linked this object's die function to the collision event with the Player, like we did for the ammo resource):


Now for the other part of the effect, the crazy looking text. This text effect will be used throughout the game pretty much everywhere so let's make sure we get it right. Here's what the effect looks like again:

First let's break this effect down into its multiple parts. The first thing to notice is that it's simply a string being drawn to the screen initially, but then near its end it starts blinking like the BoostEffect object. That blinking part turns out to use exactly the same logic as the BoostEffect so we have that covered already.

What also happens though is that the letters of the string start changing randomly to other letters, and each character's background also changes colors randomly. This suggests that this effect is processing characters individually internally rather than operating on a single string, which probably means we'll have to do something like hold all characters in a characters table, operate on this table, and then draw each character on that table with all its modifications and effects to the screen.

So to start with this in mind we can define the basics of the InfoText class. The way we're gonna call it is like this:

function Boost:die()
    ...
    self.area:addGameObject('InfoText', self.x, self.y, {text = '+BOOST', color = boost_color})
end

And so the text attribute will contain our string. Then the basic definition of the class can look like this:

function InfoText:new(...)
    ...
    self.depth = 80
  	
    self.characters = {}
    for i = 1, #self.text do table.insert(self.characters, self.text:utf8sub(i, i)) end
end

With this we simply define that this object will have depth of 80 (higher than all other objects, so will be drawn in front of everything) and then we separate the initial string into characters in a table. We use an utf8 library to do this. In general it's a good idea to manipulate strings with a library that supports all sorts of characters, and for this object it's really important that we do this as we'll see soon.

In any case, the drawing of these characters should also be done on an individual basis because as we figured out earlier, each character has its own background that can change randomly, so it's probably the case that we'll want to draw each character individually.

The logic used to draw characters individually is basically to go over the table of characters and draw each character at the x position that is the sum of all characters before it. So, for instance, drawing the first O in +BOOST means drawing it at something like initial_x_position + widthOf('+B'). The problem with getting the width of +B in this case is that it depends on the font being used, since we'll use the Font:getWidth function, and right now we haven't set any font. We can solve that easily though!

For this effect the font used will be m5x7 by Daniel Linssen. We can put this font in the folder resources/fonts and then load it. The code needed for loading it will be left as an exercise, since it's somewhat similar to the code used to load class definitions in the objects folder (exercise 14). By the end of this loading process we should have a global table called fonts that has all loaded fonts in the format fontname_fontsize. In this instance we'll use m5x7_16:

function InfoText:new(...)
    ...
    self.font = fonts.m5x7_16
    ...
end

And this is what the drawing code looks like:

function InfoText:draw()
    love.graphics.setFont(self.font)
    for i = 1, #self.characters do
        local width = 0
        if i > 1 then
            for j = 1, i-1 do
                width = width + self.font:getWidth(self.characters[j])
            end
        end

        love.graphics.setColor(self.color)
        love.graphics.print(self.characters[i], self.x + width, self.y, 
      	0, 1, 1, 0, self.font:getHeight()/2)
    end
    love.graphics.setColor(default_color)
end

First we use love.graphics.setFont to set the font we want to use for the next drawing operations. After this we go over each character and then draw it. But first we need to figure out its x position, which is the sum of the width of the characters before it. The inner loop that accumulates on the variable width is doing just that. It goes from 1 (the start of the string) to i-1 (the character before the current one) and adds the width of each character to a total final width that is the sum of all of them. After that we use love.graphics.print to draw each individual character at its appropriate position. We also offset each character up by half the height of the font (so that the characters are centered around the y position we defined).

If we test all this out now it looks like this:

Which looks about right!

Now we can move on to making the text blink a little before disappearing. This uses the same logic as the BoostEffect object so we can just kinda copy it:

function InfoText:new(...)
    ...
    self.visible = true
    self.timer:after(0.70, function()
        self.timer:every(0.05, function() self.visible = not self.visible end, 6)
        self.timer:after(0.35, function() self.visible = true end)
    end)
    self.timer:after(1.10, function() self.dead = true end)
end

And if you run this you should see that the text stays normal for a while, starts blinking and then disappears.

Now the hard part, which is making each character change randomly as well as its foreground and background colors. This changing starts at about the same that the character starts blinking, so we'll place this piece of code inside the 0.7 seconds after call we just defined above. The way we'll do this is that every 0.035 seconds, we'll run a procedure that will have a chance to change a character to another random character. That looks like this:

self.timer:after(0.70, function()
    ...
    self.timer:every(0.035, function()
    	for i, character in ipairs(self.characters) do
            if love.math.random(1, 20) <= 1 then
            	-- change character
            else
            	-- leave character as it is
            end
       	end
    end)
end)

And so each 0.035 seconds, for each character there's a 5% probability that it will be changed to something else. We can complete this by adding a variable named random_characters which is a string that contains all characters a character might change to, and then when a character change is necessary we pick one at random from this string:

self.timer:after(0.70, function()
    ...
    self.timer:every(0.035, function()
        local random_characters = '0123456789!@#$%¨&*()-=+[]^~/;?><.,|abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYXZ'
    	for i, character in ipairs(self.characters) do
            if love.math.random(1, 20) <= 1 then
            	local r = love.math.random(1, #random_characters)
                self.characters[i] = random_characters:utf8sub(r, r)
            else
                self.characters[i] = character
            end
       	end
    end)
end)

And if you run that now it should look like this:

We can use the same logic we used here to change the character's colors as well as their background colors. For that we'll define two tables, background_colors and foreground_colors. Each table will be the same size of the characters table and will simply hold the background and foreground colors for each character. If a certain character doesn't have any colors set in these tables then it just defaults to the default color for the foreground (boost_color ) and to a transparent background.

function InfoText:new(...)
    ...
    self.background_colors = {}
    self.foreground_colors = {}
end

function InfoText:draw()
    ...
    for i = 1, #self.characters do
    	...
    	if self.background_colors[i] then
      	    love.graphics.setColor(self.background_colors[i])
      	    love.graphics.rectangle('fill', self.x + width, self.y - self.font:getHeight()/2,
      	    self.font:getWidth(self.characters[i]), self.font:getHeight())
      	end
    	love.graphics.setColor(self.foreground_colors[i] or self.color or default_color)
    	love.graphics.print(self.characters[i], self.x + width, self.y, 
      	0, 1, 1, 0, self.font:getHeight()/2)
    end
end

For the background colors we simply draw a rectangle at the appropriate position and with the size of the current character if background_colors[i] (the background color for the current character) is defined. As for the foreground color, we simply set the color to draw the current character with using setColor. If foreground_colors[i] isn't defined then it defauls to self.color, which for this object should always be boost_color since that's what we're passing in when we call it from the Boost object. But if self.color isn't defined either then it defaults to white (default_color). By itself this piece of code won't really do anything, because we haven't defined any of the values inside the background_colors or the foreground_colors tables.

To do that we can use the same logic we used to change characters randomly:

self.timer:after(0.70, function()
    ...
    self.timer:every(0.035, function()
    	for i, character in ipairs(self.characters) do
            ...
            if love.math.random(1, 10) <= 1 then
                -- change background color
            else
                -- set background color to transparent
            end
          
            if love.math.random(1, 10) <= 2 then
                -- change foreground color
            else
                -- set foreground color to boost_color
            end
       	end
    end)
end)

The code that changes colors around will have to pick between a list of colors. We defined a group of 6 colors globally and we could just put those all into a list and then use table.random to pick one at random. What we'll do is that but also define 6 more colors on top of it which will be the negatives of the 6 original ones. So say you have 232, 48, 192 as the original color, we can define its negative as 255-232, 255-48, 255-192.

function InfoText:new(...)
    ...
    local default_colors = {default_color, hp_color, ammo_color, boost_color, skill_point_color}
    local negative_colors = {
        {255-default_color[1], 255-default_color[2], 255-default_color[3]}, 
        {255-hp_color[1], 255-hp_color[2], 255-hp_color[3]}, 
        {255-ammo_color[1], 255-ammo_color[2], 255-ammo_color[3]}, 
        {255-boost_color[1], 255-boost_color[2], 255-boost_color[3]}, 
        {255-skill_point_color[1], 255-skill_point_color[2], 255-skill_point_color[3]}
    }
    self.all_colors = fn.append(default_colors, negative_colors)
    ...
end

So here we define two tables that contain the appropriate values for each color and then we use the append function to join them together. So now we can say something like table.random(self.all_colors) to get a random color out of the 10 defined in those tables, which means that we can do this:

self.timer:after(0.70, function()
    ...
    self.timer:every(0.035, function()
    	for i, character in ipairs(self.characters) do
            ...
            if love.math.random(1, 10) <= 1 then
                self.background_colors[i] = table.random(self.all_colors)
            else
                self.background_colors[i] = nil
            end
          
            if love.math.random(1, 10) <= 2 then
                self.foreground_colors[i] = table.random(self.all_colors)
            else
                self.background_colors[i] = nil
            end
       	end
    end)
end)

And if we run the game now it should look like this:

And that's it. We'll improve it even more later on (and on the exercises) but it's enough for now. Lastly, the final thing we should do is make sure that whenever we collect a boost resource we actually add +25 boost to the player. This works exactly the same way as it did for the ammo resource so I'm going to skip it.


Resources Exercises

95. Make it so that the Projectile collision class will ignore the Player collision class.

96. Change the addAmmo function so that it supports the addition of negative values and doesn't let the ammo attribute go below 0. Do the same for the addBoost and addHP functions (adding the HP resource is an exercise defined below).

97. Following the previous exercise, is it better to handle positive and negative values on the same function or to separate between addResource and removeResource functions instead?

98. In the InfoText object, change the probability of a character being changed to 20%, the probability of a foreground color being changed to 5%, and the probability of a background color being changed to 30%.

99. Define the default_colors, negative_colors and all_colors tables globally instead of locally in InfoText.

100. Randomize the position of the InfoText object so that it is spawned between -self.w and self.w in its x component and between -self.h and self.h in its y component. The w and h attributes refer to the Boost object that is spawning the InfoText.

101. Assume the following function:

function Area:getAllGameObjectsThat(filter)
    local out = {}
    for _, game_object in pairs(self.game_objects) do
        if filter(game_object) then
            table.insert(out, game_object)
        end
    end
    return out
end

Which returns all game objects inside an Area that pass a filter function. And then assume that it's called like this inside InfoText's constructor:

function InfoText:new(...)
    ...
    local all_info_texts = self.area:getAllGameObjectsThat(function(o) 
        if o:is(InfoText) and o.id ~= self.id then 
            return true 
        end 
    end)
end

Which returns all existing and alive InfoText objects that are not this one. Now make it so that this InfoText object doesn't visually collide with any other InfoText object, meaning, it doesn't occupy the same space on the screen as another such that its text would become unreadable. You may do this in whatever you think is best as long as it achieves the goal.

102. (CONTENT) Add the HP resource with all of its functionality and visual effects. It uses the exact same logic as the Boost resource, but instead adds +25 to HP instead. The resource and effects look like this:

103. (CONTENT) Add the SP resource with all of its functionality and visual effects. It uses the exact same logic as the Boost resource, but instead adds +1 to SP instead. The SP resource should also be defined as a global variable for now instead of an internal one to the Player object. The resource and effects look like this:


Attacks

Alright, so now for attacks. Before anything else the first thing we're gonna do is change the way projectiles are drawn. Right now they're being drawn as circles but we want them as lines. This can be achieved with something like this:

function Projectile:draw()
    love.graphics.setColor(default_color)

    pushRotate(self.x, self.y, Vector(self.collider:getLinearVelocity()):angle()) 
    love.graphics.setLineWidth(self.s - self.s/4)
    love.graphics.line(self.x - 2*self.s, self.y, self.x, self.y)
    love.graphics.line(self.x, self.y, self.x + 2*self.s, self.y)
    love.graphics.setLineWidth(1)
    love.graphics.pop()
end

In the pushRotate function we use the projectile's velocity so that we can rotate it towards the angle its moving at. Then inside we use love.graphics.setLineWidth and set it to a value somewhat proportional to the s attribute but slightly smaller. This means that projectiles with bigger s will be thicker in general. Then we draw the projectile using love.graphics.line and importantly, we draw one line from -2*self.s to the center and then another from the center to 2*self.s. We do this because each attack will have different colors, and what we'll do is change the color of one those lines but not change the color of another. So, for instance, if we do this:

function Projectile:draw()
    love.graphics.setColor(default_color)

    pushRotate(self.x, self.y, Vector(self.collider:getLinearVelocity()):angle()) 
    love.graphics.setLineWidth(self.s - self.s/4)
    love.graphics.line(self.x - 2*self.s, self.y, self.x, self.y)
    love.graphics.setColor(hp_color) -- change half the projectile line to another color
    love.graphics.line(self.x, self.y, self.x + 2*self.s, self.y)
    love.graphics.setLineWidth(1)
    love.graphics.pop()
end

It will look like this:

In this way we can make each attack have its own color which helps with letting the player better understand what's going on on the screen.


The game will end up having 16 attacks but we'll cover only a few of them now. The way the attack system will work is very simple and these are the rules:

  1. Attacks (except the Neutral one) consume ammo with every shot;
  2. When ammo hits 0 the current attack is changed to Neutral;
  3. New attacks can be obtained through resources that are spawned randomly;
  4. When a new attack is obtained, the current attack is removed and ammo is fully regenerated;
  5. Each attack consumes a different amount of ammo and has different properties.

The first we're gonna do is define a table that will hold information on each attack, such as their cooldown, ammo consumption and color. We'll define this in globals.lua and for now it will look like this:

attacks = {
    ['Neutral'] = {cooldown = 0.24, ammo = 0, abbreviation = 'N', color = default_color},
}

The normal attack that we already have defined is called Neutral and it simply has the stats that the attack we had in the game had so far. Now what we can do is define a function called setAttack which will change from one attack to another and use this global table of attacks:

function Player:setAttack(attack)
    self.attack = attack
    self.shoot_cooldown = attacks[attack].cooldown
    self.ammo = self.max_ammo
end

And then we can call it like this:

function Player:new(...)
    ...
    self:setAttack('Neutral')
    ...
end

Here we simply change an attribute called attack which will contain the name of the current attack. This attribute will be used in the shoot function to check which attack is currently active and how we should proceed with projectile creation.

We also change an attribute named shoot_cooldown. This is an attribute that we haven't created yet, but similar to how the boost_timer and boost_cooldown attributes work, they will be used to control how often something can happen, in this case how often an attack happen. We will remove this line:

function Player:new(...)
    ...
    self.timer:every(0.24, function() self:shoot() end)
    ...
end

And instead do the timing of attacks manually like this:

function Player:new(...)
    ...
    self.shoot_timer = 0
    self.shoot_cooldown = 0.24
    ...
end

function Player:update(dt)
    ...
    self.shoot_timer = self.shoot_timer + dt
    if self.shoot_timer > self.shoot_cooldown then
        self.shoot_timer = 0
        self:shoot()
    end
    ...
end

Finally, at the end of the setAttack function we also regenerate the ammo resource. With this we take care of rule 4. The next thing we can do is change the shoot function a little to start taking into account the fact that different attacks exist:

function Player:shoot()
    local d = 1.2*self.w
    self.area:addGameObject('ShootEffect', 
    self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), {player = self, d = d})

    if self.attack == 'Neutral' then
        self.area:addGameObject('Projectile', 
      	self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), {r = self.r})
    end
end

Before launching the projectile we check the current attack with the if self.attack == 'Neutral' conditional. This function will grow based on a big conditional chain like this where we'll be checking for all 16 attacks that we add.


So let's get started with adding one actual attack to see what it's like. The attack we'll add will be called Double and it looks like this:

And as you can see it shoots 2 projectiles at an angle instead of one. To get started with this first we'll add the attack's description to the global attacks table. This attack will have a cooldown of 0.32, cost 2 ammo, and its color will be ammo_color (these values were reached through trial and error):

attacks = {
    ...
    ['Double'] = {cooldown = 0.32, ammo = 2, abbreviation = '2', color = ammo_color},
}

Now we can add it to the shoot function as well:

function Player:shoot()
    ...
    elseif self.attack == 'Double' then
        self.ammo = self.ammo - attacks[self.attack].ammo
        self.area:addGameObject('Projectile', 
    	self.x + 1.5*d*math.cos(self.r + math.pi/12), 
    	self.y + 1.5*d*math.sin(self.r + math.pi/12), 
    	{r = self.r + math.pi/12, attack = self.attack})
        
        self.area:addGameObject('Projectile', 
    	self.x + 1.5*d*math.cos(self.r - math.pi/12),
    	self.y + 1.5*d*math.sin(self.r - math.pi/12), 
    	{r = self.r - math.pi/12, attack = self.attack})
    end
end

Here we create two projectiles instead of one, each pointing with an angle offset of math.pi/12 radians, or 15 degrees. We also make it so that the projectile receives the attack attribute as the name of the attack. For each projectile type we'll do this as it will help us identify which attack this projectile belongs to. That is helpful for setting its appropriate color as well as changing its behavior when necessary. The Projectile object now looks like this:

function Projectile:new(...)
    ...
    self.color = attacks[self.attack].color
    ...
end

function Projectile:draw()
    pushRotate(self.x, self.y, Vector(self.collider:getLinearVelocity()):angle()) 
    love.graphics.setLineWidth(self.s - self.s/4)
    love.graphics.setColor(self.color)
    love.graphics.line(self.x - 2*self.s, self.y, self.x, self.y)
    love.graphics.setColor(default_color)
    love.graphics.line(self.x, self.y, self.x + 2*self.s, self.y)
    love.graphics.setLineWidth(1)
    love.graphics.pop()
end

In the constructor we set color to the color defined in the global attacks table for this attack. And then in the draw function we draw one part of the line with its color being the color attribute, and another being default_color. For most projectile types this drawing setup will hold.

The last thing we forgot to do is to make it so that this attack obeys rule 1, meaning that we forgot to add code to make it consume the amount of ammo it should consume. This is a pretty simple fix:

function Player:shoot()
    ...
    elseif self.attack == 'Double' then
        self.ammo = self.ammo - attacks[self.attack].ammo
        ...
    end
end

With this rule 1 (for the Double attack) will be followed. We can also add the code that will make rule 2 come true, which is that when ammo hits 0, we change the current attack to the Neutral one:

function Player:shoot()
    ...
    if self.ammo <= 0 then 
        self:setAttack('Neutral')
        self.ammo = self.max_ammo
    end
end

This must come at the end of the shoot function since we don't want the player to be able to shoot one extra time after his ammo resource hits 0.

If you do all this and try running it it should look like this:


Attacks Exercises

104. (CONTENT) Implement the Triple attack. Its definition on the attacks table looks like this:

attacks['Triple'] = {cooldown = 0.32, ammo = 3, abbreviation = '3', color = boost_color}

And the attack itself looks like this:

The angles on the projectile are exactly the same as Double, except that there's one extra projectile also being spawned along the middle (at the same angle that the Neutral projectile is spawned). Create this attack following the same steps that were used for the Double attack.

105. (CONTENT) Implement the Rapid attack. Its definition on the attacks table looks like this:

attacks['Rapid'] = {cooldown = 0.12, ammo = 1, abbreviation = 'R', color = default_color}

And the attack itself looks like this:

106. (CONTENT) Implement the Spread attack. Its definition on the attacks table looks like this:

attacks['Spread'] = {cooldown = 0.16, ammo = 1, abbreviation = 'RS', color = default_color}

And the attack itself looks like this:

The angles used for the shots are a random value between -math.pi/8 and +math.pi/8. This attack's projectile color also works a bit differently. Instead of having one color only, the color changes randomly to one inside the all_colors list every frame (or every other frame depending on what you think is best).

107. (CONTENT) Implement the Back attack. Its definition on the attacks table looks like this:

attacks['Back'] = {cooldown = 0.32, ammo = 2, abbreviation = 'Ba', color = skill_point_color}

And the attack itself looks like this:

108. (CONTENT) Implement the Side attack. Its definition on the attacks table looks like this:

attacks['Side'] = {cooldown = 0.32, ammo = 2, abbreviation = 'Si', color = boost_color}

And the attack itself looks like this:

109. (CONTENT) Implement the Attack resource. Like the Boost and SkillPoint resources, the Attack resource is spawned from either the left or right of the screen at a random y position, and then moves inward very slowly. When the player comes into contact with an Attack resource, his attack is changed to the attack that the resource contains using the setAttack function.

Attack resources look a bit different from the Boost or SkillPoint resources, but the idea behind it and its effects are pretty much the same. The colors used for each different attack are the same as the ones used for its projectiles and the identifying name used is the one that we called abbreviation in the attacks table. Here's what they look like:

Don't forget to create InfoText objects whenever a new attack is gathered by the player!



Roguelikes and Grinding

I've been thinking a lot about the Big Five model and how it relates to games, more specifically the relationship between the indie game development community and the types of games it likes to make more.

One thing to note about indie developers which I already wrote about here is that it seems to me they're more tilted towards openness to experience than conscientiousness. This is because indie game development is a fairly creative field and so it will attract people with that personality profile more.

This means the types of games indie developers want to make will appeal, in general, more to what they enjoy playing, as well as to games that lend themselves to the creation processes that indie developers enjoy, which generally are processes that are not very structured. See how McMullen describes one such process here:

This creates several personality blindspots where an audience that exists in the general population will not be served that well because that audience and the games they enjoy aren't represented in the indie game development community. And this blindspot brings me to roguelikes!


Roguelikes and Grinding

In my view the two main aspects that makes roguelikes interesting (and here I mean procedural generation, permadeath, lots of items/systems coming together to create varied situations) is that they appeal to the need for "new" and that they're challenging. The need for "new" is something that is higher in people that are high in openness to experience, while the appeal for a good challenge is higher in people that are high in conscientiousness.

However, one aspect of gaming that is looked upon negatively by most indie developers is the aspect of grinding. Grinding is something that also appeals to people who are conscientious, but it loads on different facets than challenge (each trait is divided into multiple subtraits, see here).

Indie developers in general are mostly fine with the challenge aspect but are not fine with the grind aspect. Maybe the aspect that commands challenge is higher among creatives in general, I don't know. But the fact is that most indie developers despise grinding. And this creates a blindspot where most roguelikes made will not have that many grindy aspects to them. The most popular grindy roguelike I think is Rogue Legacy:

There are other roguelikes that are also grindy like this one, but the general solution has to been, rather than have permanent character upgrades like this, to have unlockables/achievements that increase your pool of available items in a single run. The problem with this solution is that unlockables/achievements cater to the completionist personality, which I personally don't think correlates that well with conscientiousness. So in the end that solution is not a solution at all because it simply ignores a group of people.

One of the counter arguments against grinding, other than the normal "it's a waste of time" or "it's unethical", is that grinding is opposed to challenge. If a game is grindy, the fact that you can just spend more time to beat the game's challenges rather than simply getting better at the game through your own skill means the challenge is somehow diminished. And this is a fair argument, especially when it comes to roguelikes.


Possible solutions

So the situation is such that there exists a population of people who likes roguelikes but is not being served by the ones that exists today properly because of not enough grind, but if you add grind to a roguelike you risk ruining the challenge aspect of it. The question then becomes, how do you increase grind without diminishing challenge? Is it possible? And I don't know the answer to that yet.

There are examples of games that are both grindy and challenging, something like Dark Souls, for instance. This is a game that can be beaten with no grind by an experienced player, but for players who aren't as experienced they can grind out stats and different equipments to get through some challenge. Maybe this model is useful, where the game has grind but it's used as a backup mechanic in case the player can't progress at some point.

Another possibility is to just ignore the challenge aspect in turn and focus more on the grind. A game that does this well I think is Path of Exile and ARPGs in general. These are very grindy games that are somewhat challenging but really not that much. If you have the levels and you spent the time you will get through the content no matter what. This doesn't seem to affect these games negatively that much, they're simply catering more to the grind aspect rather than the challenge one.

On a sidenote, PoE is a very grindy game so it appeals to conscientious people, but it's also a very customizable game with lots of room for creativity in your build, which appeals highly to open people. This is also represented well by how people play the game: some people enjoy going an entire league and beating all content with a single build, others enjoy getting their build to yellow maps or so and then trying another build ad infinitum. PoE also caters a lot to the completionist personality (which I previously mentioned as liking unlockables/achievements) with their challenges system.

One way in which ARPGs in general have found to increase their challenge, PoE not being an exception, is to have a hardcore mode. This is a permadeath mode, where when you die you lose your entire character. Maybe this is another path forward, where in a roguelike, instead of the default option being permadeath, when you die you simply get thrown out of the dungeon, it generates another dungeon, but you keep some or most of what you acquired in the previous run and you balance the game around that fact. And then for those people who have mastered the game or simply want more challenge, they can enable hardcore mode (like you can do in PoE) where if you die in the game you die in real life.

There are certainly more ways in which we can approach this problem and try to solve it, but I don't honestly know which solution would be better. The point is that there likely are a huge number of people not being served properly by current roguelikes because of a personality blindspot amongst indie developers, and a different approach to the formula would likely be helpful to get games to reach those people more.

BYTEPATH #3 - Rooms and Areas

@SSYGEA

Introduction

In this tutorial I'll cover some structural code needed before moving on to the actual game. I'll explore the idea of Rooms, which are equivalent to what's called a scene in other engines. And then I'll explore the idea of an Area, which is a useful object management type of construct that can go inside a Room. Like the two previous tutorials, this one will still have no code specific to the game and will focus on higher level architectural decisions.


Room

I took the idea of Rooms from GameMaker's documentation. One thing I like to do when figuring out how to approach a game architecture problem is to see how other people have solved it, and in this case, even though I've never used GameMaker, their idea of a Room and the functions around it gave me some really good directions.

As the description there says, Rooms are where everything happens in a game. They're the places where all game objects will be created, updated and drawn and you can change from one Room to the other. Those rooms are also normal objects that I'll place inside a rooms folder. This is what one room called Stage would look like:

Stage = Object:extend()

function Stage:new()

end

function Stage:update(dt)

end

function Stage:draw()

end

Simple Rooms

At its simplest form this system only needs one additional variable and one additional function to work:

function love.load()
    current_room = nil
end

function love.update(dt)
    if current_room then current_room:update(dt) end
end

function love.draw()
    if current_room then current_room:draw() end
end

function gotoRoom(room_type, ...)
    current_room = _G[room_type](...)
end

At first in love.load a global current_room variable is defined. The idea is that at all times only one room can be currently active and so that variable will hold a reference to the current active room object. Then in love.update and love.draw, if there is any room currently active it will be updated and drawn. This means that all rooms must have an update and a draw function defined.

The gotoRoom function can be used to change between rooms. It receives a room_type, which is just a string with the name of the class of the room we want to change to. So, for instance, if there's a Stage class defined as a room, it means the 'Stage' string can be passed in. This works based on how the automatic loading of classes was set up in the previous tutorial, which loads all classes as global variables.

In Lua, global variables are held in a global environment table called _G, so this means that they can be accessed like any other variable in a normal table. If the Stage global variable contains the definition of the Stage class, it can be accessed by just saying Stage anywhere on the program, or also by saying _G['Stage'] or _G.Stage. Because we want to be able to load any arbitrary room, it makes sense to receive the room_type string and then access the class definition via the global table.

So in the end, if room_type is the string 'Stage', the line inside the gotoRoom function parses to current_room = Stage(...), which means that a new Stage room is being instantiated. This also means that any time a change to a new room happens, that new room is created from zero and the previous room is deleted. The way this works in Lua is that whenever a table is not being referred to anymore by any variables, the garbage collector will eventually collect it. And so when the instance of the previous room stops being referred to by the current_room variable, eventually it will be collected.

There are obvious limitations to this setup, for instance, often times you don't want rooms to be deleted when you change to a new one, and often times you don't want a new room to be created from scratch every time you change to it. Avoiding this becomes impossible with this setup.

For this game though, this is what I'll use. The game will only have 3 or 4 rooms, and all those rooms don't need continuity between each other, i.e. they can be created from scratch and deleted any time you move from one to the other and it works fine.


Let's go over a small example of how we can map this system onto a real existing game. Let's look at Nuclear Throne:

Watch the first minute or so of this video until the guy dies once to get an idea of what the game is like.

The game loop is pretty simple and, for the purposes of this simple room setup it fits perfectly because no room needs continuity with previous rooms. (you can't go back to a previous map, for instance) The first screen you see is the main menu:

I'd make this a MainMenu room and in it I'd have all the logic needed for this menu to work. So the background, the five options, the effect when you select a new option, the little bolts of lightning on the edges of screen, etc. And then whenever the player would select an option I would call gotoRoom(option_type), which would swap the current room to be the one created for that option. So in this case there would be additional Play, CO-OP, Settings and Stats rooms.

Alternatively, you could have one MainMenu room that takes care of all those additional options, without the need to separate it into multiple rooms. Often times it's a better idea to keep everything in the same room and handle some transitions internally rather than through the external system. It depends on the situation and in this case there's not enough details to tell which is better.

Anyway, the next thing that happens in the video is that the player picks the play option, and that looks like this:

New options appear and you can choose between normal, daily or weekly mode. Those only change the level generation seed as far as I remember, which means that in this case we don't need new rooms for each one of those options (can just pass a different seed as argument in the gotoRoom call). The player chooses the normal option and this screen appears:

I would call this the CharacterSelect room, and like the others, it would have everything needed to make that screen happen, the background, the characters in the background, the effects that happen when you move between selections, the selections themselves and all the logic needed for that to happen. Once the character is chosen the loading screen appears:

Then the game:

When the player finishes the current level this screen popups before the transition to the next one:

Once the player selects a passive from previous screen another loading screen is shown. Then the game again in another level. And then when the player dies this one:

All those are different screens and if I were to follow the logic I followed until now I'd make them all different rooms: LoadingScreen, Game, MutationSelect and DeathScreen. But if you think more about it some of those become redundant.

For instance, there's no reason for there to be a separate LoadingScreen room that is separate from Game. The loading that is happening probably has to do with level generation, which will likely happen inside the Game room, so it makes no sense to separate that to another room because then the loading would have to happen in the LoadingScreen room, and not on the Game room, and then the data created in the first would have to be passed to the second. This is an overcomplication that is unnecessary in my opinion.

Another one is that the death screen is just an overlay on top of the game in the background (which is still running), which means that it probably also happens in the same room as the game. I think in the end the only one that truly could be a separate room is the MutationSelect screen.

This means that, in terms of rooms, the game loop for Nuclear Throne, as explored in the video would go something like: MainMenu -> Play -> CharacterSelect -> Game -> MutationSelect -> Game -> .... Then whenever a death happens, you can either go back to a new MainMenu or retry and restart a new Game. All these transitions would be achieved through the simple gotoRoom function.


Persistent Rooms

For completion's sake, even though this game will not use this setup, I'll go over one that supports some more situations:

function love.load()
    rooms = {}
    current_room = nil
end

function love.update(dt)
    if current_room then current_room:update(dt) end
end

function love.draw()
    if current_room then current_room:draw() end
end

function addRoom(room_type, room_name, ...)
    local room = _G[room_type](room_name, ...)
    rooms[room_name] = room
    return room
end

function gotoRoom(room_type, room_name, ...)
    if current_room and rooms[room_name] then
        if current_room.deactivate then current_room:deactivate() end
        current_room = rooms[room_name]
        if current_room.activate then current_room:activate() end
    else current_room = addRoom(room_type, room_name, ...) end
end

In this case, on top of providing a room_type string, now a room_name value is also passed in. This is because in this case I want rooms to be able to be referred to by some identifier, which means that each room_name must be unique. This room_name can be either a string or a number, it really doesn't matter as long as it's unique.

The way this new setup works is that now there's an addRoom function which simply instantiates a room and stores it inside a table. Then the gotoRoom function, instead of instantiating a new room every time, can now look in that table to see if a room already exists, if it does, then it just retrieves it, otherwise it creates a new one from scratch.

Another difference here is the use of the activate and deactivate functions. Whenever a room already exists and you ask to go to it again by calling gotoRoom, first the current room is deactivated, the current room is changed to the target room, and then that target room is activated. These calls are useful for a number of things like saving data to or loading data from disk, dereferencing variables (so that they can get collected) and so on.

In any case, what this new setup allows for is for rooms to be persistent and to remain in memory even if they aren't active. Because they're always being referenced by the rooms table, whenever current_room changes to another room, the previous one won't be garbage collected and so it can be retrieved in the future.


Let's look at an example that would make good use of this new system, this time with The Binding of Isaac:

Watch the first minute or so of this video. I'm going to skip over the menus and stuff this time and mostly focus on the actual gameplay. It consists of moving from room to room killing enemies and finding items. You can go back to previous rooms and those rooms retain what happened to them when you were there before, so if you killed the enemies and destroyed the rocks of a room, when you go back it will have no enemies and no rocks. This is a perfect fit for this system.

The way I'd setup things would be to have a Room room where all the gameplay of a room happens. And then a general Game room that coordinates things at a higher level. So, for instance, inside the Game room the level generation algorithm would run and from the results of that multiple Room instances would be created with the addRoom call. Each of those instances would have their unique IDs, and when the game starts, gotoRoom would be used to activate one of those. As the player moves around and explores the dungeon further gotoRoom calls would be made and already created Room instances would be activated/deactivated as the player moves about.

One of the things that happens in Isaac is that as you move from one room to the other there's a small transition that looks like this:

I didn't mention this in the Nuclear Throne example either, but that also has a few transitions that happen in between rooms. There are multiple ways to approach these transitions, but in the case of Isaac it means that two rooms need to be drawn at once, so using only one current_room variable doesn't really work. I'm not going to go over how to change the code to fix this, but I thought it'd be worth mentioning that the code I provided is not all there is to it and that I'm simplifying things a bit. Once I get into the actual game and implement transitions I'll cover this is more detail.


Room Exercises

1. Create three rooms: CircleRoom which draws a circle at the center of the screen; RectangleRoom which draws a rectangle at the center of the screen; and PolygonRoom which draws a polygon to the center of the screen. Bind the keys F1, F2 and F3 to change to each room.

2. What is the closest equivalent of a room in the following engines: Unity, GODOT, HaxeFlixel, Construct 2 and Phaser. Go through their documentation and try to find out. Try to also see what methods those objects have and how you can change from one room to the another.

3. Pick two single player games and break them down in terms of rooms like I did for Nuclear Throne and Isaac. Try to think through things realistically and really see if something should be a room on its own or not. And try to specify when exactly do addRoom or gotoRoom calls would happen.

4. In a general way, how does the garbage collector in Lua work? (and if you don't know what a garbage collector is then read up on that) How can memory leaks happen in Lua? What are some ways to prevent those from happening or detecting that they are happening?


Room Exercises HELP!

I'm only going to cover the first question, which simply asks for the implementation of 3 rooms and some way to change between them. From the example of a Stage room defined at the start of the article we can see that rooms are just objects with an update and draw function, and that those functions are called if the room is currently active. So first, for the room definitions:

CircleRoom = Object:extend()

function CircleRoom:new()

end

function CircleRoom:update(dt)

end

function CircleRoom:draw()
    love.graphics.circle('fill', 400, 300, 50)
end
RectangleRoom = Object:extend()

function RectangleRoom:new()

end

function RectangleRoom:update(dt)

end

function RectangleRoom:draw()
    love.graphics.rectangle('fill', 400 - 100/2, 300 - 50/2, 100, 50)
end
PolygonRoom = Object:extend()

function PolygonRoom:new()

end

function PolygonRoom:update(dt)

end

function PolygonRoom:draw()
    love.graphics.polygon('fill', 400, 300 - 50, 400 + 50, 300, 400, 300 + 50, 400 - 50, 300)
end

These three should be created as CircleRoom.lua, RectangleRoom.lua and PolygonRoom.lua in the rooms folder. Then, similarly to how we automatically loaded the objects folder we should do it for the rooms one:

function love.load()
    local object_files = {}
    recursiveEnumerate('objects', object_files)
    requireFiles(object_files)

    local room_files = {}
    recursiveEnumerate('rooms', room_files)
    requireFiles(room_files)
end

Then, finally, we can bind the keys with the functions needed to change from one room to another:

function love.load()
    input = Input()

    current_room = nil
    input:bind('f1', function() gotoRoom('CircleRoom') end)
    input:bind('f2', function() gotoRoom('RectangleRoom') end)
    input:bind('f3', function() gotoRoom('PolygonRoom') end)
end

And then pressing f1, f2 or f3 should do this:

If the Simple Room setup is being used then whenever those keys are pressed, a new entire room is being created and the previous one is deleted. It's important to understand this idea.


Areas

Now for the idea of an Area. One of the things that usually has to happen inside a room is the management of various objects. All objects need to be updated and drawn, as well as be added to the room and removed from it when they're dead. Sometimes you also need to query for objects in a certain area (say, when an explosion happens you need to deal damage to all objects around it, this means getting all objects inside a circle and dealing damage to them), as well as applying certain common operations to them like sorting them based on their layer depth so they can be drawn in a certain order. All these funcionalities have been the same across multiple rooms and multiple games I've made, so I condensed them into a class called Area:

Area = Object:extend()

function Area:new(room)
    self.room = room
    self.game_objects = {}
end

function Area:update(dt)
    for _, game_object in ipairs(self.game_objects) do game_object:update(dt) end
end

function Area:draw()
    for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end

The idea is that this object will be instantiated inside a room. At first the code above only has a list of potential game objects, and those game objects are being updated and drawn. All game objects in the game will inherit from a single GameObject class that has a few common attributes that all objects in the game will have. That class looks like this:

GameObject = Object:extend()

function GameObject:new(area, x, y, opts)
    local opts = opts or {}
    if opts then for k, v in pairs(opts) do self[k] = v end end

    self.area = area
    self.x, self.y = x, y
    self.id = UUID()
    self.dead = false
    self.timer = Timer()
end

function GameObject:update(dt)
    if self.timer then self.timer:update(dt) end
end

function GameObject:draw()

end

The constructor receives 4 arguments: an area, x, y position and an opts table which contains additional optional arguments. The first thing that's done is to take this additional opts table and assign all its attributes to this object. So, for instance, if we create a GameObject like this game_object = GameObject(area, x, y, {a = 1, b = 2, c = 3}), the line for k, v in pairs(opts) do self[k] = v is essentially copying the a = 1, b = 2 and c = 3 declarations to this newly created instance. By now you should be able to understand how this works, if you don't then read up more on the OOP section in the past article as well as how tables in Lua work.

Next, the reference to the area instance passed in is stored in self.area, and the position in self.x, self.y. Then an ID is defined for this game object. This ID should be unique to each object so that we can identify which object is which without conflict. For the purposes of this game a simple UUID generating function will do. Such a function exists in a library called lume in lume.uuid. We're not going to use this library, only this one function, so it makes more sense to just take that one instead of installing the whole library:

function UUID()
    local fn = function(x)
        local r = math.random(16) - 1
        r = (x == "x") and (r + 1) or (r % 4) + 9
        return ("0123456789abcdef"):sub(r, r)
    end
    return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end

I place this code in a file named utils.lua. This file will contain a bunch of utility functions that don't really fit anywhere. What this function spits out is a string like this '123e4567-e89b-12d3-a456-426655440000' that for all intents and purposes is going to be unique.

One thing to note is that this function uses the math.random function. If you try doing print(UUID()) to see what it generates, you'll find that every time you run the project it's going to generate the same IDs. This problem happens because the seed used is always the same. One way to fix this is to, as the program starts up, randomize the seed based on the time, which can be done like this math.randomseed(os.time()).

However, what I did was to just use love.math.random instead of math.random. If you remember the first article of this series, the first function called in the love.run function is love.math.randomSeed(os.time()), which does exactly the same job of randomizing the seed, but for LÖVE's random generator instead. Because I'm using LÖVE, whenever I need some random functionality I'm going to use its functions instead of Lua's as a general rule. Once you make that change in the UUID function you'll see that it starts generating different IDs.

Back to the game object, the dead variable is defined. The idea is that whenever dead becomes true the game object will be removed from the game. Then an instance of the Timer class is assigned to each game object as well. I've found that timing functions are used on almost every object, so it just makes sense to have it as a default for all of them. Finally, the timer is updated on the update function.

Given all this, the Area class should be changed as follows:

Area = Object:extend()

function Area:new(room)
    self.room = room
    self.game_objects = {}
end

function Area:update(dt)
    for i = #self.game_objects, 1, -1 do
        local game_object = self.game_objects[i]
        game_object:update(dt)
        if game_object.dead then table.remove(self.game_objects, i) end
    end
end

function Area:draw()
    for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end

The update function now takes into account the dead variable and acts accordingly. First, the game object is update normally, then a check to see if it's dead happens. If it is, then it's simply removed from the game_objects list. One important thing here is that the loop is happening backwards, from the end of the list to the start. This is because if you remove elements from a Lua table while moving forward in it it will end up skipping some elements, as this discussion shows.

Finally, one last thing that should be added is an addGameObject function, which will add a new game object to the Area:

function Area:addGameObject(game_object_type, x, y, opts)
    local opts = opts or {}
    local game_object = _G[game_object_type](self, x or 0, y or 0, opts)
    table.insert(self.game_objects, game_object)
    return game_object
end

It would be called like this area:addGameObject('ClassName', 0, 0, {optional_argument = 1}). The game_object_type variable will work like the strings in the gotoRoom function work, meaning they're names for the class of the object to be created. _G[game_object_type], in the example above, would parse to the ClassName global variable, which would contain the definition for the ClassName class. In any case, an instance of the target class is created, added to the game_objects list and then returned. Now this instance will be updated and drawn every frame.

And that how this class will work for now. This class is one that will be changed a lot as the game is built but this should cover the basic behavior it should have (adding, removing, updating and drawing objects).


Area Exercises

1. Create a Stage room that has an Area in it. Then create a Circle object that inherits from GameObject and add an instance of that object to the Stage room at a random position every 2 seconds. The Circle instance should kill itself after a random amount of time between 2 and 4 seconds.

2. Create a Stage room that has no Area in it. Create a Circle object that does not inherit from GameObject and add an instance of that object to the Stage room at a random position every 2 seconds. The Circle instance should kill itself after a random amount of time between 2 and 4 seconds.

3. The solution to exercise 1 introduced the random function. Augment that function so that it can take only one value instead of two it should generate a random real number between 0 and the value. Also augment the function so that min and max values can be reversed, meaning that the first value can be higher than the second.

4. What is the purpose of the local opts = opts or {} in the addGameObject function?


Area Exercises HELP!

In the first question, the first thing needed is the creation of the Stage room with an Area in it:

Stage = Object:extend()

function Stage:new()
    self.area = Area()
end

function Stage:update(dt)
    self.area:update(dt)
end

function Stage:draw()
    self.area:draw()
end

And then the creation of the Circle game object:

Circle = GameObject:extend()

function Circle:new(area, x, y, opts)
    Circle.super.new(self, area, x, y, opts)
end

function Circle:update(dt)
    Circle.super.update(self, dt)
end

function Circle:draw()
    love.graphics.circle('fill', self.x, self.y, 50)
end

As required, it inherits from GameObject, receives an area, position and optional arguments as arguments. Similarly, it calls the new and update functions from the its parent class. If you don't remember how that works go back to the OOP section in the last article.

After that we can add the functionality where the circle instance kills itself after 2-4 seconds. One of the challenges in doing this is that earlier I said we should only use love.math.random for getting random numbers, but this function only returns integers if we pass in a range. We want a number between 2-4 seconds that includes all non-integer values as well. The way to solve this is through this function (place in utils.lua):

function random(min, max)
    return love.math.random()*(max - min) + min
end

love.math.random() returns a real number between 0 and 1 if no range is passed in, so what this function is doing is taking the min and max values, multiplying the 0-1 value by their difference and then offsetting by the lower amount. Essentially, lets say we called it as random(2, 4), then the love.math.random()*(max - min) + min line parses as [real value between 0 and 1]*(2) + 2, which means that if the value generated is 0.4, for instance, it would become 0.4*2 + 2 -> 2.8, which is a value between 2 and 4 line we wanted.

Anyway, now that this is sorted out we can go back to the Circle class:

function Circle:new(area, x, y, opts)
    Circle.super.new(self, area, x, y, opts)
    self.timer:after(random(2, 4), function() self.dead = true end)
end

And so this makes it that the instance kills itself after 2-4 seconds. After that we need to create one instance of Circle at a random position every 2 seconds:

function Stage:new()
    self.area = Area()
    self.timer:every(2, function()
        self.area:addGameObject('Circle', random(0, 800), random(0, 600))
    end)
end

And then after that all that's left is activating the Stage room in main.lua with gotoRoom('Stage').


END

And with that this part of the tutorial is over. With Rooms and Areas defined we now have some basic structure that our game code can take place in. For the next part I'll finally start with the game itself.

Tilemap to Collision Geometry

2015-01-09 17:30:45

This post describes an algorithm for transforming tilemaps into a level's collision geometry. Here's a high level description of it:

-- grid creation
for each tile
    if should be collsion then set as 1
    else set as 0
  
-- edge creation
for each tile 
    create 4 edges, v0 -> v1, v1 -> v2, v2 -> v3 and v3 -> v0
    where v0 is the bottom left most point 
    where v1 is the top left most point
  
-- edge deletion 1
for each edge e1
    for each edge e2
        if e1 == e2 then delete both e1 and e2
  
-- edge deletion 2
for each edge
    take p1, p2 as the edges start and end points
    find the next edge, the one that starts with p2
    take p3 as the next edges end point
    if p1, p2, p3 are collinear (on the same line)
        delete the p2 -> p3 edge
        make p2 in p1 -> p2 point to p3
  
-- group tagging
set tag number
for each edge
    walk this edge recursively until it cant go on anymore while
    tagging edges you walk through with the current tag number
    increase tag number
  
-- holes
set shapes as polygons defined by the tag numbers
for each shape s1
    for each shape s2
        if s1 is inside s2
            s1 is a hole, so
            set s1 tag number as a hole
  
-- finding zero width points
set holes as shapes that are holes
set all_points as all points from all shapes
for each shape s in holes
    find the two points, one in s and one in all_points
    that have the minimum distance between each other
  
-- make zero width channel
for each pair of points p1, p2 found from the previous step
    set mid_point as the average of p1, p2 
    check the tile value in the position of mid_point
    if the tile value is 1
        create outgoing edge e1 (p1 -> p2)
        create incoming edge e2 (p2 -> p1)
  
-- define vertices from edges 
while #edges > 0
    edge = get an edge from the edges list
    walk this edge recursively until it cant go on anymore while
    adding the points of the edges you go through to a vertices list
  
-- create shapes
using the vertices list, create your shapes

Grid Creation

Create a grid that represents your tilemap in terms of collision. For every tile that needs to be considered as a solid, set it to 1, for every tile that shouldn't, set it to 0. Example:

grid = {
    {1, 1, 1, 1, 1}
    {1, 0, 1, 1, 1}
    {1, 0, 0, 0, 1}
    {1, 1, 1, 0, 1}
    {1, 1, 1, 1, 1}
}

Edge Creation

For every tile that is 1, create the four edges that define it. An edge can be defined as a struct of two points, where the order they appear in defines the edge's direction. For this algorithm, it's important you define the edges in a clockwise manner, starting from v0 (bottom-left point), going to v1 (top-left point) and so on. In the end you should have added the following edges to an edges list so that it looks like this: v0 -> v1, v1 -> v2, v2 -> v3, v3 -> v0. In other words, define edges clockwisely and add each new edge to the end of the list.


Edge Deletion 1

For every edge in the edges list, check to see if it appears once or more than once. If it appears more than once then delete all instances of it.

This image shows how the dotted edges appear more than once and how they're always inner edges instead of outer ones. For purposes of collision we only care about the outer ones, so it makes sense to delete inners. It's important to realize that because of the order in which edges are created, edge equality means that for one edge, its points are v0 -> v1, but for the other its points are v1 -> v0. If you try to go through the edges list and only look for edges that have the same start and end points (without reversing them), then this procedure will failure.


Edge Deletion 2

This is where we get rid of redundant edges. After taking away the inner edges, we still have multiple outer edges that sometime define a single line. Ideally, these multiple edges should be merged into one when possible. The way to do this is to go through each edge, to take its points p1 and p2, find the next edge (the one that starts with p2), take its end point p3, and then check if p1, p2 and p3 are collinear (if they are on the same line). If they are collinear, then delete the p2 -> p3 edge and make the p1 -> p2 edge point to p3 instead of p2.

And then you repeat that until you only have the relevant edges. In this case you'd only have four edges in the end:

Now, the algorithm could end here. For a lot of cases it works fine without any problems. You should have a list of edges and all you'd have to do is to go through them, grab the vertices in the right order (by choosing an edge, then finding the edge that follows from it by checking the end/start points (they're not necessarily ordered like this in the edges list)) and then just pushing those vertices into whatever data structure you use for creating level geometry. I use box2d chain shapes so I just have to push the vertices list with the vertices in an order that makes sense and it works fine.

There's one big problem, though, this algorithm doesn't work for shapes with holes in them. To the edges list, holes are just another shape, since you have the main outer shape, and then you have these other inner edges that don't connect with anyone but that define a shape anyway, since they were never deleted. If your physics library allows you to carve polygons into other polygons and get a single carved polygon out of that then that's what you should do here. If somehow you can also easily triangulate polygons then I think you can avoid the rest of the work and just create multiple triangles instead of one single polygon. Otherwise, read further.


Group Tagging

The first thing to do is identifying how many shapes there are in this tilemap, where a shape is defined by an actually usable outer shape, or a shape inside another shape (either a hole or not). To do this, simply set a starting tag number. After this, for each edge, walk recursively to the edges that are connected with each other and not yet tagged until you can't walk anymore (usually when you reach the initial edge), all the while tagging those edges with the current tag number. After this procedure ends, increase the tag number and continue doing it until all edges are tagged.

The image above shows how four shapes are tagged. Since none of the edges in each shape connect to the others, they're all considered different by the algorithm and so they're numbered differently. This is what the grid for the shapes above looks like, in case it's confusing:

grid = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 1, 1, 1, 1, 1, 1, 0, 1},
    {1, 0, 1, 0, 0, 0, 0, 1, 0, 1},
    {1, 0, 1, 1, 1, 1, 1, 1, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
}

Holes

Now we need to figure out which shapes are holes. For the example above we have the space between 2 and 3 being holes, and the space inside 4 being holes. So it makes sense that we should come to the conclusion that 2 and 4 are holes. However, to simplify the algorithm we'll just say that a shape is a hole if it is contained inside another shape. With this in mind, the only shape that isn't a hole in the example above would be the one tagged with 1.

So, to do this, we first define each shape as a list of vertices. You can do this by walking through the edges of the shape until it reaches the starting one and just adding all the points to another list. Then, for each shape against each shape (double for loop), check to see if one shape is inside the other (polygon inside polygon check), if it is, then set that tag number as a hole.


Zero Width Points

The reason we're identifying holes and not holes is because ultimately we want to make a holed polygon into an actual polygon with a hole in it. But you can't really define a polygon with a hole (at least as far as I know), so to do this we need create "zero-width channels" (as explained in Real Time Collision Detection, 12.5.1.1 Triangulating Polygons with Holes, page 499):

To be able to triangulate polygons with holes, a reasonable representation must first be established. A common representation is as a counterclockwise outer polygon boundary plus one or more clockwise inner hole boundaries. The drawing on the left in Figure 12.19 illustrates a polygon with one hole.

To be able to triangulate the polygon, the outer and inner boundaries must be merged into a single continuous chain of edges. This can be done by cutting a zero-width "channel" from the outer boundary to each inner boundary. Cutting this channel consists of breaking both the inner and outer boundary loops at a vertex each and then connecting the two vertices with two coincident edges in opposite directions such that the outer boundary and the hole boundary form a single counterclockwise continuous polygon boundary. Figure 12.19 shows how a hole is effectively resolved by this procedure. Multiple holes are handled by repeating the cutting procedure for each hole.

Where you just change counterclockwise for clockwise and where Figure 12.19 is the one below (edge orientation already changed):

Now, to find the points that we'll use for the zero width channel we do the following: hold all shapes that are holes inside one array, hold all points from all shapes (holes or not) in another. Then for each shape in the holes array we find the minimum distance between one of its points with all the other points (except its own points). So now for each hole shape, you have that hole's zero-width point and another point in an outer shape, which is the destination of the zero-width channel.


Zero Width Channel

To create the channel, for each hole shape we first must check if the point between the two zero-width points we found in the previous step is occupied as a tile or not. This is because there is no way of knowing which hole is solid or isn't. So, in this situation:

Something like this would happen:

The channels between 1, 2 and 3, 4 are okay because they're supposed to exist, but the channel between 2, 3 shouldn't. Since the algorithm doesn't know that what's between 2 and 3 is not supposed to be solid, we need to do a midpoint tile value check (to see if the tile is solid or not). After doing this check, add the new edges to the edges list.


END

After this it's pretty much over. All that's left to do is to define all vertices from the edges list. This list is not ordered in any way, so you have to walk the edges (like before) from one to the next. If your tilemap has multiple isolated shapes then you need to make sure you do this walk multiple times to cover all those shapes, but between each shape, a single walk should be fine, since even if it has holes it's all connected because of the zero-width channels. After you have all vertices you can just go through those and create your level geometry.

Downwell Trails

2016-01-01 07:05

This post will explain how to make trails like the ones in Downwell from scratch using LÖVE. There's another tutorial on this written by Zack Bell here. If you don't know how to use LÖVE/Lua but have some programming knowledge then this tutorial might also serve as a learning exercise on LÖVE/Lua itself.


LÖVE

Before starting with the actual effect we need to get some base code ready, basically just an organized way of creating and deleting entities as well as drawing them to the screen. Since LÖVE doesn't really come with this built-in we need to build it ourselves.

The first thing to do is create a folder for our project and in it a main.lua file with the following contents:

function love.load()

end

function love.update(dt)

end

function love.draw()

end

When a LÖVE project is loaded, love.load() is called on startup once and then love.update and love.draw are called every frame. To run this basic LÖVE project you can check out the Getting Started page. After you manage to run it you should see a black screen.


GameObject

Now, we'll need some entities in our game and for that we'll use an OOP library called classic. Like the github page says, after downloading the library and placing the classic folder in the same folder as the main.lua file, you can require it by doing:

Object = require 'classic/classic'

function love.load()
...

Now Object is a global variable that holds the definition of the classic library, and with it you can create new classes, like Point = Object:extend(). We'll use this to create a GameObject class which will be the main class we use for our entities:

-- in GameObject.lua
GameObject = Object:extend()

function GameObject:new()

end

function GameObject:update(dt)

end

function GameObject:draw()

end

Since we defined this in GameObject.lua, we need to require that file in the main.lua script, otherwise we won't have access to this definition:

-- in main.lua
Object = require 'classic/classic'
require 'GameObject'

And now GameObject is a global variable that holds the definition of the GameObject class. Alternatively, we could have defined the GameObject variable locally in GameObject.lua and then made it a global variable in main.lua, like so:

-- in GameObject.lua
local GameObject = Object:extend()

function GameObject:new()
...
...

return GameObject
-- in main.lua
Object = require 'classic/classic'
GameObject = require 'GameObject'

Sometimes, like when writing libraries for other people, this is a better way of doing things so you don't pollute their global state with your library's variables. This is what classic does as well, which is why you have to initialize it by assigning it to the Object variable. One good result of this is that since we're assigning a library to a variable, if you wanted to you could have named Object as Class instead, and then your class definitions would look like GameObject = Class:extend().

Finally, a GameObject needs to have some properties. One simple setup that I've found useful was to make it so that the constructor calls for all my objects follow the same pattern of ClassName(x, y, optional_arguments). I've found that all objects need to have some position (and if they don't then that can just be 0, 0) and then optional_arguments is a table with as many optional arguments as you want it to have. For our GameObjects this would look like this:

-- in GameObject.lua
local GameObject = Object:extend()

function GameObject:new(x, y, opts)
  self.x, self.y = x, y
  local opts = opts or {} -- this handles the case where opts is nil
  for k, v in pairs(opts) do self[k] = v end
end
-- in main.lua
...
function love.load()
  game_object = GameObject(100, 100, {foo = 1, bar = true, baz = 'hue'})
  print(game_object.x, game_object.foo, game_object.bar, game_object.baz)
end

And so in this example you'd create a GameObject instance called game_object with those attributes. You can read up more about how the for in the constructor works here in the Objects, attributes and methods in Lua section.

Another thing from the code above is the usage of the print function. It's a useful tool for debugging but you need to be running your application from a console, or if you're on Windows, whatever hooks you have created to run your project, you need to add the --console option so that a console shows up and print statements are printed there. print statements will not be printed on the main game screen and it's kind of a pain to debug things and tie them to the main screen, so I highly recommend figuring out how to make the console show up as well whenever you run the game.


Object Creation and Deletion

The trail effect is achieved by creating multiple trail instances and quickly deleting them (like after 0.2 seconds they were created), so because of that we need some logic for object creation and deletion. Object creation was shown above, the only additional step we need is adding the object to a table that will hold all of them (table.insert):

function love.load()
  game_objects = {}
  createGameObject(100, 100)
end

function love.update(dt)
  for _, game_object in ipairs(game_objects) do
    game_object:update(dt)
  end
end

function love.draw()
  for _, game_object in ipairs(game_objects) do
    game_object:draw()
  end
end

function createGameObject(x, y, opts)
  local game_object = GameObject(x, y, opts)
  table.insert(game_objects, game_object)
  return game_object -- return the instance in case we wanna do anything with it
end

So with this setup I created a function called createGameObject, which creates a new GameObject instance and adds it to the game_objects table. The objects in this table are being updated and drawn every frame in love.update and love.draw respectively (and they need to have both those functions defined). If you run this example nothing will happen still, but if you change GameObject:draw a bit to draw something to the screen, then you should see it being drawn (love.graphics.circle):

function GameObject:draw()
  love.graphics.circle('fill', self.x, self.y, 25)
end

For deleting objects we need a few extra steps. First, for each GameObject we need to create a new variable called dead which will tell you if this GameObject is alive or not:

function GameObject:new(x, y, opts)
  ...
  self.dead = false
end

Then, we need to change our update logic a bit to take into account dead entities, and making sure that we remove them from the game_objects list (table.remove):

function love.update(dt)
  for i = #game_objects, 1, -1 do
    local game_object = game_objects[i]
    game_object:update(dt)
    if game_object.dead then table.remove(game_objects, i) end
  end
end

And so whenever a GameObject instance has its dead attribute set to true, it will automatically get removed from the game_objects list. One of the important things to note here is that we're going through the list backwards. This is because in Lua, whenever something is removed from a list it gets resorted so as to leave no nil spaces in it. This means that if object 1 and 2 need to be deleted, if we go with a normal forwards loop, object 1 will be deleted, the list will be resorted and now object 2 will be object 1, but since we already went to the next iteration, we'll not get to delete the original object 2 (because it's in the first position now). To prevent this from happening we go over the list backwards.

To test if this works out we can bind the deletion of a single instance we created to a mouse click (love.mousepressed):

function love.load()
  game_objects = {}
  game_object = createGameObject(100, 100)
end

...

function love.mousepressed(x, y, button)
  if button == 1 then -- 1 = left click
    game_object.dead = true
  end
end

Finally, one thing we can do with the mouse is binding game_object's position to the mouse position (love.mouse.getPosition):

function GameObject:update(dt)
  self.x, self.y = love.mouse.getPosition()
end

Screen Resolution

Before we move on to making the actual trails and getting into the meat of this, we need to do one last thing. A game like Downwell has a really low native resolution that gets scaled up to the size of the screen, and this creates a good looking pixelated effect on whatever you draw to the screen. The default resolution a LÖVE game uses is 800x600 and this is a lot higher than what we need. So to decrease this to 320x240 what we can do is play with conf.lua, LÖVE's configuration file, and change the default resolution to our desired size. To do this, create a file named conf.lua at the same level that main.lua is in, and fill it with these contents (conf.lua):

function love.conf(t)
    t.window.width = 320
    t.window.height = 240
end

Now, to scale this up to, say, 960x720 (scaling it up by 3) while maintaining the pixel size of 320x240, we need to draw the whole screen to a canvas and then scale that up by 3. In this way, we'll always be working as if everything were at the small native resolution, but at the last step it will be drawn way bigger (love.graphics.newCanvas):

function love.load()
  ...
  main_canvas = love.graphics.newCanvas(320, 240)
  main_canvas:setFilter('nearest', 'nearest')
end

function love.draw()
  love.graphics.setCanvas(main_canvas)
  love.graphics.clear()
  for _, game_object in ipairs(game_objects) do
      game_object:draw()
  end
  love.graphics.setCanvas()

  love.graphics.draw(main_canvas, 0, 0, 0, 3, 3)
end

First we create a new canvas, main_canvas, with the native size and set its filter to nearest (so that it scales up with the nearest neighbor algorithm, essential for pixel art). Then, instead of just drawing the game objects directly to the screen, we set main_canvas with love.graphics.setCanvas, clear the contents from this canvas from the last frame with love.graphics.clear, draw the game objects, and then draw the canvas scale up by 3 (love.graphics.draw). This is what it looks like:

Compared to what it looked like before the pixel art effect is there, so it seems to be working. You might wanna also make the window itself bigger (instead of 320x240), and you can do that by using love.window.setMode:

function love.load()
  ...
  love.window.setMode(960, 720)
end

If you're following along by coding this yourself you might have noticed that the mouse position code is now broken. This is because love.mouse.getPosition works in based on the screen size, and if the mouse is now on the middle of the screen 480, 360, this is bigger than 320, 240, which means the circle won't be drawn on the screen. Basically because of the way we're using the canvas we only work in the 320, 240 space, but the screen is bigger than that. To really solve this we should use some kind of camera system, but for this example we can just do it the quick way and divide the mouse position by 3:

function GameObject:update(dt)
  local x, y = love.mouse.getPosition()
  self.x, self.y = x/3, y/3
end

Timing and Multiple Object Types

Now we have absolutely everything we need to actually make the trail effect. That was a lie. First we need a timing library. This is because we need an easy way to delete an object n seconds after it has been created and an easy way to tween an object's properties, since this is what trails generally do. To do this I'll use HUMP. To install it, download it and place it on the project's folder, then require the timer module:

Object = require 'classic/classic'
GameObject = require 'GameObject'
Timer = require 'hump/timer'

Now we can create an instance of a timer to use. I find it a good idea to create one timer per object that needs a timer, but for this examples it's fine it we just use a global one:

function love.load()
  timer = Timer()
  ...
end

function love.update(dt)
  timer.update(dt)
  ...
end

We can test to see if the timer works by using one of its functions, after. This function takes as arguments a number n and a function which is executed after n seconds:

function love.load()
  timer = Timer()
  timer.after(4, function() game_object.dead = true end)
  ...
end

And so in this example game_object will be deleted after the game has run for 4 seconds.


And this is finally the very last thing we need to do before we can actually do trails, which is defining multiple object types. Each trail object (that will get deleted really fast) will need to be an instance of some class, but it can't be the GameObject class, since we want that class to have the behavior that follows the mouse and draws the circle. So now what we need to do is come up with a way of supporting multiple object types. There are multiple ways of doing this, but I'll go with the simple one:

function createGameObject(type, x, y, opts)
  local game_object = _G[type](x, y, opts)
  table.insert(game_objects, game_object)
  return game_object -- return the instance in case we wanna do anything with it
end

All we changed in this function is accepting a type variable and then using that with _G. _G is the table that holds all global state in Lua. Since it's just a table, you can access the values in it by using the appropriate keys, which happen to be the names of the variables. So for instance _G['game_object'] contains the GameObject instance that we've been using so far, and _G['GameObject'] contains the GameObject class definition. So whenever using createGameObject instead of calling createGameObject(x, y, opts) we now have to do createGameObject(class_name, x, y, opts):

function love.load()
  game_object = createGameObject('GameObject', 100, 100)
  ...
end

When we add the trail objects all we need to do to create them instead of a GameObject is changing the first parameter we pass to createGameObject.


Trails

Now to make trails. One way trails can work is actually pretty simple. You have some object you want to emit some sort of trail effect, and then every n seconds (like 0.02) you create a Trail object that will look like you want the trail to look. This Trail object will usually be deleted very quickly (like 0.2 seconds after its creation) otherwise you'll get a really really long trail. We have everything we need to do that now, so let's start by creating the Trail class:

-- in Trail.lua
local Trail = Object:extend()

function Trail:new(x, y, opts)
  self.dead = false
  self.x, self.y = x, y
  local opts = opts or {} -- this handles the case where opts is nil
  for k, v in pairs(opts) do self[k] = v end

  timer.after(0.15, function() self.dead = true end)
end

function Trail:update(dt)

end

function Trail:draw()
  love.graphics.circle('fill', self.x, self.y, self.r)
end

return Trail

The first 4 lines of the constructor are exactly the same as for GameObject, so this is just standard object attributes. The important part comes in the next line that uses the timer. Here all we're doing is deleting this trail object 0.15 seconds after it has been created. And in the draw function we're simply drawing a circle and using self.r as its radius. This attribute will be specified in the creation call (via opts) so we don't need to worry about it in the constructor.

Next, we need to create the trails and we'll do this in the GameObject constructor. Every n seconds we'll create a new Trail instance at the current game_object position:

function GameObject:new(x, y, opts)
  ...
  timer.every(0.01, function() createGameObject('Trail', self.x, self.y, {r = 25}) end)
end

So here every 0.01 seconds, or every frame, we're creating a trail at self.x, self.y with r = 25. One important thing to realize is that self.x, self.y inside that function is always up to date with the current position instead of only the self.x, self.y values we have in the constructor. This is because that function is getting called every frame and because of the way closures work in Lua, that function has access to self, and self is always being updated so it all works out. And that should look like this:

Not terribly amazing but we're getting somewhere.


Downwell Trail Effect

To make the Downwell trail effect we want to erase lines from the trail only, either horizontally or vertically. To do this we need to separate the drawing of the main game object and the drawing of the trails into separate canvases, since we only want to apply the effect to one of those types of objects. The first thing we need to do to get this going is being able to differentiate between which objects are of which class when drawing, and currently we have no way of doing that. Instances of classes created by classic have no default attributes magically set to them that say "I'm of this class", so we have to do that manually.

-- in main.lua
function createGameObject(type, x, y, opts)
  local game_object = _G[type](type, x, y, opts)
  ...
end
-- in GameObject.lua
function GameObject:new(type, x, y, opts)
  self.type = type
  ...
end
-- in Trail.lua
function Trail:new(type, x, y, opts)
  self.type = type
  ...
end

All we've done here is added the type attribute to all objects, so that now we can do stuff like if object.type == 'Trail' and figure out what kind of object we're dealing with.

Now before we separate object drawing to different canvases we should create those:

function love.load()
  ...
  game_object_canvas = love.graphics.newCanvas(320, 240)
  game_object_canvas:setFilter('nearest', 'nearest')
  trail_canvas = love.graphics.newCanvas(320, 240)
  trail_canvas:setFilter('nearest', 'nearest')
end

Those creation calls are exactly the same as the ones we used for creating our main_canvas. Now, to separate trails and the game object, we need to draw the trails to trail_canvas, draw the game object to game_object_canvas, draw both of those to the main_canvas, and then draw main_canvas to the screen while scaling it up. And that looks like this:

function love.draw()
  love.graphics.setCanvas(trail_canvas)
  love.graphics.clear()
  for _, game_object in ipairs(game_objects) do
    if game_object.type == 'Trail' then
      game_object:draw()
    end
  end
  love.graphics.setCanvas()

  love.graphics.setCanvas(game_object_canvas)
  love.graphics.clear()
  for _, game_object in ipairs(game_objects) do
    if game_object.type == 'GameObject' then
      game_object:draw()
    end
  end
  love.graphics.setCanvas()

  love.graphics.setCanvas(main_canvas)
  love.graphics.clear()
  love.graphics.draw(trail_canvas, 0, 0)
  love.graphics.draw(game_object_canvas, 0, 0)
  love.graphics.setCanvas()

  love.graphics.draw(main_canvas, 0, 0, 0, 3, 3)
end

There's totally some dumb stuff going on here, like for instance we're going over the list of objects twice now (even though we're not drawing twice), and that could totally be optimized. But really for an example this small this doesn't matter at all. What's important is that it works!

So now that we've separated different things into different render targets we can move on with our effect. The way to erase lines from all the trails is to draw lines to the trail canvas using the subtract blend mode. What this blend mode does is literally just subtract the values of the thing you're drawing from what's already on the screen/canvas. So in this case what we want is to draw a bunch of white lines (1, 1, 1, 1) with subtract enabled to trail_canvas (after the trails have been drawn), in this way the places where those lines would appear will become (0, 0, 0, 0) (love.graphics.setBlendMode):

function love.load()
  ...
  love.graphics.setLineStyle('rough')
end

function love.draw()
  love.graphics.setCanvas(trail_canvas)
  love.graphics.clear()
  for _, game_object in ipairs(game_objects) do
    if game_object.type == 'Trail' then
      game_object:draw()
    end
  end

  love.graphics.setBlendMode('subtract')
  for i = 0, 360, 2 do
    love.graphics.line(i, 0, i, 240)
  end
  love.graphics.setBlendMode('alpha')
  love.graphics.setCanvas()

  ...
end

And this is what that looks like:

One important thing to do before drawing this is to set the line style to 'rough' using love.graphics.setLineStyle, since the default is 'smooth' and that doesn't work with the pixel art style generally. And if you didn't really understand what's going on here, here's what the lines by themselves would look like if they were drawn normally:

So all we're doing is subtracting that from the trail canvas, and since the only places in the trail canvas where there are things to be subtracted from are the trails, we get the effect only there. Another thing to note is that the subtract blend mode idea would only work if you have a black background. For instance, I tried this effect in my game and this is what it looks like:

If I were to use the subtract blend mode here it just would look even worse than it does. So instead what I did was use multiply. Like the name indicates, it just multiplies the values instead of subtracting them. So the white lines (1, 1, 1, 1) won't change the output in any way, while the gaps (0, 0, 0, 0) will make whatever collides with them transparent. In this way you get the same effect, the only difference is that with subtract the white lines themselves result in transparency, while with multiply the gaps between them do.


Details

Now we already have the effect working but we can make it more interesting. For starters, we can randomly draw extra lines so that it creates some random gaps in the final result:

function love.load()
  ...
  trail_lines_extra_draw = {}
  timer.every(0.1, function()
    for i = 0, 360, 2 do
      if love.math.random(1, 10) >= 2 then trail_lines_extra_draw[i] = false
      else trail_lines_extra_draw[i] = true end
    end
  end)
end

function love.draw()
  ...
  love.graphics.setBlendMode('subtract')
  for i = 0, 360, 2 do
    love.graphics.line(i, 0, i, 240)
    if trail_lines_extra_draw[i] then
      love.graphics.line(i+1, 0, i+1, 240)
    end
  end
  love.graphics.setBlendMode('alpha')
  ...
end

So here every 0.1 seconds, for every line we're drawing, we set some values to true or false to an additional table, trail_lines_extra_draw. If the value for some line in this table is true, then whenever we get to drawing that line, we'll also draw the another line 1 pixel to its right. Since we're looping over lines on a 2 by 2 pixels basis, this will create a section where there are 3 lines being drawn at once, and this will create the effect of a few lines looking like they're missing from the trail. You can play with different chances and times (every 0.05 seconds?) to see what you think looks best.


Now another thing we can do is tween the radius of the trail circles down before they disappear. This will give the effect a much more trail-like feel:

function Trail:new(type, x, y, opts)
  ...
  timer.tween(0.3, self, {r = 0}, 'linear', function() self.dead = true end)
end

I deleted the previous timer.after call that was here and changed it for this new timer.tween. The tween call takes a number of seconds, the target table, the target value inside that table to be tweened, the tween method, and then an optional function that gets called when the tween ends. In this case, we're tweening self.r to 0 over 0.3 seconds using the linear interpolation method, and then when that is done we kill the trail object. That looks like this:


Another thing we can do is, every frame, adding some random amount within a certain range to the radius of the trail circles being drawn. This will give the trails an extra bit of randomization:

-- in main.lua
function randomp(min, max)
    return (min > max and (love.math.random()*(min - max) + max)) or (love.math.random()*(max - min) + min)
end
-- in Trail.lua
function Trail:draw()
    love.graphics.circle('fill', self.x, self.y, self.r + randomp(-2.5, 2.5))
end

Here we just need to define a function called randomp, which returns a float between min and max. In this case we use it to add, to the radius of every trail, every frame, a random amount between -2.5 and 2.5. And that looks like this:


Something else that's possible is rotating the angle of the lines based on the angle of the velocity vector of whatever is generating the trails. To do that first we need to figure out the velocity of our game object. An easy way to achieve this is storing its previous position, then subtracting the current position from the previous one and getting the angle of that:

function GameObject:new(type, x, y, opts)
  ...
  self.previous_x, self.previous_y = x, y
end

function GameObject:update(dt)
  ...
  self.angle = math.atan2(self.y - self.previous_y, self.x - self.previous_x)
  self.previous_x, self.previous_y = self.x, self.y
end

To calculate the current angle we use the current self.x, self.y and the values for x, y from the previous frame. Then after that, at the end of the update function, we set the values of the current frame to the previous variables. If you want to test if this actually works you can try print(math.deg(self.angle)). Keep in mind that LÖVE uses a reverse angle system (up from 0 is negative).

After we have the angle we can try to rotate the lines being drawn like this (push, pop):

-- in main.lua
function pushRotate(x, y, r)
    love.graphics.push()
    love.graphics.translate(x, y)
    love.graphics.rotate(r or 0)
    love.graphics.translate(-x, -y)
end

function love.draw()
  ...
  pushRotate(160, 120, game_object.angle + math.pi/2)
  love.graphics.setBlendMode('subtract')
  for i = 0, 360, 2 do
    love.graphics.line(i, 0, i, 240)
    if trail_lines_extra_draw[i] then
      love.graphics.line(i+1, 0, i+1, 240)
    end
  end
  love.graphics.setBlendMode('alpha')
  ...
end

pushRotate is a function that will make everything drawn after it rotated by r with the pivot position being x, y. This is useful in a lot of situations and in this instance we're using it to rotate all the lines by the angle of game_object. I added math.pi/2 to the angle because it came out sideways for whatever reason... Anyway, that should look like this:

Not really sure if it looks better or worse, looks like it's fine for slow moving situations but when its too fast it can get a bit chaotic (probably because I stop the object and then the angle changes abruptly midway).

One problem with this way of doing things is that since all the lines are being rotated but they're being drawn at first to fit the screen, you'll get places where the lines simply don't exist anymore and the effect fails to happen, which causes the trails to look all white. To prevent this we can just draw lines way beyond the screen boundaries on all directions, such that with any rotation happening lines will still be drawn anyway:

  ...
  trail_lines_extra_draw = {}
  timer.every(0.1, function()
    for i = -360, 720, 2 do
      if love.math.random(1, 10) >= 2 then trail_lines_extra_draw[i] = false
      else trail_lines_extra_draw[i] = true end
    end
  end)
  ...
  pushRotate(160, 120, game_object.angle + math.pi/2)
  love.graphics.setBlendMode('subtract')
  for i = -360, 720, 2 do
    love.graphics.line(i, -240, i, 480)
    if trail_lines_extra_draw[i] then
      love.graphics.line(i+1, -240, i+1, 480)
    end
  end
  love.graphics.setBlendMode('alpha')
  ...

Finally, one last cool thing we can do is changing the shape of the main ball and of the trails based on its velocity. We can use the same idea from the example above, except to calculate the magnitude of the game object's velocity instead of the angle:

function GameObject:update(dt)
  ...
  self.vmag = Vector(self.x - self.previous_x, self.y - self.previous_y):len()
  ...
end

Vector was initialized in main.lua and it comes from HUMP. I'll omit that code because you should be able to do that by now. Anyway, here we calculate a value called vmag. This value is basically an indicator of how fast the object is moving in any direction. Since we already have the angle the magnitude of the velocity is all we really need. With that information we can do the following:

-- in main.lua
function map(old_value, old_min, old_max, new_min, new_max)
    local new_min = new_min or 0
    local new_max = new_max or 1
    local new_value = 0
    local old_range = old_max - old_min
    if old_range == 0 then new_value = new_min
    else
        local new_range = new_max - new_min
        new_value = (((old_value - old_min)*new_range)/old_range) + new_min
    end
    return new_value
end
-- in GameObject.lua
function GameObject:update(dt)
  ...
  self.vmag = Vector(self.x - self.previous_x, self.y - self.previous_y):len()
  -- print(self.vmag)
  self.xm = map(self.vmag, 0, 20, 1, 2)
  self.ym = map(self.vmag, 0, 20, 1, 0.25)
  ...
end

function GameObject:draw()
  pushRotate(self.x, self.y, self.angle)
  love.graphics.ellipse('fill', self.x, self.y, self.xm*15, self.ym*15)
  love.graphics.pop()
end

And that looks like this:

Let's go part by part. First the map function. This is a function that takes some value named old_value, two values named old_min and old_max representing the range that old_value can take, and two other values named new_min and new_max, representing the desired new range. With all this it returns a new value that corresponds to old_value, but if it were in the new_min, new_max range. For instance, if we do map(0.5, 0, 1, 0, 100) we'll get 50 back. If we do map(0.5, 0, 1, 200, -200) we'll get 0 back. If we do map(0.5, 0, 1, -200, 100) we'll get -50 back. And so on...

We use this function to calculate the variables self.xm and self.ym. These variables will be used as multiplication factors to change the size of the ellipse we're drawing. One thing to keep in mind is that when drawing the ellipse we're first rotating everything by self.angle, this means that we should always consider what we want to happen when drawing as if we were at angle 0 (to the right), because all other angles will just happen automatically from that.

Practically, this means that we should consider the changes in shape of our ellipse from a horizontal perspective. So when the game object is going really fast we want the ellipse to stretch horizontally and shrink vertically, which means that the values we find for self.xm and self.ym have to reflect that. As you can see, for self.xm, when self.vmag is 0 we get 1 back, and when it's 20 we get 2, meaning, when self.vmag is 20 we double the size of the ellipse horizontally, bringing it up to 30. For self.vmag values greater than that we increase it even more. Similar logic applies to self.ym and how it shrinks.

It's important to note that those values (0, 20, 2, 0.25) used in those two map functions up there were reached by trial and error and seeing what looks good. Most importantly, they're really exaggerated so that the effect can actually be seen well enough in these gifs. I would personally go with the values 0 and 60 instead of 20 if I were doing this normally.

Finally, we can also apply this to the Trail objects:

-- in GameObject.lua
function GameObject:new(type, x, y, opts)
    ...
    timer.every(0.01, function() createGameObject('Trail', self.x, self.y, {r = 20, 
        xm = self.xm, ym = self.ym, angle = self.angle})
    end)
end
-- in Trail.lua
function Trail:draw()
    pushRotate(self.x, self.y, self.angle)
    love.graphics.ellipse('fill', self.x, self.y, self.xm*(self.r + randomp(-2.5, 2.5)), 
        self.ym*(self.r + randomp(-2.5, 2.5)))
    love.graphics.pop()
end

We just change the way trails are being drawn to be the same as the main game object. We also make sure to send the trail object the information needed (self.xm, self.ym, self.angle), otherwise it can't really do anything. And that looks like this:

There are a few bugs like the velocity going way too big and the self.ym value becoming negative, making each ellipse really huge, but this can be fixed by just doing some checks. Also, sometimes when you go kinda fast with abrupt angle changes you can see the shapes of each ellipse individually and that looks kinda broken. I don't know exactly how I'd fix that other than not having abrupt angle changes on an object with that kind of trail effect.


END

That's it. I hope we've all learned something about friendship today.

Why I really like Artifact

Artifact has been out for about a month now and I've been playing it a lot. By now I have a pretty good grasp on the game's mechanics, I can easily tell when mistakes are made and I've reached a pretty good rank (67 out of 75). Although because of the way Valve implemented the ranking system I can't tell if this number means anything. Either way, the point is that I'm fairly experienced at the game at this point.

The main reason for writing this article is that Artifact is a game with a lot of sources of randomness and this makes it the perfect environment to exercise the mindset I went over in this article about luck. And so that's what this article will talk about!

If you already play Artifact you can skip to the Luck section.


RNG sources

Artifact has about 5 types of RNG sources that I can think about right now: hero placement and creep placement, arrows, item shop, and the effects of some specific heroes or cards. Let's go over them one by one.


Hero and creep placement

Every deck has to have 5 heroes and you get to choose the order in which they are deployed. Since the game has 3 lanes, the game will start deploying the first three and then the last 2 over the next 2 turns.

In the image above, we have a deck with the following heroes: Venomancer, Necrophos, Zeus, Ogre Magi and Tinker. The first 3 will be deployed first, and this is what the board state could look like right after the game starts:

As you can see, the first 3 heroes (Veno, Necro and Zeus) are deployed against 3 enemy heroes. The catch is that creep deployment is randomized, and so in the picture above you can see that the first lane has two creeps on our side, the second has none, and the third has one. If creep placement is randomized, the chances that our heroes will face the opponents heroes are also randomized. In the picture above we got lucky and our heroes faced none of the enmy heroes. But check this deployment out:

In this one, our Necro is facing the enemy Bristleback mid and immediately dying. And Zeus is facing Viper right and dying next turn.

This source of RNG is the first one that you have to learn to deal with, and the general way of by placing stronger heroes first (as the first 3 deploys) so that they can trade better. Often times this won't be possible (like in a blue deck because blue generally has weaker heroes), but more often than not it is. And this also has to be weighted against the fact that losing a hero early is hardly ever a huge advantage to the enemy, so sometimes you'll want to place weaker heroes first because you have better cards for that hero's color in your deck, even though there's a chance it will die immediately.

For instance, blue has a lot of cards that control the board state (ally/enemy placement and arrows), so if you have lots of early game cards that do that, placing blue heroes first isn't that big of a problem. In the deployment above, Zeus is facing Viper, but in our hand we have a card called Cunning Plan:

Which means that we can go from this:

To this:

The point being, hero and creep placement at the start is random, but there are many ways around it. Finally, as rounds go on, you can choose to place next heroes on specific lanes, but you also can't choose their specific positions, nor the positions of your creeps. So for instance, the next round of the game above looks like this:

And so now we have to choose where to place the Ogre Magi. If we place him mid he will 100% of the times face Bristleback and die immediately. The only scenario where that doesn't happen is if the enemy places Magnus mid also and then there's a 50% chance that Ogre will face Magnus, but relying on this would be a big mistake, especially given that we don't know if the enemy will place Magnus there.

Placing Ogre right has a 50% chance that he will face Viper, given that we also have a creep going there and either the creep or Ogre would be placed in front of Viper. And finally, placing the Ogre left has a 33% chance it will be in front of Ursa and die immediately. Additionally, we might want to consider where the enemy will place Magnus, which will probably also be left, given that the first lane is generally the strongest one that you want to contest and win. If the enemy does that we're also good, because even though our Ogre might die in 2 turns if facing Magnus, it's better than all the other options we have.

And so in this case what actually happened is that we got unlucky and our Ogre faced Ursa, and on top of that he played a spell that gives him cleave and so he will just kill everything that lane:

This is the exact type of scenario that happens when you have sources of RNG like that in the game. We chose the best option out of what we had and it still didn't work in our favor!


Arrows

Arrows are another source of RNG in Artifact which dictate where the unit will attack in case there's nothing in front of it. There's a 50% chance the unit will attack the tower, and a 25% chance it will attack a neighbor to either side. In the image below, for instance, the enemy creep routed to our Necrophos instead of dealing damage to the tower:

In this image we have 3 enemy heroes routing to a creep in a very unlucky manner and overkilling it by a lot instead of dealing more damage to the tower:

And the examples are endless. This is also something that you can play around by having cards that deal damage and remove enemy creeps/heroes altogether before the combat phase, or by using cards that manipulate arrows, like Compel:

Either way, it's another source of RNG that exists and is fundamental to the way the game works.


Item shop

Every deck has to also have an item deck. These are at least 9 item cards that can be bought with gold in the game. Gold is acquired by killing creeps (1 gold) or enemy heroes (5 gold). Between each round you will be shown the item shop and it will you give you 3 options to buy from:

On the left you have the secret shop, which as far as I can tell gives you the option to buy a random item from the pool of all items in the game. The secret shop will occasionally show items that are really good to have if you have the gold to buy them, but that you wouldn't necessarily put in your item deck for many reasons. You can also spend 1 gold to hold that item so that it shows up on the next turn in case you don't have the gold to buy it right now.

In the middle you have your item deck. These are the items you choose yourself. They are also shown in random order each round, so a strategy that people developed is to place 6 very cheap but useful items in the deck, and then 3 really good items that they actually want. This means that they can buy the cheap items more easily and also find the 3 really good ones with more consistency. For instance, this is an item deck I just drafted:

The 3 really good items here are the bottom ones, while the top ones are the cheap filler ones. Depending on your heroes and cards you might not want to do this. For instance, if you have lots of heroes that can trade well and get kills you can go for more expensive item decks. You can also do that if you have multiple Paydays or Iron Fog Goldmines:

Finally, in the right you have consumables, which are health flasks, a potion that draws a card, or town portal scrolls. These are also shown randomly. One thing about this game is that heroes can get "stuck" in lanes that are dead. For instance, if you placed 2 heroes in one lane and won it, but now you want to place them in other lanes so that they can do work there, you either have to find a way to kill them or use a town portal scroll. Because town portal scrolls are randomly spawned in the consumables section of the item shop, you may not get one when you need it. This is another source of randomness that can be played around by always buying portals and never using them needlessly, but it's still a source of randomness that can fuck you over.


Heroes and cards

And finally, the last main source of randomness is in how some heroes or cards work themselves. The best example of this is probably my favorite hero, Ogre Magi, who has this passive ability:

When you get multiple multicasts with this or win because you multicasted the right spell at the right time it feels really good, and when you lose because the enemy did that it feels really bad. But it's still something that can be played around in many ways. To play multicasted spells the enemy has to have mana, and generally if he's playing high impact spells those are costing lots of mana so he has to use one either Aghanim's Sanctum or Satyr Magician to replenish it:

Both of which can be removed from the game. One being an improvement which can be destroyed, and another being a creep which can be killed. And you can also just kill Ogre Magi himself which makes the passive not work.

Another source of randomness that people don't seem to like is Bounty Hunter's Jinada:

Bounty Hunter is a 7/7 hero, if Jinada procs it makes him 11/7, which is enough to kill most heroes in the game in one shot. The way to counter play this is by simply always assuming that it will proc and trying to play around that. Being a 7 health hero makes him kinda squishy too so while he is a big threat it can be dealt with pretty well.

There are also spells that have some randomness to them, like Chain Frost or Eclipse:

With Chain Frost you can choose the starting enemy from where the bouncing will start so you can generally math it out before using it if it's likely to kill everything you need or not. And with Eclipse you can also calculate if it's worth it or not. If you have an enemy you want to kill that has 6 HP, he has two creeps with 4 HP by his side and your Eclipse has 5 charges, it's very unlikely that you won't kill the 6 HP guy. There's a chance the spell might hit both creeps twice and the other one only once, but that's less likely to happen than it hitting either creep only once.

Finally, there are cards that have affect things randomly like:

Lost in Time locks 3 random cards in the opponent's hand for 3 rounds, which is a really good effect. You can't control which cards get locked, but this card is best used when opponents have few cards available to play. Generally when people are doing really well in the game they're playing all their cards every turn and being left with few cards in their hands, meaning that no mana in any lane goes wasted. And some people even go so far as to make "spend mana effectively" one of the primary goals in their way of playing the game, which is a fair way to approach things. Lost in Time though punishes players that are doing that too well, which is a great way disable key plays that the enemy might have on this or the next turns (given that 6+ mana turns are when the game starts getting dangerous).

Golden Ticket is the best item in my opinion, I really like running it whenever I can because it gives you any random item from the pool of items in the game for 9 gold. When this works in your favor it's really really good because you essentially pay 9 gold for a 20+ gold item that has really high impact in the game, and generally you can get this high impact early on, which makes it even more impactful. Obviously people don't like it because it's random, but yea.


Luck

Now, with all that said about sources of randomness in the game, I can get to the meat of the article here. As hopefully I've explained above, the game has various sources of randomness that can be mitigated directly in various ways. For the things that can't be mitigated directly, you can usually set up your board in ways that will mitigate those things indirectly over time. This feels bad and counterintuitive for people in the wrong mindset but it's the way these games go. If the best play in one instance works 70% of the time, you still do it, even if 30% of the time it won't work, because it's the best play all things considered.

Artifact is kinda like Poker in this sense, which falls into the category of high luck and high skill games. There are sources of RNG and you have to know your probabilities and costs to every action to take the statistically correct course of action most of the time. This won't help you win the game 100% of the time, but it will increase your win rate significantly above other people. Sometimes you'll get unlucky, but as long as you can identify the correct plays, over time you should be able to succeed.

With all this in mind, Artifact is the game that is most perfect for me to exercise the mindset that I explained in this article. The mindset is that essentially you filter the world based on a combination of both conscious ideas you have about the world but also what you trained your body to pay attention to over time. In the case of luck/RNG this makes itself very evident in Artifact: people who have trained their bodies to pay attention to the role that luck plays in life, will see the game in terms of luck.

A very simple example of this is people who say that they lost because of some unlucky arrow. This is a clear case of someone who is only paying attention to unlucky events and not how they could have played
things better throughout the game. It is the case that you can miss lethal on a lane because an arrow went the wrong way and lose the game, but it's also the case that you could have spent more resources or other lanes so that you wouldn't lose the game in case you couldn't get lethal this turn, right? But instead of thinking about how they could have played things differently, people will focus on the arrow because they want to blame external factors instead of themselves. Two current examples from Artifact's subreddit:

The number of posts and people complaining about this are endless. And as I've explained, these people are simply wrong. Worse than that, they're in a downward spiral that prevents them from improving. Once you start noticing only how luck affects things you will start noticing it more and more and this will prevent you from thinking about how you could have done things differently, which will make it way harder for you to improve. This goes for Artifact and literally anything in life, which is the point I make in the article about luck.

The solution to this is to slowly train yourself to not blame external factors like luck. Whenever you lose a game, think about how you could have played differently instead of how you got unlucky. Catch yourself if you can whenever you blame luck. This is hard to do and takes actual effort. Even a number of Artifact streamers who are pros at the game still do this and occasionally blame RNG for losing a game. It's a really hard thing to overcome consistently. But it's worth it to train yourself to avoid doing it, since it's a very useful skill in general.

On a side note, this is the main role that I think games play in life. Unlike movies, games are active and because of this you can use them to change the patterns in your body or mind. In this case, learning to never blame luck in Artifact will train your body to never blame luck in general, which is a very good thing to do when you're trying to do anything in life, be it making a game, starting a business, getting a girlfriend, and so on.

In the end this is why I like Artifact, the main skill that it exercises is one of learning how to deal with luck, which is a fundamental skill in life that once mastered provides tremendous gain. On another side note, this is also how some types of games that we like now were created. In the old days simulation games were created to help generals train for battles, and the random elements in those games were derived from statistical analysis of what kinds of things would go wrong in real battlefields. This meant that the better generals got at playing the game, the better they got at dealing with all sorts of random unlucky events that can happen in a real battlefield. These morphed over time and became all sorts of game mechanics that games have today.

Armies lose battles when their generals are unable to adapt to the situation.
— Legion Commander on maintaining poise

Programming lessons learned from making my first game and why I'm writing my own engine in 2018

I just released my first game, BYTEPATH, and I thought it would be useful to write down my thoughts on what I learned from making it. I'll divide these lessons into soft and hard: soft meaning more software engineering related ideas and guidelines, hard meaning more technical programming stuff. And then I'll also talk about why I'm gonna write my own engine.


Soft Lessons

For context, I've been trying to make my own games for about 5-6 years now and I have 3 "serious" projects that I worked on before this one. Two of those projects are dead and failed completely, and the last one I put on hold temporarily to work on this game instead. Here are some gifs of those projects:

The first two projects failed for various reasons, but in the context of programming they ultimately failed (at least from my perspective) because I tried to be too clever too many times and prematurely generalized things way too much. Most of the soft lessons I've learned have to do with this, so it's important to set this context correctly first.


Premature Generalization

By far the most important lesson I took out of this game is that whenever there's behavior that needs to be repeated around to multiple types of entities, it's better to default to copypasting it than to abstracting/generalizing it too early.

This is a very very hard thing to do in practice. As programmers we're sort of wired to see repetition and want to get rid of it as fast as possible, but I've found that that impulse generally creates more problems than it solves. The main problem it creates is that early generalizations are often wrong, and when a generalization is wrong it ossifies the structure of the code around it in a way that is harder to fix and change than if it wasn't there in the first place.

Consider the example of an entity that does ABC. At first you just code ABC directly in the entity because there's no reason to do otherwise. But then comes around another type of entity that does ABD. At this point you look at everything and say "let's take AB out of these two entities and then each one will only handle C and D by itself", which is a logical thing to say, since you abstract out AB and you start reusing it everywhere else. As long as the next entities that come along use AB in the way that it is defined this isn't a problem. So you can have ABE, ABF, and so on...

But eventually (and generally this happens sooner rather than later) there will come an entity that wants AB*, which is almost exactly like AB, but with a small and incompatible difference. Here you can either modify AB to accommodate for AB*, or you can create a new thing entirely that will contain the AB* behavior. If we repeat this exercise multiple times, in the first option we end up with an AB that is very complex and has all sorts of switches and flags for its different behaviors, and if we go for the second option then we're back to square one, since all the slightly different versions of AB will still have tons of repeated code among each other.

At the core of this problem is the fact that each time we add something new or we change how something behaves, we now have to do these things AGAINST the existing structures. To add or change something we have to always "does it go in AB or AB*?", and this question which seems simple, is the source of all issues. This is because we're trying to fit something into an existing structure, rather than just adding it and making it work. I can't overstate how big of a difference it is to just do the thing you wanna do vs. having to get it to play along with the current code.

So the realization I've had is that it's much easier to, at first, default to copypasting code around. In the example above, we have ABC, and to add ABD we just copypaste ABC and remove the C part to do D instead. The same applies to ABE and ABF, and then when we want to add AB*, we just copypaste AB again and change it to do AB* instead. Whenever we add something new in this setup all we have to do is copy code from somewhere that does the thing already and change it, without worrying about how it fits in with the existing code. This turned out to be, in general, a much better way of doing things that lead to less problems, however unintuitive it might sound.


Most advice is bad for solo developers

There's a context mismatch between most programming advice I read on the Internet vs. what I actually have learned is the better thing to do as a solo developer. The reason for this is two fold: firstly, most programmers are working in a team environment with other people, and so the general advice that people give each other has that assumption baked in it; secondly, most software that people are building needs to live for a very long time, but this isn't the case for an indie game. This means that most programming advice is very useless for the domain of solo indie game development and that I can do lots of things that other people can't because of this.

For instance, I can use globals because a lot of the times they're useful and as long as I can keep them straight in my head they don't become a problem (more about this in article #24 of the BYTEPATH tutorial). I can also not comment my code that much because I can keep most of it in my head, since it's not that large of a codebase. I can have build scripts that will only work on my machine, because no one else needs to build the game, which means that the complexity of this step can be greatly diminished and I don't have to use special tools to do the job. I can have functions that are huge and I can have classes that are huge, since I built them from scratch and I know exactly how they work the fact that they're huge monsters isn't really a problem. And I can do all those things because, as it turns out, most of the problems associated with them will only manifest themselves on team environments or on software that needs to live for long.

So one thing I learned from this project is that nothing went terribly wrong when I did all those "wrong" things. In the back of my head I always had the notion that you didn't really need super good code to make indie games, given the fact that so many developers have made great games that had extremely poor code practices in them, like this:

And so this project only served to further verify this notion for me. Note that this doesn't mean that you should go out of your way to write trash code, but that instead, in the context of indie game development, it's probably useful to fight that impulse that most programmers have, that sort of autistic need to make everything right and tidy, because it's an enemy that goes against just getting things done in a timely manner.


ECS

Entity Component Systems are a good real world example that go against everything I just mentioned in the previous two sections. The way indie developers talk about them, if you read most articles, usually starts by saying how inheritance sucks, and how we can use components to build out entities like legos, and how that totally makes reusable behavior a lot easier to reuse and how it makes literally everything about your game easier.

By definition this view that people push forward of ECS is talking about premature generalization, since if you're looking at things like legos and how they can come together to create new things, you're thinking in terms of reusable pieces that should be put together in some useful way. And for all the reasons I mentioned in the premature generalization section I deeply think that this is VERY WRONG!!!!!!!! This very scientific graph I drew explains my position on this appropriately:

As you can see, what I'm defending, "yolo coding", starts out easier and progressively gets harder, because as the complexity of the project increases the yolo techniques start showing their problems. ECS on the other hand has a harder start because you have to start building out reusable components, and that by definition is harder than just building out things that just work. But as time goes on the usefulness of ECS starts showing itself more and eventually it beats out yolo coding. My main notion on this is that in the context of MOST indie games, the point where ECS becomes a better investment is never reached.

As for the context mismatch part of this, if this article gets any traction anywhere, there will surely be some AAA developer in the comments who goes like "I have 2️⃣ 0️⃣ years of 👨‍🏫experience👨‍🏫 in the industry 🎲🎮👾 and what this 👶 kid is saying is 👇😡TRASH!!😡👇 ECS is 💯very💯 useful and I've shipped 🚢☁️🌧 multiple 🥇AAA games that made 🤑 💵 millions of dollars 💸💰 and are played by 🌎billions of people🌏 all over the world!! 🗺 Stop ✋👮 this 🚓 nonsense at once!!🚓😠💢"

And while the AAA developer has a point that ECS was useful for him, it's not necessarily true that it will be useful for me or for other indie developers, since because of the context mismatch, both groups are solving very different problems at some level.

Anyway, I feel like I've made my point here as well as I could. Does this mean I think anyone who uses ECS is stupid and dumb? No 🙂 I think that if you're used to using ECS already and it works for you then it's a no-brainer to keep using it. But I do think that indie developers in general should think more critically about these solutions and what their drawbacks are. I feel like this point made by Jonathan Blow is very relevant (in no way do I think he agrees with my points on ECS or I am saying or implying that he does, though):


Avoiding separating behavior into multiple objects

One of the patterns that I can't seem to break out of is the one of splitting what is a single behavior into multiple objects. In BYTEPATH this manifested itself mostly in how I built the "Console" part of the game, but in Frogfaller (the game I was making before this one) this is more visible in this way:

This object is made up of the main jellyfish body, each jellyfish leg part, and then a logical object that ties everything together and coordinates the body's and the leg's behaviors. This is a very very awkward way of coding this entity because the behavior is split between 3 different types of objects and coordination becomes really hard, but whenever I have to code an entity like this (and there are plenty of multi-body entities in this game), I naturally default to doing it this way.

One of the reasons I naturally default to separating things like this is because each physics object feels like it should be contained in a single object in code, which means that whenever I need to create a new physics object, I also need to create a new object instance. There isn't really a hard rule or limitation that this 100% has to be this way, but it's just something that feels very comfortable to do because of the way I've architectured my physics API.

I actually have thought for a long time on how I could solve this problem but I could never reach a good solution. Just coding everything in the same object feels awkward because you have to do a lot of coordination between different physics objects, but separating the physics objects into proper objects and then coordinating between them with a logical one also feels awkward and wrong. I don't know how other people solve this problem, so any tips are welcome!!


Hard Lessons

The context for these is that I made my game in Lua using LÖVE. I wrote 0 code in C or C++ and everything in Lua. So a lot of these lessons have to do with Lua itself, although most of them are globally applicable.

nil

90% of the bugs in my game that came back from players had to do with access to nil variables. I didn't keep numbers on which types of access were more/less common, but most of the time they have to do with objects dying, another object holding a reference to the object that died and then trying to do something with it. I guess this would fall into a "lifetime" issue.

Solving this problem on each instance it happens is generally very easy, all you have to do is check if the object exists and continue to do the thing:

if self.other_object then
    doThing(self.other_object)
end

However, the problem is that coding this way everywhere I reference another object is way overly defensive, and since Lua is an interpreted language, on uncommon code paths bugs like these will just happen. But I can't think of any other way to solve this problem, and given that it's a huge source of bugs it seems to be worth it to have a strategy for handling this properly.

So what I'm thinking of doing in the future is to never directly reference objects between each other, but to only reference them by their ids instead. In this situation, whenever I want to do something with another object I'll have to first get it by its id, and then do something with it:

local other_object = getObjectByID(self.other_id)
if other_object then
    doThing(other_object)
end

The benefit of this is that it forces me to fetch the object any time I want to do anything with it. Combined with me not doing anything like this ever:

self.other_object = getObjectByID(self.other_id)

It means that I'll never hold a permanent reference to another object in the current one, which means that no mistakes can happen from the other object dying. This doesn't seem like a super desirable solution to me because it adds a ton of overhead any time I want to do something. Languages like MoonScript help a little with this since there you can do this:

if object = getObjectByID(self.other_id) 
    doThing(object)

But since I'm not gonna use MoonScript I guess I'll just have to deal with it 😡


More control over memory allocations

While saying that garbage collection is bad is an inflammatory statement, especially considering that I'll keep using Lua for my next games, I really really dislike some aspects of it. In a language like C whenever you have a leak it's annoying but you can generally tell what's happening with some precision. In a language like Lua, however, the GC feels like a blackbox. You can sort of peek into it to get some ideas of what's going on but it's really not an ideal way of going about things. So whenever you have a leak in Lua it's a way bigger pain to deal with than in C. This is compounded by the fact that I'm using a C++ codebase that I don't own, which is LÖVE's codebase. I don't know how they set up memory allocations on their end so this makes it a lot harder for me to have predictable memory behavior on the Lua end of things.

It's worth noting that I have no problems with Lua's GC in terms of its speed. You can control it to behave under certain constraints (like, don't run for over n ms) very easily so this isn't a problem. It's only a problem if you tell it to not run for more than n ms, but it can't collect as much garbage as you're generating per frame, which is why having the most control over how much memory is being allocated is desirable. There's a very nice article on this here http://bitsquid.blogspot.com.br/2011/08/fixing-memory-issues-in-lua.html and I'll go into more details on this on the engine part of this article.


Timers, Input and Camera

These are 3 areas in which I'm really happy with how my solutions turned out. I wrote 3 libraries for those general tasks:

And all of them have APIs that feel really intuitive to me and that really make my life a lot easier. The one that's by far the most useful is the Timer one, since it let's me to all sorts of things in an easy way, for instance:

timer:after(2, function() self.dead = true end)

This will kill the current object (self) after 2 seconds. The library also lets me tween stuff really easily:

timer:tween(2, self, {alpha = 0}, 'in-out-cubic', function() self.dead = true end)

This will tween the object's alpha attribute to 0 over 2 seconds using the in-out-cubic tween mode, and then kill the object. This can create the effect of something fading out and disappearing. It can also be used to make things blink when they get hit, for instance:

timer:every(0.05, function() self.visible = not self.visible, 10)

This will change self.visible between true and false every 0.05 seconds for a number of 10 times. Which means that this creates a blinking effect for 0.5 seconds. Anyway, as you can see, the uses are limitless and made possible because of the way Lua works with its anonymous functions.

The other libraries have similarly trivial APIs that are very powerful and useful. The camera one is the only one that is a bit too low level and that can be improved substantially in the future. The idea with it was to create something that enabled what can be seen in this video:

But in the end I created a sort of middle layer between having the very basics of a camera module and what can be seen in the video. Because I wanted the library to be used by people using LÖVE, I had to make less assumptions about what kinds of attributes the objects that are being followed and/or approached would have, which means that it's impossible to add some of the features seen in the video. In the future when I make my own engine I can assume anything I want about my game objects, which means that I can implement a proper version of this library that achieves everything that can be seen in that video!

Rooms and Areas

A way to manage objects that really worked out for me is the notion of Rooms and Areas. Rooms are equivalent to a "level" or a "scene", they're where everything happens and you can have multiple of them and switch between them. An Area is a game object manager type of object that can go inside Rooms. These Area objects are also called "spaces" by some people. The way an Area and a Room works is something like this (the real version of those classes would have way more functions, like the Area would have addGameObject, queryGameObjectsInCircle, etc):

Area = Class()

function Area:new()
    self.game_objects = {}
end

function Area:update(dt)
    -- update all game objects
end
Room = Class()

function Room:new()
    self.area = Area()
end

function Room:update(dt)
    self.area:update(dt)
end

The benefits of having this difference between both ideas is that rooms don't necessarily need to have Areas, which means that the way in which objects are managed inside a Room isn't fixed. For one Room I can just decide that I want the objects in it to be handled in some other way and then I'm able to just code that directly instead of trying to bend my Area code to do this new thing instead.

One of the disadvantages of doing this, however, is that it's easy to mix local object management logic with the object management logic of an Area, having a room that has both. This gets really confusing really easily and was a big source of bugs in the development of BYTEPATH. So in the future one thing that I'll try to enforce is that a Room should either use an Area or it's own object management routines, but never both at the same time.

snake_case over camelCase

Right now I use snake_case for variable names and camelCase for function names. In the future I'll change to snake_case everywhere except for class/module names, which will still be in CamelCase. The reason for this is very simple: it's very hard to read long function names in camelCase. The possible confusion between variables and function names if everything is in snake_case is generally not a problem because of the context around the name, so it's all okay 🤗


Engine

The main reason why I'll write my own engine this year after finishing this game has to do with control. LÖVE is a great framework but it's rough around the edges when it comes to releasing a game. Things like Steamworks support, HTTPS support, trying out a different physics engine like Chipmunk, C/C++ library usage, packaging your game up for distribution on Linux, and a bunch of other stuff that I'll mention soon are all harder than they should be.

They're certainly not impossible, but they're possible if I'm willing to go down to the C/C++ level of things and do some work there. I'm a C programmer by default so I have no issue with doing that, but the reason why I started using the framework initially was to just use Lua and not have to worry about that, so it kind of defeats the purpose. And so if I'm going to have to handle things at a lower level no matter what then I'd rather own that part of the codebase by building it myself.

However, I'd like to make a more general point about engines here and for that I have to switch to trashing on Unity instead of LÖVE. There's a game that I really enjoyed playing for a while called Throne of Lies:

It's a mafia clone and it had (probably still has) a very healthy and good community. I found out about it from a streamer I watch and so there were a lot of like-minded people in the game which was very fun. Overall I had a super positive experience with it. So one day I found a postmortem of the game on /r/gamedev by one of the developers of the game. This guy happened to be one of the programmers and he wrote one comment that caught my attention:

So here is a guy who made a game I really had a good time with saying all these horrible things about Unity, how it's all very unstable, and how they chase new features and never finish anything properly, and on and on. I was kinda surprised that someone disliked Unity so much to write this so I decided to soft stalk him a little to see what else he said about Unity:

And then he also said this: 😱

And also this: 😱 😱

And you know, I've never used Unity so I don't know if all he's saying is true, but he has finished a game with it and I don't see why he would lie. His argument on all those posts is pretty much the same too: Unity focuses on adding new features instead of polishing existing ones and Unity has trouble with keeping a number of their existing features stable across versions.

Out of all those posts the most compelling argument that he makes, in my view, which is also an argument that applies to other engines and not just Unity, is that the developers of the engine don't make games themselves with it. One big thing that I've noticed with LÖVE at least, is that a lot of the problems it has would be solved if the developers were actively making indie games with it. Because if they were doing that those problems would be obvious to them and so they'd be super high priority and would be fixed very quickly. xblade724 has found the same is true about Unity. And many other people I know have found this to be true of other engines they use as well.

There are very very few frameworks/engines out there where its developers actively make games with it. The ones I can think off the top of my head are: Unreal, since Epic has tons of super successful games they make with their engine, the latest one being Fortnite; Monogame, since the main developer seems to port games to various platforms using it; and GameMaker, since YoYo Games seems to make their own mobile games with their engine.

Out of all the other engines I know this doesn't hold, which means that all those other engines out there have very obvious problems and hurdles to actually finishing games with it that are unlikely to get fixed at all. Because there's no incentive, right? If some kinds of problems only affect 5% of your users because they only happen at the end of the development cycle of a game, why would you fix them at all unless you're making games with your own engine and having to go through those problems yourself?

And all this means is that if I'm interested in making games in a robust and proven way and not encountering tons of unexpected problems when I get closer to finishing my game, I don't want to use an engine that will make my life harder, and so I don't want to use any engine other than one of those 3 above. For my particular case Unreal doesn't work because I'm mainly interested in 2D games and Unreal is too much for those, Monogame doesn't work because I hate C#, and GameMaker doesn't work because I don't like the idea visual coding or interface-based coding. Which leaves me with the option to make my own. 🙂

So with all that reasoning out of the way, let's get down to the specifics:

C/Lua interfacing and memory

C/Lua binding can happen in 2 fundamental ways (at least from my limited experience with it so far): with full userdata and with light userdata. With full userdata the approach is that whenever Lua code asks for something to be allocated in C, like say, a physics object, you create a reference to that object in Lua and use that instead. In this way you can create a full object with metatables and all sorts of goodies that represents the C object faithfully. One of the problems with this is that it creates a lot of garbage on the Lua side of things, and as I mentioned in an earlier section, I want to avoid memory allocations as much as possible, or at least I want to have full control over when it happens.

So the approach that seems to make the most sense here is to use light userdata. Light userdata is just a a normal C pointer. This means that we can't have much information about the object we're pointing to, but it's the option that provides the most control over things on the Lua side of things. In this setup creating and destroying objects has to be done manually and things don't magically get collected, which is exactly what I need. There's a very nice talk on this entire subject by the guy who made the Stingray Engine here:

You can also see how some of what he's talking about happens in his engine in the documentation.

The point of writing my own engine when it comes to this is that I get full control over how C/Lua binding happens and what the tradeoffs that have to happen will be. If I'm using someone else's Lua engine they've made those choices for me and they might have been choices that I'm not entirely happy with, such as is the case with LÖVE. So this is the main way in which I can gain more control over memory and have more performant and robust games.

External integrations

Things like Steamworks, Twitch, Discord and other sites all have their own APIs that you need to integrate against if you want to do cool things and not owning the C/C++ codebase makes this harder. It's not impossible to do the work necessary to get these integrations working with LÖVE, for instance, but it's more work than I'm willing to do and if I'll have to do it anyway I might as well just do it for my own engine.

If you're using engines like Unity or Unreal which are extremely popular and which already have integrations for most of these services done by other people then this isn't an issue, but if you're using any engine that has a smaller userbase than those two you're probably either having to integrate these things on your own, or you're using someone's half implemented code that barely works, which isn't a good solution.

So again, owning the C/C++ part of the codebase makes these sorts of integrations a lot easier to deal with, since you can just implement what you'll need and it will work for sure.

Other platforms

This is one of the few advantages that I see in engines like Unity or Unreal over writing my own: they support every platform available. I don't know if that support is solid at all, but the fact that they do it is pretty impressive and it's something that it's hard for someone to do alone. While I'm not a super-nerdlord who lives and breathes assembly, I don't think I would have tons of issues with porting my future engine to consoles or other platforms, but it's not something that I can recommend to just anyone because it's likely a lot of busywork.

One of the platforms that I really want to support from the get-go is the web. I've played a game once called Bloons TD5 in the browser and after playing for a while the game asked me to go buy it on Steam for $10. And I did. So I think supporting a version of your game on the browser with less features and then asking people to get it on Steam is a very good strategy that I want to be able to do as well. And from my preliminary investigations into what is needed to make an engine in C, SDL seems to work fine with Emscripten and I can draw something on the screen of a browser, so this seems good.

Replays, trailers

Making the trailer for this game was a very very bad experience. I didn't like it at all. I'm very good at thinking up movies/trailers/stories in my head (for some reason I do it all the time when listening to music) and so I had a very good idea of the trailer I wanted to make for this game. But that was totally not the result I got because I didn't know how to use the tools I needed to use enough (like the video editor) and I didn't have as much control over footage capturing as I wanted to have.

One of the things I'm hoping to do with my engine is to have a replay system and a trailer system built into it. The replay system will enable me to collect gameplay clips more easily because I won't need to use an external program to record gameplay. I think I can also make it so that gameplay is recorded at all times during development, and then I can programmatically go over all replays and look for certain events or sequence of events to use in the trailer. If I manage to do this then the process of getting the footage I want will become a lot easier.

Additionally, once I have this replay system I can also have a trailer system built into the engine that will enable me to piece different parts of different replays together. I don't see any technical reason on why this shouldn't work so it really is just a matter of building it.

And the reason why I need to write an engine is that to build a replay system like I 100% need to work at the byte level, since much of making replays work in a manageable way is making them take less space. I already built a replay system in Lua in this article #8, but a mere 10 seconds of recording resulted in a 10MB file. There are more optimizations available that I could have done but at the end of the day Lua has its limits and its much easier to optimize stuff like this in C.

Design coherence

Finally, the last reason why I want to build my own engine is design coherence. One of the things that I love/hate about LÖVE, Lua, and I'd also say that of the Linux philosophy as well is how decentralized it is. With Lua and LÖVE there are no real standard way of doing things, people just do them in the way that they see fit, and if you want to build a library that other people want to use you can't assume much. All the libraries I built for LÖVE (you can see this in my repository) follow this idea because otherwise no one would use them.

The benefits of this decentralization is that I can easily take someone's library, use it in my game, change it to suit my needs and generally it will work out. The drawbacks of this decentralization is that the amount of time that each library can save me is lower compared to if things were more centralized around some set of standards. I already mentioned on example of this with my camera library in a previous section. This goes against just getting things done in a timely manner.

So one of the things that I'm really looking forward to with my engine is just being able to centralize everything exactly around how I want it to be and being able to make lots of assumptions about things, which will be refreshing change of pace (and hopefully a big boost in productivity)!


Anyway, I rambled a lot in this article but I hope it was useful. Also, buy my game (click below to buy 🙂)

Luck Isn't Real

Top-down & bottom-up

I see that there are two ways you can change yourself over time to become more effective at life in general.

The top-down manner is when you become consciously aware of something and over time you train your body to accept that idea. If you realize that you need to run every morning to lose some weight, you need to spend a lot of conscious effort in the first few months of doing it so that you actually go run every morning. This conscious effort is necessary to build the habit in your body so that subsequently running every morning isn't that much of a problem.

The bottom-up manner is when you let the habit take hold and run its course. As you start running every morning, the habit builds up and you need to spend less and less conscious effort to actually run daily. The fact that you spend less conscious effort on this saves you this effort to be used elsewhere on your day. And as you start running more, your body starts changing and getting more healthy, and this has positive effects on your rational mind as well.

The main argument I'll make in this article is that these two processes happen all the time for EVERYTHING in your life, and the earlier you become aware of it, the more successfully you'll be able to change yourself to suit the environment.


Objective & pragmatic

There are two main ways of viewing the world in regards to truth: through the lens of objective truth and through the lens of pragmatic truth.

The lens of objective truth states that reality is what it is and we can extract objective truths from it that will serve us well. The lens of pragmatic truth pertains to the notion that things are only true enough for us to survive and get along in the world well.

For instance, the idea that porcupines can shoot their quills like projectiles is objectively false, but is pragmatically true. If people believe that lie, they're less likely to be near porcupines, which makes them less likely to get hurt by them.

Another main argument I'll make in this article is that sometimes it is useful to believe things that aren't objectively true because they lead to better results.


Luck Isn't Real

Saying "luck isn't real" is one of those things that is objectively false but pragmatically true. Note that I'm not playing games with the definition of real (I'm not saying that abstract concepts aren't real) and I'm not talking about the determinism or indeterminism of the universe.

Believing that "luck isn't real" is simply a top-down idea that, once you train your body to accept, becomes extremely useful in removing unhelpful thought patterns from yourself, and helps you become a better and more successful person in general.

The best way I can show how this happens is with tons of examples. I'll start with some luck narratives in indie game development:


Lottery

The notion of indie game development being a lottery is a well spread one. Articles like this one sound extremely reasonable and rational, and it's hard to disagree with them.

And while the article may not be objectively wrong, it is pragmatically wrong. The idea outlined is that essentially making games is playing the lottery:

Flip a coin 20 times. On every 1 out of 2 times should be heads. But you don’t get a pattern of alternating heads and tails. You get streaks. You may see 10 heads in a row. This is within the bounds of chance. However, if you really needed tails to come up, you are in a lot of trouble.

Random systems have natural variability and game development does as well. The best team in the world can strike out 10 times in a row. It is just as likely for your failures to be front loaded as it is for your success. So not only do you need your success to pay for the average rate of failure. You need it to pay for the worst possible luck.

The problem here is that the top-down idea is that luck exists. Your body isn't nuanced enough to understand "luck is real but if we mitigate our risks through various projects we'll be able to do it". Your body is dumb. The message will be cut off at "luck is real", and then that's what you'll train your body to understand and that's what you'll see in the world.

And seeing "luck is real" in the world is harmful, because it's a message that takes power away from you and puts power in "luck". In general, you want to cultivate ideas in your brain and your body that give you power, not that take power away from you. This author is doing a good job at approaching this from a risk minimization perspective, which gives him power over luck, but ultimately he still accepts that luck plays a massive role in it and that's not good enough in my opinion.


Indiepocalypse

The same notion applies to "indiepocalypse". Articles like this one blame the game's failure for the state of the indie market.

And while it is true that as time goes on the market will become more and more competitive, like the example before this one, it's about the mindset you take. If you believe that it's about the market, then that's what you'll train your body to see in the world and that's all you'll see: the market. This takes power away from you and places it elsewhere.

And it's also important to realize how subtle these thought patterns become and how they infect every inch of your life without you realizing it. This article is written by a very highly respected and successful indie developer, and most of the article is pretty much true and good advice, but at the end he says this:

Treat this as a disclaimer for my blog: You are reading the thoughts of a guy who was coding since age 11, has 36 years coding experience, has shipped over a dozen games, several of which made millions of dollars, got into indie dev VERY early, knows a lot of industry people, and has a relatively high public profile. And still almost NOBODY covered my latest game (in terms of gaming websites). Its extremely, extremely tough right now.

There are many reasons why his last game wasn't covered by gaming websites, and most of them probably don't have to do with the state of the market.

The writer of this article does the same. He has lots of good insights into why his game failed, but in multiple places in the article he goes on to blame the market instead.


Art criticism

Often times I'll see tweets like this one where an artist doesn't respond well to criticism:

Or ideas like this one that promote the notion that you, the reader, are the problem:

And again, these are ideas that take power away from the artist and place power on the reader. The artist is saying: I'm powerless against your words, so you must change. But this is evidently a losing proposition, since the artist is asking thousands of people to change, instead of changing himself to learn how to take criticism properly.

In general, you want to cultivate ideas that give you power. The artist, in this case, needs to learn how to deal with criticism in one way or another or he won't be able to post his work publicly on the Internet without it being an issue. It's just not a good long term solution to have to rely on the goodwill of other thousands of people who you've never met to behave like you expect them to behave.

This edit, for instance, is a much better take on the issue:


brum's tweet

Finally, one last gamedev related example. Someone who frequents the same indie gamedev related Discord server as me posted this tweet a few days ago:

It got retweeted by a few people but most noticeably someone who had a lot of followers. brum then went onto the server and talked about how the retweet didn't get him that many more additional likes, even though the person had tons of followers. He went on to say that that probably happened because the person who retweeted him was either inactive or had bought fake followers.

This is another subtle one, but brum, instead of focusing on the thought that maybe his work should have been of higher quality, immediately jumped to blame an external factor instead of an internal one. He immediately focused on something took power away from him and gave power to something else.


External Factors

The generalization of this idea that luck isn't real is that luck is just a single instance of a more general class called "external factor". And so the more general formulation of "luck isn't real" would be something like "you shouldn't blame external factors for your failures". Another way of phrasing this, which I like more and seems easier to remember (and therefore to train your body to remember) is that you should focus on ideas that empower you.

Empowerment then becomes the key focus of the way to filter the world. Ideas that empower you should be accepted, ideas that remove power away from you should be rejected. I got this notion from this article a few years ago and since then, after integrating this idea into me properly it has been extremely useful. But again, it's better to show what I mean with examples (not directly related to gamedev now):


White Privilege

The idea of white privilege, fundamentally, in its name, takes power away from non-white people and places it in white people. It is an idea that weakens non-white people by saying that they are unable to fix their problems, and that all their problems rely on the goodwill of white people fixing them.

Like all the ideas I mentioned previously, it's one that once your train your body to accept, becomes the way through which you view the world. So once you truly accept the idea of white privilege, the top-down & bottom-up process takes place and you'll start seeing it everywhere:

Feminism

Feminism falls under the same category, in that it takes power away from women and places it in men. Like all the other ideas, it is fundamentally flawed because it weakens women. And like the other ideas, once you train your body to accept it, you will see it everywhere:

Incels

Incels (stands for involuntary celibate = virgins who are frustrated about being virgins) are a group that does the same thing, instead they take power away from themselves and place it in women. The typical incel mindset is that women have a much easier time having sex, that women only want to date "chads" (good looking alpha types of men), and that women are basically immoral beings who don't care about anyone other than themselves.

Incels place themselves as victims of terrible women, instead of looking inwards and focusing on becoming better people who women would want to be around. Similarly to the previous two examples, once you train your body to accept this idea you start seeing it everywhere:

White Supremacy

White supremacy also does this, taking power away from white men and placing it in immigrants or jews. Like the previous ideas, it weakens white people who believe in it because it takes power away from them. And like the others, once you accept it into your body you start seeing it everywhere.

The best example of this is in the idea that jews are overrepresented in positions of power in America, and therefore this must be due to some kind of conspiracy, when it is easily explained by other factors.


I made sure to pick examples on "both sides" of the political spectrum to make it clear that this is something that everyone does, and it should be obvious how there's a pattern here.

The pattern follows the same logic as the pattern for the indiedev examples: blaming external factors, focusing on things that take power away from you and give it to something else. This could also be summarized as "playing the victim".


Luck Isn't Real

After all these examples it's easy to see why I would say that "luck isn't real": like all the mindsets above, "luck isn't real" is an idea that once you accept and integrate in your body, you start seeing everywhere. I can see the same pattern of behavior across all these different examples because I've trained my body to filter the world through this lens.

The difference is that "luck isn't real" is a mindset that gives me power. It does not, in any way, take power away from me and give it to something else. It's an idea that fundamentally states that everything is under my control (even though it isn't), and that as long as I do a good enough job and work hard enough, things will work out. This may seem naive, idealistic and unrefined, but when it comes to the messages you send to your body, they have to be this simple, otherwise the message gets lost and becomes something else.

It is of extreme importance to cultivate ideas that give you power and to reject ideas that take power away from you. Once ideas get integrated into your body it is extremely hard to get rid of them because they're self reinforcing. Once your body learns them, your conscious mind will start filtering the world through that lens and it's what makes the top-down -> bottom-up -> top-down process work. You need to use this process to your benefit by cultivating ideas that will make you stronger.


Conclusion

Hopefully this article was useful. Cultivate ideas that give you power and make you stronger and you will become a better indie developer and a better person in general. Here's a good video to end the article on that has a correct mindset when it comes to luck:

Repeated crashes

Crashed in two back-to-back runs, both with the same error message that ExplodeParticles table index is 0.

I haven't specced any explosion-related passives or classes, and I wasn't using the Explode attack during the crashes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.