



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 gamewithin 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:keypressedinside 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 mouse1will 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:pressedand input:releasedwill 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, addincrease the value of the variable sum(initially 0) by 1 every 0,25second. Print the value sumto the console with each increment.up, left, rightanddownand 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 Timeror 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, everyand 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 afterquite 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 afteris 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 everywithout 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.everyis 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 everyin this way lies in the fact that we can change the time between operations by changing the value in the second call afterinside 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 tweenharder 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 circlehas a key radiuswith 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:
tweencan 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, everyand 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!cancelto 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) afterrandom 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 cancelwith 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.luaand run the project, then after clicking ron the screen with a delay, a random number should appear. If you press rseveral 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, rwe 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_pressattached 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.luawith libraries/hump/timerthe file location EnhancedTimer.lua. Personally, I put it in libraries/enhanced_timer/EnhancedTimer. This also means that the library humpis 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.forand one function declaration afterwithin 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 wfirst rectangle within 1 second in the transition mode in-out-cubic. After that, perform the transition attribute of the hsecond 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:
denergy bar should simulate the damage taken. It should look like this:
after.eto the expansion of the circle when it is pressed, and the key sto 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 tweenand without placing a variable ainside 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} ato the console using the function each.b.d, using the function map.map, apply the afollowing 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 bvalue 9?c.dso that only the numbers less than 5 are left.cso that only the rows are left.cand dfigures. The code should return false for the first table and true for the second.d.dgo in reverse order.d.b, cand dno duplicates.band d.bto 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.updateand 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 Stagedefined 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 Stagecontains 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_typeand get access to the class definition via the global table.room_typeis a string'Stage', the line inside the function gotoRoomparses 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.

MainMenuand 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, Settingsand Stats.MainMenuthat 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, MutationSelectand DeathScreen. But if you think about it, some of them may be redundant.LoadingScreenfrom 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 LoadingScreenand 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 MainMenuor 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_typevalue 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_namemust be unique. This room_namecan be a string or a number - if they are unique, then it does not matter.addRoomthat simply creates an instance of a room and stores it inside the table. Then, gotoRoominstead 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.activateanddeactivate. 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_roomto another room, the previous one will not be deleted by the garbage collector and can be obtained in the future.
Roomin which the whole gameplay of the room takes place. And there will be a common room Gamethat coordinates the data at a higher level. For example, the Gamealgorithm will generate levels in the room and from its results by callingaddRoommultiple instances will be created Room. Each of these instances will have its own unique ID and will be used gotoRoomto activate one of them when the game starts . In the process of moving the player and exploring the dungeons, further calls will be made gotoRoomand the instances already created Roomwill be activated / deactivated as the player moves.
current_roomwill 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.CircleRoomwhich draws a circle in the center of the screen, RectangleRoomwhich draws a rectangle in the center of the screen and PolygonRoomwhich draws a polygon in the center of the screen. Assign keys F1, F2and F3to switch between rooms.addRoomor 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 GameObjectthat 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 areaposition x, yand a table optscontaining additional optional arguments. The first thing that happens in the code is that this additional table is taken optsand all its attributes are assigned to this object. For example, if we create it GameObjectthis 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] = vessentially copies the ads a = 1, b = 2and c = 3into 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.randomusedlove.math.random. As we remember from the first part of the tutorial, the first function called in love.runis 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 deadit is true, the game object is removed from the game. The same thing happens with a class instance.Timerassigned 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 updateupdates the timer.Areamust 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 deadand 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 .addGameObjectthat adds to the Areanew 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_typewill 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.Stagein which there is Area. Then create an object Circlethat inherits from GameObjectand add an instance of this object to the room Stagein a random position every two seconds. The instance Circlemust destroy itself at a random time interval from 2 to 4 seconds.Stagethat does not exist Area. Create an object Circlethat does not inherit from GameObjectand add an instance of this object to the scene Stageat a random position every two seconds. The instance Circlemust 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 minand maxcan 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.Areafunction 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, fand 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 printAllthat 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.printTextthat receives an unknown number of lines, which performs their concatenation into a common string and outputting this string to the console.Rectangledrawing 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 drandom 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.Circledrawing 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.Areafunction inside the class queryCircleAreathat works as follows: -- 'Enemy' 'Projectile' 50 100, 100 objects = area:queryCircleArea(100, 100, 50, {'Enemy', 'Projectile'}) x, y, radiusand 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 radiuswith a radius centered on the position x, y.Areafunction inside the class getClosestGameObjectthat 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