-
Notifications
You must be signed in to change notification settings - Fork 142
BYTEPATH #2 - Libraries #16
Comments
Just a small correction, where you talk about using |
@nickdonnelly @Sn0wCrack Yea, it's |
Hi, just a small thing, in the ex 21, when you say "Using only the tween function, tween the |
EDIT: Just saw that this is a proposed exercise. Spoilers ahead! I will leave the comment here since it got me a bit confused. Hey, You wrote:
But then in requireFiles() you expect the class to be declared without the local attribute, since you do just This should be a better implementation of the function requireFiles(files)
for _, filepath in ipairs(files) do
local filepath = filepath:sub(1, -5)
local parts = filepath:split("/")
local class = parts[#parts]
_G[class] = require(filepath)
end
end This will work nicely with the With the proposed change this Class declaration will get imported nicely: local Object = require 'libraries/classic/classic'
local Circle = Object:extend()
function Circle:new(x, y, radius)
self.x = x or 0
self.y = y or 0
self.radius = radius or 50
end
return Circle and are able to do: local object_files = {}
recursiveEnumerate('objects', object_files)
requireFiles(object_files)
local circle = Circle(10, 10, 10) |
Exercise 32 says the expected result of summing all the values in table |
@harrisi Thank you! I fixed it. |
No, thank you, @SSYGEN. I've been enjoying these posts quite a bit - about to start #18 (number 4 in the series). I was wanting to also ask if you had a preferred method of bringing up some small corrections/suggestions like this. Is commenting on the issues okay, since I can't make PRs for these? |
Yea, it's okay to do it like this. |
Edit: Wait, it was an exercise I just noticed. Is my answer good enough. Or I screw up? I think the require order is important, imagine you have a Circle class that extends from a Shape class. With that function Circle will be required before Shape and love will throw an error.
Maybe a workaround that also helps to keep things neat and tidy. If a class extends from another, put the subclass in a folder with the name of the parent class: Directory structure: SSYGEN
├── lib
│ └── classic
│ └── classic.lua
├── objects
│ ├── Shape
│ │ └── Circle.lua
│ └── Shape.lua
├── conf.lua
└── main.lua And a little tweak to the script: 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
--- Start of edit
-- insert at the beginning
-- also called unshift in other languages
-- I think... :v
table.insert(file_list, 1, file)
-- end of edit
elseif love.filesystem.isDirectory(file) then
recursiveEnumerate(file, file_list)
end
end
end -- recursiveEnumerate
|
What does this mean? A delay?
|
@hikkithegene as far as your question about |
I like to use dots instead of slashes with |
I'm probably missing something really simple, but I can't seem to get "self" to work with the Circle class declaration. I keep getting this error:
I'm on Löve 11.2, and my code is this: local Object = require 'libraries/classic'
local Circle = Object:extend()
function Circle:new(x, y, radius)
self.x = x or 0
self.y = y or 0
self.r = radius or 50
self.time = love.timer.getTime()
end
function Circle:update(dt)
self:draw()
end
function Circle:draw()
love.graphics.circle('fill', self.x, self.y, self.r) --this is line 18
end
return Circle |
how did you create that object? may i see the snippet? |
Object = require 'libraries/classic'
Circle = require 'objects/Test'
function love.load()
Circle_instance = Circle(400, 300, 50)
end
function love.update(dt)
end
function love.draw()
Circle_instance:draw()
end Edit: I've just changed the update to call the Circle's update, and removed the call from draw, but that failed to give a different result (though is probably better code). |
@Iralie is you shouldnt call draw in update, because update wont draw stuff. Is there new error? |
No, it's still crashing on Line 18 Self is nul. |
Ah, and the answer to that is: yes, it is. Sorry, thought I'd answered already. |
When I have this
in my code, including the timer:update(dt) and the draw calls of course, it's saying timer.action() is a nil value at Timer.lua:42 and this is using the chrono library you created. any reason why this is happening? It starts tweening but when it reaches the end it gives me this error had no problem with humpy |
I can not reproduce your error, I downloaded plain chrono lib and have the following main file that does exactly what it should do: Timer = require 'chrono-master/Timer'
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}
timer:tween(1, rect_1, {w = 0}, 'in-out-cubic')
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 Anyway I would assume your error results due to the difference in the prototypes as that action refers to the function that should be called after the tween do you still get your error if u append the empty function as another argument. Anyway maybe you mixed up EnhancedTimer and Chrono as the have different prototypes it is quiet easy to mess up (happened to me as well in the past). If you want or need further help let me know but I need a full main file and or the structure of the files you are using cause the rest would just be guessing ;), Cheers |
I dunno if this is a bit too late, but I found out that a key issue is that the love.graphics.circle wants the first argument to be a string e.g. "fill",x,y,r. I stopped getting errors once I inserted this into my code. |
yet this only works if and only if your class name and your file class name are the same, otherwise it gives an error saying you attempted to access a nil value. |
my dumb solution to the recursiveEnumerate require problem:
removes file from files if requiring it doesn't throw an error. If it gets an error, it keeps the file and sets package.loaded to false for that file, so that require() will do it again. Does it over and over until files is empty. Thanks for making this tutorial :) |
"pressRepeat" is also mentioned in answer of Exercise 16. |
Minor update: To accomplish the same thing listed in this post we need to just do the following:
|
The Input library needs an update for 'mouse1' to work: |
If thought that I could replace a call like this:
with
Now when I transfer this syntactic sugar to my inheritance exercise... This one works:
But this one won't
Any clues? |
a327ex/boipushy@6cc02f1 |
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 alibraries
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: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 aTest
class and instantiating it once would look like this:So when
require 'objects/Test'
is called inmain.lua
, everything that is defined in theTest.lua
file happens, which means that theTest
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:By defining the
Test
variable as local inTest.lua
it won't be bound to a global variable, which means you can bind it to whatever name you want when requiring it inmain.lua
. At the end of theTest.lua
script the local variable is returned, and so inmain.lua
whenTest = require 'objects/Test'
is declared, theTest
class definition is being assigned to the global variableTest
.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 namedObject
asClass
instead, and then your class definitions would look likeTest = 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: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..
) thefolder
string and theitem
string.Let's say that the folder string is
'objects'
and that inside theobjects
folder there is a single file namedGameObject.lua
. And so theitems
list will look likeitems = {'GameObject.lua'}
. When that list is iterated over, thelocal file = folder .. '/' .. item
line will parse tolocal 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
andlove.filesystem.isDirectory
functions. If it is a file then simply add it to thefile_list
table that was passed in from the caller, otherwise callrecursiveEnumerate
again, but now using this path as thefolder
variable. When this finishes running, thefile_list
table will be full of strings corresponding to the paths of all files insidefolder
. In our case, theobject_files
variable will be a table full of strings corresponding to all the classes in theobjects
folder.There's still a step left, which is to take all those paths and require them:
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 therequire
function spits out an error if it's left in. The line that does that islocal file = file:sub(1, -5)
and it uses one of Lua's builtin string functions. So after this is done all classes defined inside theobjects
folder can be automatically loaded. TherecursiveEnumerate
function will also be used later to automatically load other resources like images, sounds and shaders.OOP Exercises
6. Create a
Circle
class that receivesx
,y
andradius
arguments in its constructor, hasx
,y
,radius
andcreation_time
attributes and hasupdate
anddraw
methods. Thex
,y
andradius
attributes should be initialized to the values passed in from the constructor and thecreation_time
attribute should be initialized to the relative time the instance was created (see love.timer). Theupdate
method should receive adt
argument and the draw function should draw a white filled circle centered atx, y
withradius
radius (see love.graphics). An instance of thisCircle
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 theCircle
class. AnHyperCircle
is just like aCircle
, except it also has an outer ring drawn around it. It should receive additional argumentsline_width
andouter_radius
in its constructor. An instance of thisHyperCircle
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:
What is the value of
counter_table.value
? Why does theincrement
function receive an argument namedself
? Could this argument be named something else? And what is the variable thatself
represents in this example?10. Create a function that returns a table that contains the attributes
a
,b
,c
andsum
.a
,b
andc
should be initiated to 1, 2 and 3 respectively, andsum
should be a function that addsa
,b
andc
together. The final result of the sum should be stored in thec
attribute of the table (meaning, after you do everything, the table should have an attributec
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:
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 thatParentClass
is required beforeSomeClass
? 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:
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:
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 alevel
object which has inside aplayer
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 callgame:keypressed
insidelove.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: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 theinput:pressed
orinput:released
calls would have returned false and so whatever is inside of the conditional wouldn't be run. The same applies to theinput: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:So in this example, once the key bound to the
test
action is held down, every 0.5 secondstest event
will be printed to the console.Input Exercises
15. Suppose we have the following code:
Will anything happen when
mouse1
is pressed? What about when it is released? And held down?16. Bind the keypad
+
key to an action namedadd
, then increment the value of a variable namedsum
(which starts at 0) by 1 every0.25
seconds when theadd
action key is held down. Print the value ofsum
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
anddown
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.
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 globaltimer
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
andtween
. And while I personally don't use thescript
function, some people might find it useful so it's worth a mention. So let's go through them: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 withafter
is that you can chain multiple of those together, so for instance: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.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: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 theevery
call was made.Another way you can get the behavior of the
every
function is through theafter
function, like so: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 secondafter
call inside the first: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 thetween
function: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 tablecircle
has a keyradius
in it with the initial value of 24. Over the span of 6 seconds this value will changed to 96 using thein-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:And that looks like this:
These 3 functions -
after
,every
andtween
- 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: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 thehandle_1
variable. Then we cancel that call by callingcancel
withhandle_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 keyr
we want to print a random number to the console after 2 seconds:If you add the code above to the
main.lua
file and run the project, after you pressr
a random number should appear on the screen with a delay. If you pressr
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 pressr
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: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 ther
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
fromlibraries/hump/timer
to wherever you end up placing theEnhancedTimer.lua
file, I personally placed it inlibraries/enhanced_timer/EnhancedTimer
. This also assumes that thehump
library was placed inside thelibraries
folder. If you named your folders something different you must change the path at the top of theEnhancedTimer
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 theafter
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:
Using only the
tween
function, tween thew
attribute of the first rectangle over 1 second using thein-out-cubic
tween mode. After that is done, tween theh
attribute of the second rectangle over 1 second using thein-out-cubic
tween mode. After that is done, tween both rectangles back to their original attributes over 2 seconds using thein-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 thes
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:
Using only the
tween
function and without placing thea
variable inside another table, how would you tween its value to 20 over 1 second using thelinear
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:
Table Exercises
For all exercises assume you have the following tables defined:
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 theeach
function.29. Count the number of 1 values inside the
b
table.30. Add 1 to all the values of the
d
table using themap
function.31. Using the
map
function, apply the following transformations to thea
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:
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
andd
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
andd
tables that doesn't have any duplicates.42. Find the common values between
b
andd
tables.43. Append the
b
table to thed
table.BYTEPATH on Steam
Tutorial files
The text was updated successfully, but these errors were encountered: