{ 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