

{ x = x, y = y, text = love.graphics.newText(font, {boost_color, 'blue text', default_color, 'white text'} } x, y , as well as the text attribute. This text attribute is a Text object. We will use Text objects from LÖVE, because with their help you can easily define colored text. But before we can add lines to the Console room, we need to create it, so let's get this done. Basically, this task is similar to creating a SkillTree room.lines in which all text lines will be stored, and then in the draw function we will go around the entire table and draw each line. We will also add the addLine function, which will add a new text line to the lines table: function Console:new() ... self.lines = {} self.line_y = 8 camera:lookAt(gw/2, gh/2) self:addLine(1, {'test', boost_color, ' test'}) end function Console:draw() ... for _, line in ipairs(self.lines) do love.graphics.draw(line.text, line.x, line.y) end ... end function Console:addLine(delay, text) self.timer:after(delay, function() table.insert(self.lines, {x = 8, y = self.line_y, text = love.graphics.newText(self.font, text)}) self.line_y = self.line_y + 12 end) end line_y attribute that tracks the position of y, in which we must add the following line. It is incremented by 12 each time you call addLine , because we want new lines to be added under the previous one, as it happens in normal terminals.addLine function has a delay. This delay is useful because when adding multiple lines to the console, we do not want them added at the same time. We want a slight delay before each addition, because everything looks better this way. In addition, we can do here so that, together with the delay in adding each line, it is added character by character. That is, instead of one line added at a time, each character is added with a slight delay, which will give us an even more pleasant effect. To save time, I will not do it myself, but it can be a good exercise for you (and we already have some logic for this in the InfoText object).

addInputLine function, which will behave as addLine , except that it will add text to the input line and include the ability for the player to enter text. By default, we will use the text of the [root]arch~ input line, placed before the input, as in a normal terminal. function Console:addInputLine(delay) self.timer:after(delay, function() table.insert(self.lines, {x = 8, y = self.line_y, text = love.graphics.newText(self.font, self.base_input_text)}) self.line_y = self.line_y + 12 self.inputting = true end) end base_input_text looks like this: function Console:new() ... self.base_input_text = {'[', skill_point_color, 'root', default_color, ']arch~ '} ... end inputting to true. This boolean value will be used to tell us whether we should read keyboard input. If so, we can simply add to the list as a string all the characters that the player enters, and then add this string to our Text object. It looks like this: function Console:textinput(t) if self.inputting then table.insert(self.input_text, t) self:updateText() end end function Console:updateText() local base_input_text = table.copy(self.base_input_text) local input_text = '' for _, character in ipairs(self.input_text) do input_text = input_text .. character end table.insert(base_input_text, input_text) self.lines[#self.lines].text:set(base_input_text) end Console:textinput will be called at every call of love.textinput , which happens every time a player love.textinput key: -- in main.lua function love.textinput(t) if current_room.textinput then current_room:textinput(t) end end inputting value to false, get the contents of the input_text table and do something with it. That is, if a player has entered “help” and then pressed Enter, we will launch the help command. And the backspace key should simply remove the last element from the input_text table: function Console:update(dt) ... if self.inputting then if input:pressed('return') then self.inputting = false -- Run command based on the contents of input_text here self.input_text = {} end if input:pressRepeat('backspace', 0.02, 0.2) then table.remove(self.input_text, #self.input_text) self:updateText() end end end base_input_text , concatenated with the contents of input_text . function Console:new() ... self.cursor_visible = true self.timer:every('cursor', 0.5, function() self.cursor_visible = not self.cursor_visible end) end cursor_visible is true. Next we draw a rectangle: function Console:draw() ... if self.inputting and self.cursor_visible then local r, g, b = unpack(default_color) love.graphics.setColor(r, g, b, 96) local input_text = '' for _, character in ipairs(self.input_text) do input_text = input_text .. character end local x = 8 + self.font:getWidth('[root]arch~ ' .. input_text) love.graphics.rectangle('fill', x, self.lines[#self.lines].y, self.font:getWidth('w'), self.font:getHeight()) love.graphics.setColor(r, g, b, 255) end ... end x stores the position of the cursor. We add 8 to it, because each line is drawn by default, starting at position 8, so if we do not take this into account, the position of the cursor will be incorrect. We will also assume that the width of the cursor rectangle will be equal to the width of the letter 'w' of the current font. Usually w is the widest letter, so we chose it. But it can be any constant number, for example 10 or 8.
ResolutionModule , which we implement, will allow the player to change the resolution in the game. We will separate the modules from the rest of the Console room code, because their logic can be quite complex, and it makes sense to separate them into separate objects. We implement a module that looks like this:
Console:addLine . In addition to these added lines, it has a selection logic that allows you to select the desired resolution. After selecting the resolution and clicking on the Enter window changes to reflect this new resolution, we add a new input line using the Console:addInputLine and disable the option to select ResolutionModule in this object, returning control to the console. function Console:new() ... self.modules = {} ... end function Console:update(dt) self.timer:update(dt) for _, module in ipairs(self.modules) do module:update(dt) end if self.inputting then ... end function Console:draw() ... for _, module in ipairs(self.modules) do module:draw() end camera:detach() ... end self.modules table, update and draw them. At the appropriate time, each module will activate / deactivate itself, that is, on the part of the Console, we do not need to do almost anything. function Console:update(dt) ... if self.inputting then if input:pressed('return') then self.line_y = self.line_y + 12 local input_text = '' for _, character in ipairs(self.input_text) do input_text = input_text .. character end self.input_text = {} if input_text == 'resolution' then table.insert(self.modules, ResolutionModule(self, self.line_y)) end end ... end end input_text variable will store what the player entered in the input line, and then, if this text is “resolution”, we create a new ResolutionModule object and add it to the modules list. Most modules will need a link to the console, as well as the current position of y, to which the lines are added, so the module will be located under the lines of code already present in the console. To do this, when creating a new module object, we pass self and self.line_y . function ResolutionModule:new(console, y) self.console = console self.y = y self.console:addLine(0.02, 'Available resolutions: ') self.console:addLine(0.04, ' 480x270') self.console:addLine(0.06, ' 960x540') self.console:addLine(0.08, ' 1440x810') self.console:addLine(0.10, ' 1920x1080') end function ResolutionModule:new(console, y) ... self.selection_index = sx self.selection_widths = { self.console.font:getWidth('480x270'), self.console.font:getWidth('960x540'), self.console.font:getWidth('1440x810'), self.console.font:getWidth('1920x1080') } end selection_index keeps track of the current selection, and initially it is equal to sx . sx can be 1, 2, 3 or 4, depending on the size selected by main.lua when calling the resize function. selection_widths stores the widths of the rectangle in each selection line. Since the rectangle should close each resolution, we need to determine its size based on the size of the characters that make up the string of this resolution. function ResolutionModule:update(dt) ... if input:pressed('up') then self.selection_index = self.selection_index - 1 if self.selection_index < 1 then self.selection_index = #self.selection_widths end end if input:pressed('down') then self.selection_index = self.selection_index + 1 if self.selection_index > #self.selection_widths then self.selection_index = 1 end end ... end selection_index so that the value is not less than 1 and not more than 4. function ResolutionModule:draw() ... local width = self.selection_widths[self.selection_index] local r, g, b = unpack(default_color) love.graphics.setColor(r, g, b, 96) local x_offset = self.console.font:getWidth(' ') love.graphics.rectangle('fill', 8 + x_offset - 2, self.y + self.selection_index*12, width + 4, self.console.font:getHeight()) love.graphics.setColor(r, g, b, 255) end function ResolutionModule:new(console, y) ... self.console.timer:after(0.02 + self.selection_index*0.02, function() self.active = true end) end function ResolutionModule:update(dt) if not self.active then return end ... if input:pressed('return') then self.active = false resize(self.selection_index) self.console:addLine(0.02, '') self.console:addInputLine(0.04) end end function ResolutionModule:draw() if not self.active then return end ... end active to true several frames after the module is created. Due to this, the rectangle will not be drawn before adding rows, because the rows are added with a slight delay. If the active variable is not active, then the update and draw functions will not be executed, that is, we will not read the input for this object and draw the selection rectangle. In addition, when you press Enter, we assign active to false, call the resize function, and then transfer control back to the Console, adding a new input line. All this gives us the appropriate behavior and because of this, everything will work as it should.AchievementsModule module. It shows all the achievements and requirements necessary to unlock them. We will look at achievements in the next part of the tutorial, so go back to this exercise later!ClearModule . This module allows you to delete all saved data or clear the skill tree. Saving / loading data will also be discussed in the next article, so you can also return to this exercise later.ChooseShipModule . This module allows the player to select and unlock ships for gameplay.mute , skills , start , exit and device . mute mutes all sounds. skills makes the transition to the room SkillTree. start creates a ChooseShipModule, and then starts the game after the player selects the ship. exit exits the game.dumpLoveFile and loadLoveFile . These functions will save and load any data to / from the file that we pass to them using the love.filesystem . As they say on the link, the location of saving files depends on the operating system. In Windows, the file will be saved in C:\Users\user\AppData\Roaming\LOVE . To change the save location, we can use love.filesystem.setIdentity . If we change the value to BYTEPATH , the save file will be saved to C:\Users\user\AppData\Roaming\BYTEPATH .save and load . They will be defined in main.lua . Let's start with the save function: function save() local save_data = {} -- Set all save data here bitser.dumpLoveFile('save', save_data) end save_data table and put all the data we need to save into it. For example, if we want to save the amount of skill points a player has, then we simply write save_data.skill_points = skill_points , that is, the value contained in the global variable skill_points will be stored in skill_points . The same applies to all other types of data. However, it is important to limit yourself to storing values and tables of values. Saving objects as a whole, images, and other types of more complex data will most likely not work.save_data , we simply call bitser.dumpLoveFile and save all this data to the 'save' file. When in C:\Users\user\AppData\Roaming\BYTEPATH a save file is created, and when this file exists, all the information we need to save will be saved there. We can call this function when you close the game or at the end of a round, this is up to you. The only problem I can see here is that if you save only at the end of the game in the event of a program crash, the player’s progress will most likely not be saved. function load() if love.filesystem.exists('save') then local save_data = bitser.loadLoveFile('save') -- Load all saved data here else first_run_ever = true end end bitser.loadLoveFile with the name of the saved file ( save ), and then put all the data inside the local table save_data . By writing all the saved data to this table, we can assign them to the corresponding variables. For example, if we want to load player skill points, we will write skill_points = save_data.skill_points , that is, we assign the saved skill points to our global skill points variable.love.filesystem.exists and load it only if it exists. If not, then we simply set the global variable first_run_ever to true. This variable is useful because usually when you first start the game, we need to perform some additional actions, such as starting a tutorial or displaying a message. The download function will be called once in love.load when loading the game. It is important that this function be called after the globals.lua file, because in it we rewrite the global variables.bought_node_indexes table, because it stores all the nodes the player has bought.achievements . And this table will contain the keys that represent the name of the achievement, and the values that determine whether the achievement is unlocked. , '50K' , , 50 000 , , achievements['50K'] true, — false.10K Fighter , , 10 000 Fighter. , achievements['10K Fighter'] true, , 10K, Fighter. It looks like this: function Stage:finish() timer:after(1, function() gotoRoom('Stage') if not achievements['10K Fighter'] and score >= 10000 and device = 'Fighter' then achievements['10K Fighter'] = true -- Do whatever else that should be done when an achievement is unlocked end end) end achievements['10K Fighter'] true. , , save load . extern vec2 amount; vec4 effect(vec4 color, Image texture, vec2 tc, vec2 pc) { return color*vec4(Texel(texture, tc - amount).r, Texel(texture, tc).g, Texel(texture, tc + amount).b, Texel(texture, tc).a); } rgb_shift.frag resources/shaders , Stage love.graphics.newShader . — effect . color , love.graphics.setColor , 0-255 0-1. 255, 255, 255, 255, vec4 1.0, 1.0, 1.0, 1.0. texture , . , LÖVE, . effect , . vec4, 4 — , , -.tc . 0 1 . — 0, 0 , — 1, 1 . texture2D ( LÖVE Texel ) . pc . .amount , RGB-. send . return color*vec4( Texel(texture, tc - amount).r, Texel(texture, tc).g, Texel(texture, tc + amount).b, Texel(texture, tc).a); Texel . , , RGB-. ( ) , «» . tc - amount tc + amount , , . , ( -), .Area draw , . : function TrailParticle:new(area, x, y, opts) TrailParticle.super.new(self, area, x, y, opts) self.graphics_types = {'rgb_shift'} ... end function Area:drawOnly(types) table.sort(self.game_objects, function(a, b) if a.depth == b.depth then return a.creation_time < b.creation_time else return a.depth < b.depth end end) for _, game_object in ipairs(self.game_objects) do if game_object.graphics_types then if #fn.intersection(types, game_object.graphics_types) > 0 then game_object:draw() end end end end Area:draw , . intersection , , graphics_types types . , , rgb_shift , area:drawOnly({'rgb_shift'}) , graphics_types . - , #fn.intersection , .Area:drawExcept , , , , - . It looks like this: function Area:drawExcept(types) table.sort(self.game_objects, function(a, b) if a.depth == b.depth then return a.creation_time < b.creation_time else return a.depth < b.depth end end) for _, game_object in ipairs(self.game_objects) do if not game_object.graphics_types then game_object:draw() else if #fn.intersection(types, game_object.graphics_types) == 0 then game_object:draw() end end end end graphics_types , types 0, , .TrailParticle , RGB- . , RGB- TrailParticle, : function Stage:draw() ... love.graphics.setCanvas(self.rgb_shift_canvas) love.graphics.clear() camera:attach(0, 0, gw, gh) self.area:drawOnly({'rgb_shift'}) camera:detach() love.graphics.setCanvas() ... end main_canvas rgb_shift_canvas . , , 'rgb_shift' . , , . Shockwave Downwell .main_canvas , , . It will look like this: function Stage:draw() ... love.graphics.setCanvas(self.main_canvas) love.graphics.clear() camera:attach(0, 0, gw, gh) self.area:drawExcept({'rgb_shift'}) camera:detach() love.graphics.setCanvas() ... end rgb_shift_canvas final_canvas , RGB-. : function Stage:draw() ... love.graphics.setCanvas(self.final_canvas) love.graphics.clear() love.graphics.setColor(255, 255, 255) love.graphics.setBlendMode("alpha", "premultiplied") self.rgb_shift:send('amount', { random(-self.rgb_shift_mag, self.rgb_shift_mag)/gw, random(-self.rgb_shift_mag, self.rgb_shift_mag)/gh}) love.graphics.setShader(self.rgb_shift) love.graphics.draw(self.rgb_shift_canvas, 0, 0, 0, 1, 1) love.graphics.setShader() love.graphics.draw(self.main_canvas, 0, 0, 0, 1, 1) love.graphics.setBlendMode("alpha") love.graphics.setCanvas() ... end send amount , , . 0 1, gw gh . , , , 2 , rgb_shift_mag 2, 2/gw b 2/gh, 2 / , 2. main final, final , . function Stage:draw() ... love.graphics.setColor(255, 255, 255) love.graphics.setBlendMode("alpha", "premultiplied") love.graphics.draw(self.final_canvas, 0, 0, 0, sx, sy) love.graphics.setBlendMode("alpha") love.graphics.setShader() end final_canvas , , distortion , , .
ripple.newSound :play . , , , : -- in globals.lua shoot_sound = ripple.newSound('resources/sounds/shoot.ogg') function Player:shoot() local d = 1.2*self.w self.area:addGameObject('ShootEffect', ... shoot_sound:play() ... end :play , , . , , , , , . , . , sound.lua .Source: https://habr.com/ru/post/353596/
All Articles