New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BYTEPATH #2 - Libraries #12

Closed
adnzzzzZ opened this Issue Apr 22, 2017 · 16 comments

Comments

Projects
None yet
@adnzzzzZ
Copy link
Owner

adnzzzzZ commented Apr 22, 2017

@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.

@egordorichev

This comment has been minimized.

Copy link

egordorichev commented Apr 24, 2017

Thanks for your tutorials!

@marcJV

This comment has been minimized.

Copy link

marcJV commented Apr 26, 2017

Watch out for those pesky .DS_Store files on mac.

To get around this I ignored any file that is hidden (starting with a period). If anyone has a better way please let me know.

function recursiveEnumerate(folder, file_list)
    local items = love.filesystem.getDirectoryItems(folder)
    for _, item in ipairs(items) do
        local file = folder .. '/' .. item

        if item:sub(0,1) ~= '.' then --Added this if statement
          if love.filesystem.isFile(file) then
              table.insert(file_list, file)
            elseif love.filesystem.isDirectory(file) then
              recursiveEnumerate(file, file_list)
          end
      end
    end
end
@karai17

This comment has been minimized.

Copy link

karai17 commented May 2, 2017

Your requires are wrong. You should be using dot notation, not slashes. If you don't use dots, then Lua doesn't properly cache required files, amongst other issues this may cause.

Object = require "libraries.classic.classic"
@synthkaf

This comment has been minimized.

Copy link

synthkaf commented May 4, 2017

@karai17 could you tell us more about the other issues that might arise?

I hadn't heard of this before. Would it have something to do with system path separators?

@karai17

This comment has been minimized.

Copy link

karai17 commented May 4, 2017

Well, the main concern here is that the require path is not a system path. You can put a system path in there, but it is not guaranteed to work across all platforms. When you require a file, the file is cached and placed in _G.packages.loaded and other such luastate things. If you use a malformed path (such as with slashes), none of that good stuff happens.

@oniietzschan

This comment has been minimized.

Copy link

oniietzschan commented May 8, 2017

First of all, nice tutorial series! I think I'm a little beyond the target audience, but I'm still learning some nice things here and there.

Anyways, I was hoping you could answer OOP Exercises 8, the exercise regarding ensuring that ParentClass is defined before you load ChildClass. I'm wondering which approach you use here, because I haven't found anything I really like myself. Do you just ensure that the parent is loaded first purely through the filesystem, ie. loading a "BaseClasses" directory first, or similar? Or is there a more robust way of accomplishing this which you can recommend, something like auto-loading or lazily-loading classes? Please let me know when you have some time, thanks!

@adnzzzzZ

This comment has been minimized.

Copy link
Owner Author

adnzzzzZ commented May 8, 2017

@oniietzschan The way I solve this is by just loading the base classes manually. This is not a good general solution if you use a lot of inheritance, but in this game the only case of inheritance is having all objects that go inside an Area inherit from the base GameObject. This means that all I do is load the GameObject file manually first and then call the auto-loading code for the rest.

I haven't really thought much about a general case solution so I can't help you with any better way.

@sharkusk

This comment has been minimized.

Copy link

sharkusk commented May 26, 2017

Great tutorials. I'm having lots of fun with them...

Any reason we shouldn't use the built-in (non-callback) input functions : love.keyboard.isDown and love.mouse.isDown?

@frarf

This comment has been minimized.

Copy link

frarf commented May 26, 2017

Before these tutorials, I didn't really use Love2D for anything large, mostly just small games and tests, but here's my answer.
Essentially, the default love callbacks are very barebones, leaving a whole lot for you to solve. In other words, they're the bare minimum.
For example, to get the left and right keys of the keyboard and translate them into an input number (like -1, 0, 1), we'd have to do:

  local keyDirX = 0
  keyDirX = keyDirX + (love.keyboard.isDown("right") and 1 or 0)
  keyDirX = keyDirX - (love.keyboard.isDown("left" and 1 or 0))

Then there's the whole issue with dealing with different input methods, so if we want both the keyboard and a controller, we'd have to make a new method, and maybe if we want mouse support, we'd have to too, and so on and so forth.
Not to mention custom input methods, which adds another layer.

It's a lot cleaner to just abstract it all by having things like action maps, that do a thing when an input is pressed, where the input can be anything, and multiple things, and the callback is similar.
In a nutshell, just a bit more neat.

@athros

This comment has been minimized.

Copy link

athros commented May 26, 2017

for @frarf and @sharkusk - here's a very good article detailing input handling in Löve2d and Lua:
http://lua.space/gamedev/handling-input-in-lua - especially when dealing with more than a couple of states in the game.

@frarf

This comment has been minimized.

Copy link

frarf commented May 26, 2017

Great article, really shows how expressive you can get in Lua. Thanks! Was a nice read.

@Conquistor

This comment has been minimized.

Copy link

Conquistor commented Jun 21, 2017

For some reason I cant name a class's
lua file 'Circle'. I tried to make a class inherit traits from another but when I changed the names and made it match the other class it inherited it came back as an error. I have a fix for it but can some tell me why it is a problem?
Circle.txt

@frarf

This comment has been minimized.

Copy link

frarf commented Jun 23, 2017

Circle is required before Shape. This is because most OSes sort by alphabetical order. One way to solve this is by requiring Shape manually in main.lua, which fixes the inheritance issue.

@plouricio

This comment has been minimized.

Copy link

plouricio commented Dec 1, 2017

im a totally noob in programming, and maybe this is a dumb question. How do i insert a object into a table?
i mean, i created a bullet class, and everytime that i press a key, i want to insert a new bullet into a table called bullets.

@oniietzschan

This comment has been minimized.

Copy link

oniietzschan commented Dec 1, 2017

This should work:

function insertObjIntoTable(tbl, obj)
  local newTable = {}
  for k, v in pairs(tbl) do
    newTable[k] = v
  end
  table.insert(newTable, obj)
  return newTable
end

and then you can simply call it like this:

local bullet = Bullet()
allBullets = insertObjIntoTable(allBullets, bullet)
@oniietzschan

This comment has been minimized.

Copy link

oniietzschan commented Dec 1, 2017

wait, i've made a huge oversight. actually you can just use:

table.insert(allBullets, bullet)

@adnzzzzZ adnzzzzZ closed this Feb 10, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment