main.lua
with the following contents: function love.load() end function love.update(dt) end function love.draw() end
love.load
once the program is started, and love.update
and love.draw
are executed in each frame. That is, for example, if you want to upload an image and draw it, then write 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 into the image
variable, and then in each frame it is drawn at position 0, 0. To see that love.draw
actually draws the image in each frame, try this: love.graphics.draw(image, love.math.random(0, 800), love.math.random(0, 600))
800x600
, that is, this function will very quickly randomly draw an image on the screen:love.run
. 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 -- , dt , love.load. if love.timer then love.timer.step() end local dt = 0 -- . while true do -- . 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 -- dt, update if love.timer then love.timer.step() dt = love.timer.getDelta() end -- update draw if love.update then love.update(dt) end -- 0, love.timer 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
love.run
, love.run
is love.run
, and then everything else starts from here. The function is well commented out, and the purpose of each function can be found in the LÖVE wiki. But we will go through the basics: if love.math then love.math.setRandomSeed(os.time()) end
love.math
for nil inequality. All values in Lua are true, except false and nil, so if love.math
condition will be true if love.math
defined. In the case of LÖVE, these variables are set in the conf.lua
file. You do not need to worry about this file yet, but I mentioned it because it is possible to enable and disable certain systems, such as love.math
, in it, so before working with its functions, you need to make sure that it is enabled in this file.if random_variable
condition, then it will be false if the variable has not been defined previously, for example random_variable = 1
.love.math
module love.math
enabled (and by default it is), then its initial number (seed) is set based on the current time. See love.math.setRandomSeed
and os.time
. After that, the love.load
function is love.load
: if love.load then love.load(arg) end
arg
is the command line argument passed to the executable LÖVE file when it runs the project. As you can see, love.load
is executed only once because it is called only once, and the update and draw functions are called in a loop (and each iteration of this loop corresponds to a frame). -- , dt , love.load. if love.timer then love.timer.step() end local dt = 0
love.load
and performing the function of all our work, we check that love.timer
set and call love.timer.step
, which measures the time spent between the last two frames. As written in the comment, processing love.load
can take a long time (because it can contain all sorts of things, such as images and sounds), and this time should not be the first value returned by love.timer.getDelta
in the first frame of the game.dt
is initialized to 0. Variables in Lua are global by default, so by assigning local dt
we assign the current block only a local scope, that is, we restrict it to the love.run
function. Read more about the blocks here . -- . while true do -- . 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
love.event.pump
passes events to the event queue and according to its description, these events are somehow generated by the user. These can be keystrokes, mouse clicks, window resizing, window focus changes and the like. The loop using love.event.poll
goes through the event queue and processes each event. love.handlers
is a table of functions that calls the appropriate event handling mechanisms. For example, love.handlers.quit
will call the function love.quit
, if it exists.main.lua
file that will be called when the event main.lua
. A full list of event handlers is available here . I’ll no longer look at event handlers in detail, but will briefly explain how everything happens. Arguments a, b, c, d, e, f
passed to love.handlers[name]
are all possible arguments that the corresponding functions can use. For example, love.keypressed
gets the key pressed as an argument, its scancode and information about whether a keystroke event repeats. That is, in the case of love.keypressed
values of a, b, c
will be defined, and d, e, f
will be nil. -- dt, update if love.timer then love.timer.step() dt = love.timer.getDelta() end -- update draw if love.update then love.update(dt) end -- 0, love.timer
love.timer.step
measures the time between the last two frames and changes the value returned by love.timer.getDelta
. That is, in this case dt
will contain the time it took to complete the last frame. This is useful because then this value is passed to the love.update
function, and from this point on it can be used by the game to provide constant speeds regardless of the change in the frame rate. 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
love.update
is love.draw
. But before that, we make sure that the love.graphics
module exists and check with love.graphics.isActive
that we can draw on the screen. The screen is cleared, filled with the specified background color (initially black) using love.graphics.clear
, using love.graphics.origin
transformations are reset, love.draw
is love.draw
, and then love.graphics.present
used to transfer everything drawn in love.draw
on the screen. And finally: if love.timer then love.timer.sleep(0.001) end
love.timer.sleep
should be here, at the end of the file, but the explanation from the LÖVE developer seems quite logical.love.run
function ends. Everything that happens inside the while true
refers to the frame, that is, love.update
and love.draw
are called once per frame. The whole game is essentially a very fast repetition of the contents of the loop (for example, at 60 frames per second), so get used to this thought. I remember that at first it took me some time to instinctively realize why everything is so arranged.love.window.setMode
with the vsync
attribute vsync
to false.Fixed Delta Time
cycle from the Fix Your Timestep article by changing love.run
.Variable Delta Time
cycle from Fix Your Timestep by changing love.run
.Semi-Fixed Timestep
from the article Fix Your Timestep, changing love.run
.Free the Physics
cycle from Fix Your Timestep by changing love.run
.classic
folder into the project folder. I usually create a libraries
folder and drop all libraries there.main.lua
file by doing the following: Object = require 'libraries/classic/classic'
objects
folder. Then, for example, creating a Test
class and one of its instances will look like this: -- objects/Test.lua Test = Object:extend() function Test:new() end function Test:update(dt) end function Test:draw() end
-- main.lua Object = require 'libraries/classic/classic' require 'objects/Test' function love.load() test_instance = Test() end
require 'objects/Test'
in main.lua
, everything that is defined in the Test.lua
file is Test.lua
, which means that the global variable Test
now contains the definition of the class Test. In our game, each class definition will be performed in this way, that is, class names must be unique, as they are bound to a global variable. If you do not want to do this, you can make the following changes: -- objects/Test.lua local Test = Object:extend() ... return Test
-- main.lua Test = require 'objects/Test'
Test
variable local in Test.lua
, then it will not be bound to a global variable, that is, it will be possible to bind it to any name when it is needed in main.lua
. At the end of the Test.lua
script, a local variable is returned, and therefore in main.lua
when declaring Test = require 'objects/Test'
definition of the class Test
assigned to the global variable Test
.Object
variable. One of the good consequences of this is that if you assign a library to a variable, if we want, we can give Object
name Class
, and then our class definitions will look like Test = Class:extend()
.require 'objects/ClassName'
. The problem here is that there may be a multitude of classes, and entering this string for each class can be tedious. So to automate this process, you can do something like this: 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
recursiveEnumerate
function recursively lists all the files within a given folder and adds them to the table as rows. It uses the LÖVE filesystem module , which contains many useful functions for performing such operations.love.filesystem.getDirectoryItems
as a string table. Then it iterates through all of them and gets the full path to the file by concatenation (the concatenation of the lines in Lua is performed using ..
) the folder
string and the item
string.'objects'
, and inside the objects
folder there is a single file called GameObject.lua
. Then the items
list will look like items = {'GameObject.lua'}
. When iterating through the list, the string local file = folder .. '/' .. item
sparsya in local file = 'objects/GameObject.lua'
, that is, in the full path to the corresponding file.love.filesystem.isFile
and love.filesystem.isDirectory
functions, whether it is a file or a directory. If this is a file, then we simply add it to the file_list
table passed by the called function, otherwise we call recursiveEnumerate
again, but this time we use this path as the folder
variable. When this process is completed, the file_list
table will be filled with lines corresponding to the paths to all files within the folder
. In our case, the object_files
variable will be a table filled with rows corresponding to all classes in the objects
folder. 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
.lua
from the end of the line, because the require
function produces an error if you leave it. This can be done with the string local file = file:sub(1, -5)
, which uses one of the built-in Lua string functions . So after doing this, all classes defined inside the objects
folder will be automatically loaded. Later, the recursiveEnumerate
function will also be used to automatically load other resources, such as images, sounds, and shaders.Circle
class, which in its constructor receives the x
, y
and radius
arguments, which have the x
, y
, radius
and creation_time
attributes, as well as the update
and draw
methods. The x
, y
and radius
attributes must be initialized with the values passed from the constructor, and the creation_time
attribute must be initialized with a relative instance creation time (see love.timer ). The update
method should take the argument dt
, and the draw function should draw the filled-in cycle centered at x, y
with a radius of radius
(see love.graphics ). An instance of this Circle
class must be created at position 400, 300 with a radius of 50. It must also be updated and drawn on the screen. Here's what the screen should look like:HyperCircle
class that inherits from the Circle
class. HyperCircle
is similar to Circle
, only the outer circle is drawn around it. It should receive additional line_width
and outer_radius
arguments in the constructor. An instance of this HyperCircle
class HyperCircle
be created at position 400, 300 with a radius of 50, a line width of 10, and an outer radius of 120. The screen should look like this:.
and when should you use each of them? 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
counter_table.value
? Why does the increment
function take an argument called self
? Can this argument have some other name? And what is the variable that is represented by self
in this example?a
, b
, c
and sum
. a
, b
and c
must be initialized with values of 1, 2, and 3, and sum
must be a function that adds a
, b
and c
. The value of the sum must be stored in the attribute c
table (that is, after all operations are performed, the table must have the attribute c
with a value of 6).someMethod
, can it have an attribute with the same name? If not, why not? SomeClass = ParentClass:extend()
ParentClass
variable ParentClass
already be defined? Or, in other words, is there any guarantee that the required ParentClass
will be earlier than SomeClass
? If so, how is this guaranteed? If not, how can you fix this problem? local ClassName = Object:extend() ... return ClassName
requireFiles
function requireFiles
that it can still automatically load all classes? 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
game
within which there is an object level
, within which there is an object player
. In order for the player object to receive keyboard input, all these three objects must have two call handlers associated with the keyboard, because at the top level we only want to call game:keypressed
inside love.keypressed
, because we don’t want lower levels to know about the level or the player. Therefore, I created a library to solve this problem. You can download and install it like any other library we have reviewed. Here are some 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
mouse1
will be printed on the screen pressed
, and the button release frame will be printed released
. In all other frames when pressing is not performed, the challenges input:pressed
and input:released
will return false, and everything will be performed inside the conditional. The same applies to the function input:down
, only it returns true in each frame when the button is held down, and false otherwise.down
: function love.update(dt) if input:down('test', 0.5) then print('test event') end end
test
, then every 0.5 seconds the console will be printed test event
. function love.load() input = Input() input:bind('mouse1', function() print(love.math.random()) end) end
mouse1
? And when you let go? And while holding?+
to the action add
; then, while holding the action key, add
increase the value of the variable sum
(initially 0) by 1 every 0,25
second. Print the value sum
to the console with each increment.up
, left
, right
anddown
and then output the action name to the console when each button is pressed.trigger
. Trigger buttons return a Boolean value from 0 to 1, indicating a click. How will you get this value? Timer = require 'libraries/hump/timer' function love.load() timer = Timer() end function love.update(dt) timer:update(dt) end
Timer
or create a new instance. I decided to choose the second option. I use a global variable for global timers timer
, and when I need timers inside objects, for example, in the Player class, they will have their own instances of timers created locally.after
, every
and tween
. And although I personally do not use the function script
, for some it may be useful, so it is worth mentioning it. Let's look at the timing functions: function love.load() timer = Timer() timer:after(2, function() print(love.math.random()) end) end
after
quite simple. It receives the number and function, and performs the function after the specified number of seconds. In the example above, two seconds after starting the game, a random number should be displayed in the console. One of the convenient features after
is that this function can be connected in chains. For example: 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
script
, so you can choose the one that is most convenient for you. function love.load() timer = Timer() timer:every(1, function() print(love.math.random()) end) end
after
, it receives a number and a function, and then performs the function after a specified number of seconds. In addition, it may also receive a third argument, in which the number of operations is passed. For example: function love.load() timer = Timer() timer:every(1, function() print(love.math.random()) end, 5) end
every
without explicitly specifying the number of retries is to force it to return false. This is useful in situations where the stop condition is not fixed or unknown at the time of the call every
.every
is to use a function after
, for example, like this: function love.load() timer = Timer() timer:after(1, function(f) print(love.math.random()) timer:after(1, f) end) end
every
in this way lies in the fact that we can change the time between operations by changing the value in the second call after
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
every
. Triggering at variable intervals is very useful in a variety of situations, so you should know how they are implemented. We now turn to the function tween
: 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
tween
harder to master, because it takes many arguments: it gets the number of seconds, the worktable, the target table, and the transition mode. It jumps to the target table in the worksheet. In the example above, the table circle
has a key radius
with an initial value of 24. For 6 seconds, the value will change to 96 in the transition mode in-out-cubic
. (Here is a useful list of all transition modes ) It seems complicated, but it looks like this:tween
can also receive an additional argument after the transition mode - a function that will be called after the transition is completed. It can be used in many cases, but if we take the previous example, we can use it to compress the circle after expanding back: 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
after
, every
and tween
- remain in the group of the most useful functions in my code base. They are very flexible and with their help you can achieve a lot. So understand them to have an intuitive understanding of what you are doing!cancel
to cancel a specific timer: function love.load() timer = Timer() local handle_1 = timer:after(2, function() print(love.math.random()) end) timer:cancel(handle_1)
after
random number for output to the console two seconds later and store the handle of this timer in a variable handle_1
. Then we cancel this call by calling cancel
with an argument handle_1
. It is very important to learn how to do this, because often we have situations when we create timer calls based on certain events. For example, when a player presses a key r
, we want to output a random number to the console in two seconds: function love.keypressed(key) if key == 'r' then timer:after(2, function() print(love.math.random()) end) end end
main.lua
and run the project, then after clicking r
on the screen with a delay, a random number should appear. If you press r
several times, several numbers will appear with a delay, one after the other. But sometimes we need this behavior so that when you repeat an event several times, it would reset the timer and start counting from 0 again. This means that when you click on, r
we want all previous timers created during the execution of this event to be canceled in the past. One way to do this is to somehow store all the descriptors, somehow bind them to the event identifier and call a certain undo function for the event identifier itself, which will undo the descriptors of all the timers associated with this event. Here is the solution: function love.keypressed(key) if key == 'r' then timer:after('r_key_press', 2, function() print(love.math.random()) end) end end
r_key_press
attached to a timer that is created when the key is pressed r
. If the key is pressed repeatedly several times, the module automatically sees that the event has other registered timers and, by default, cancels the previous timers, which is what we are striving for. If the label is not used, the default behavior is the usual module behavior.main.lua
with libraries/hump/timer
the file location EnhancedTimer.lua
. Personally, I put it in libraries/enhanced_timer/EnhancedTimer
. This also means that the library hump
is located inside the folder.libraries
. If you name your folders in some other way, then you need to change the path at the top of the file EnhancedTimer
. In addition, you can also use the library written by me , which has the same functionality as hump.timer, plus handles event labels.for
and one function declaration after
within this loop, print 10 random numbers on the screen with an interval of 0.5 seconds before each output. 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
tween
, move the attribute of the w
first rectangle within 1 second in the transition mode in-out-cubic
. After that, perform the transition attribute of the h
second triangle for 1 second in the transition mode in-out-cubic
. After that, move both rectangles back to their original attributes after 2 seconds in transition mode in-out-cubic
. It should look like this:d
energy bar should simulate the damage taken. It should look like this:after
.e
to the expansion of the circle when it is pressed, and the key s
to compression when pressed. Each new keystroke should cancel the current expansion / contraction. function love.load() timer = Timer() a = 10 end function love.update(dt) timer:update(dt) end
tween
and without placing a variable a
inside another table, to make the transition of its value to 20 within 1 second in the transition mode linear
? for k, v in pairs(some_table) do print(k, v) end
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}
a
to the console using the function each
.b
.d
, using the function map
.map
, apply the a
following transformations to the table : if the value is a number, then it should be doubled; if the value is a string, then add to it by concatenation 'xD'
; if the value is boolean, then you need to switch its state; and finally, if the value is a table, you should skip it.d
. The result should be equal to 26. if _______ then print('table contains the value 9') end
b
value 9?c
.d
so that only the numbers less than 5 are left.c
so that only the rows are left.c
and d
figures. The code should return false for the first table and true for the second.d
.d
go in reverse order.d
.b
, c
and d
no duplicates.b
and d
.b
to a table d
.
(Room), which are similar to scenes in other engines. We will also study the principle
(Area), the type of construction for managing objects that may be inside the room. As in the two previous parts, there will be no game-related code in this, we will only consider high-level architectural solutions.rooms
. Here's what a room might look like Stage
: Stage = Object:extend() function Stage:new() end function Stage:update(dt) end function Stage:draw() end
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
love.load
, a global variable is defined current_room
. The idea is that only one room can be active at a time, so this variable will store a link to the current active room object. Then, if there is a current active room, it will be drawn in love.update
and love.draw
. This means that all rooms must have the update and draw functions defined.gotoRoom
. It receives room_type
, which is a simple string with the class name of the room to go to. Therefore, if, for example, we have a class Stage
defined as a room, then this function can be passed a string'Stage'
. The implementation of this functionality depends on how the automatic loading of classes in the previous part of the tutorial was configured, that is, from loading all classes as global variables._G
, that is, they can be accessed like any other variable in a regular table. If the global variable Stage
contains the definition of the Stage class, then it can be accessed by simply using the program anywhere Stage
, or _G['Stage']
, or _G.Stage
. Since we want to be able to load any arbitrary room, it will be logical to get a row room_type
and get access to the class definition via the global table.room_type
is a string'Stage'
, the line inside the function gotoRoom
parses it in current_room = Stage(...)
. This means that a copy of the new room will be created Stage
. It also means that every time you move to a new room, this new room is created from scratch, and the previous room is deleted. This works in Lua as follows: when no variable is referenced to a table, the garbage collector deletes it. And when a variable is no longer referenced to an instance of the previous room current_room
, it will be deleted by the garbage collector.MainMenu
and in it I would create all the logic necessary for the main menu, that is, the background, five options, the effect arising when choosing a new option, small lightning along the edges of the screen, etc. And when a player chooses an option, I would call gotoRoom(option_type)
that would replace the current room with the one created for this option. In our case it would be an extra room Play
, CO-OP
, Settings
and Stats
.MainMenu
that processes all these additional options without having to separate them into different rooms. It is often better to store everything in the same room and process the transitions inside, rather than in an external system. The choice depends on the situation and in this case I do not have enough information to say which is better.gotoRoom
). The player selects the normal option and the following screen appears:CharacterSelect
. Like the others, she would do everything necessary on this screen: the background, the characters in the background, the effects that occur when changing the character, the choice of character and all the logic necessary for this. After selecting a character, the boot screen appears:LoadingScreen
, Game
, MutationSelect
and DeathScreen
. But if you think about it, some of them may be redundant.LoadingScreen
from Game
. The loading process most likely performs the generation of the level that should occur in the room Game
, so there is no point in separating them from each other, because the loading would occur in the room LoadingScreen
and not in the room Game
, and then the data created in the first one would have to be transferred in the second. In my opinion, this is an unnecessary over-complication.MutationSelect
.MainMenu
-> Play
-> CharacterSelect
-> Game
-> MutationSelect
-> Game
-> ... When death happens, the player can either return to MainMenu
or try again and restart the new one Game
. All these transitions can be implemented through a simple function gotoRoom
. 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
room_type
value is also passed room_name
. This is necessary because in this case I want to be able to refer to the rooms using an identifier, that is, each room_name
must be unique. This room_name
can be a string or a number - if they are unique, then it does not matter.addRoom
that simply creates an instance of a room and stores it inside the table. Then, gotoRoom
instead of creating a new instance of a room, the function can check this table, and if the room already exists, then simply restore it, and otherwise create it from scratch.activate
anddeactivate
. If the room already exists and you want to go into it again by calling gotoRoom
, the current room is deactivated first, then it changes to the target room, and then the target room is activated. These calls are useful in many cases, for example, to save data or to load data from disk, dereference of variables (so that they can be deleted) and so on.rooms
, when changing current_room
to another room, the previous one will not be deleted by the garbage collector and can be obtained in the future.Room
in which the whole gameplay of the room takes place. And there will be a common room Game
that coordinates the data at a higher level. For example, the Game
algorithm will generate levels in the room and from its results by callingaddRoom
multiple instances will be created Room
. Each of these instances will have its own unique ID and will be used gotoRoom
to activate one of them when the game starts . In the process of moving the player and exploring the dungeons, further calls will be made gotoRoom
and the instances already created Room
will be activated / deactivated as the player moves.current_room
will not work. I will not now consider ways to change the code to solve this problem, but I thought that it is worth mentioning that the code presented here will not fully correspond to what is happening in the game and I have simplified everything a bit. When I move on to writing my own game and implementing transitions, I’ll talk about this in more detail.CircleRoom
which draws a circle in the center of the screen, RectangleRoom
which draws a rectangle in the center of the screen and PolygonRoom
which draws a polygon in the center of the screen. Assign keys F1
, F2
and F3
to switch between rooms.addRoom
or gotoRoom
.
(Area). One of the operations usually performed inside the room is the management of various objects. All objects must be updated and drawn, as well as added to the room and deleted after death. Sometimes it is also required to request objects in a certain area (for example, when an explosion occurs, we need to cause damage to all objects around it, that is, take all the objects inside the circle and cause them damage), and also apply certain general actions to them, for example, sorting by the depth of their layer so that they can be drawn in a specific order. All these functions were the same in different rooms and different games that I created, so I gathered 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
GameObject
that has several common attributes that all objects in the game have. This 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
area
position x, y
and a table opts
containing additional optional arguments. The first thing that happens in the code is that this additional table is taken opts
and all its attributes are assigned to this object. For example, if we create it GameObject
this way:, game_object = GameObject(area, x, y, {a = 1, b = 2, c = 3})
then the line for k, v in pairs(opts) do self[k] = v
essentially copies the ads a = 1
, b = 2
and c = 3
into this newly created instance. Now you should understand what is happening here, but if you do not understand, then reread the section on OOP in the previous part, and also about the work of the tables in Lua.self.area
, and the position inself.x, self.y
. Then this game object is assigned an ID. This ID must be unique for each object so that we can identify each object without conflict. A simple UUID generation function is quite suitable for our game. Such a function is in a library called lume in lume.uuid
. We will not use this library, we need only this one function, that is, it will be more logical to take only it, and not to install the entire 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
utils.lua
. This file will contain support functions that cannot be assigned to any particular category. This function produces a string of the form '123e4567-e89b-12d3-a456-426655440000'
that will be unique for any purpose.math.random
. If you use print(UUID())
to see what it generates, then we will see that when the project is executed, it will generate the same ID. This problem occurs because it always uses the same seed number. One way to solve this problem is that each time you start the program, you can randomize the seed number based on the time. You can do this with math.randomseed(os.time())
.math.random
usedlove.math.random
. As we remember from the first part of the tutorial, the first function called in love.run
is the love.math.randomSeed(os.time())
one that deals with the randomization of the seed, but only for the random number generator LÖVE. Since I use LÖVE, when I need randomness, I will use its functions, not the Lua functions. Having made such a change to a function UUID
, you will see that it will begin to generate different IDs.dead
. The idea is that when dead
it is true, the game object is removed from the game. The same thing happens with a class instance.Timer
assigned to each game object. I saw that time functions are used in almost every object, so it seemed logical to use them by default for all objects. Finally, the function update
updates the timer.Area
must 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
dead
and acts accordingly. First, the game object is updated in the usual way, then it is checked for dead. If it is true, the object is simply removed from the list game_objects
. It is important here that the loop is performed in reverse order, from the end of the list to the beginning. This is because if we delete the elements from the Lua table, moving in a straightforward order, as a result, we will skip some elements, as can be seen from this discussion .addGameObject
that adds to the Area
new game object: 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
area:addGameObject('ClassName', 0, 0, {optional_argument = 1})
. The variable game_object_type
will work the same way as the lines in the function gotoRoom
, that is, they are the class names of the object being created. _G[game_object_type]
in the example above, it performs the parsing into a global variable ClassName
, which will contain the definition of the class ClassName
. An instance of the target class is created, added to the list game_objects
, and then returned. Now this instance will be updated and drawn in each frame.Stage
in which there is Area
. Then create an object Circle
that inherits from GameObject
and add an instance of this object to the room Stage
in a random position every two seconds. The instance Circle
must destroy itself at a random time interval from 2 to 4 seconds.Stage
that does not exist Area
. Create an object Circle
that does not inherit from GameObject
and add an instance of this object to the scene Stage
at a random position every two seconds. The instance Circle
must destroy itself at a random time interval from 2 to 4 seconds.random
. Improve this function so that it receives only one value instead of two and generates a random real number from 0 to a value in this case (when only one argument is obtained). Also improve function so that values min
and max
can be reversed, that is, to the first value may be greater than the second.local opts = opts or {}
the function addGameObject
?engine
, although I understand that in fact it is not an engine. The further we progress in the game, the more and more I will add code belonging to this category, which can be used in various games. If you want to take the most important of these tutorials, then this should definitely be the code. He was very welcome to me many times.Area
function inside a class.getGameObjects
which will work as follows: -- Enemy all_enemies = area:getGameObjects(function(e) if e:is(Enemy) then return true end end) -- 50 healthy_objects = area:getGameObjects(function(e) if e.hp and e.hp >= 50 then return true end end)
getGameObjects
.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
printAll
that takes an unknown number of arguments and prints them all to the console. printAll(1, 2, 3)
will output 1, 2 and 3 printAll(1, 2, 3, 4, 5, 6, 7, 8, 9)
to the console , and will output numbers from 1 to 9 to the console. The number of arguments passed is unknown and may vary.printText
that receives an unknown number of lines, which performs their concatenation into a common string and outputting this string to the console.Rectangle
drawing a rectangle with a width and height at the creation position. Create 10 instances of this class at random positions with random width and height. When clicked, a d
random instance should be deleted from the environment. When the number of copies reaches 0, 10 more new copies with random width and height should be created in random positions of the screen.Circle
drawing a circle with some radius in the creation position. Create 10 copies of this class in random positions on the screen with a random radius and with an interval of 0.25 seconds between each copy. After creating all instances (that is, after 2.5 seconds), start deleting one random instance every [0.5, 1] seconds (random number from 0.5 to 1). After deleting all instances, repeat the entire process of recreating 10 instances and their sequential deletion. This process should be repeated endlessly.Area
function inside the class queryCircleArea
that works as follows: -- 'Enemy' 'Projectile' 50 100, 100 objects = area:queryCircleArea(100, 100, 50, {'Enemy', 'Projectile'})
x
, y
, radius
and a list of strings containing the names of the target classes. Then it returns all the objects that belong to these classes and are inside a circle radius
with a radius centered on the position x, y
.Area
function inside the class getClosestGameObject
that works like this: -- 'Enemy' 50 100, 100 closest_object = area:getClosestObject(100, 100, 50, {'Enemy'})
queryCircleArea
, but returns only one object (closest).for
?Source: https://habr.com/ru/post/349276/
All Articles