📜 ⬆️ ⬇️

LÖVE Development

image

The purpose of the post is to describe the main stages of development in the simplest possible way using the LÖVE framework, using the classic Asteroids atari machine game as an example.

Pochemuchuk


What is LÖVE and why exactly this?

LÖVE is a framework for two-dimensional games. It is not an engine, only an interlayer between Lua and SDL2, with additional nice features, such as clean syntax, a minimum of additional gestures to get OpenGL to work, and a set of libraries (like Box2d) that allow you to do something funny, and places to pick what came out. But, moreover, LÖVE is distinguished by a minimum of gag and a low level of interaction with iron, which allows you to make your engine around the framework (for self-study / further use) or to immediately hardcod a toy.

The simplicity of the framework allows you to write simple prototypes or even mini-games for those who are not programmers, concentrating on the programming process, and not on the development of a specific engine technology. My practice has shown that students aged 14-17 years old, are much more pleased to develop simple games than they do classical laboratory work to calculate the roots of quadratic equations or calculating credit rates, and some students begin to go deeper into the material, after which, sometimes become good programmers.
')
Why Lua? The language is quite simple to learn, simpler than JavaScript and Python, but it is enough to simply switch from it to both the above and to the low level (C / C ++). He is also quite popular in the development of video games, as part of something larger (cryEngine, GMod, OpenComputers in Minecraft, etc), and if there is modding in some game - with a very high probability, he uses Lua.
Let the poverty of the standard library not frighten, for most of the tasks there are third-party developments (so popular as to become practically the standard of the language), but in poverty there is a downside, such as the speed of development and the ability to push the language interpreter into the microcontroller, which some with all the advantages and disadvantages of scripts.

Plus LÖVE by default comes with a LuaJIT virtual machine that speeds up execution many times (critical for games), and allows you to use FFI: connecting C libraries, initializing and using C-structures, which, with metatables, can be turned into lua objects, and which save creation time / memory, etc.

Slightly closer to the point



For further work, we will need to perform the following set of actions:
  1. Download the latest version of LÖVE from the official site ;
  2. We configure the launch of the current project in LÖVE, the standard test run method is to open the directory with the main.lua file in the executable love file. Also, you can pack the contents of the directory with the main.lua file into a zip archive, and either drag and drop onto the executable file, or rename .zip to .love and set up file associations. I believe that it is easier to configure shortcuts for the current editor, notepad ++ is, for example:
    <Command name=...>path/to/love.exe $(CURRENT_DIRECTORY)</Command> 
    Examples for sublime can be found in the next article ;
  3. Create an empty directory and add a file named main.lua to it. It is desirable that there are no gaps and Cyrillic characters on the way, or some people write spaces, and then complain, but to get around you can change the shortcut or the launch method;
  4. Open in our favorite editor our clean and untainted file main.lua, and LÖVE-Wiki in your favorite browser.

Closer, but not quite


The first thing to know is that the framework functions through a set of callbacks that we write to the global table of love, which is already declared:

 function love.load(arg) --    love.load    , --     . end function love.update(dt) --   update  draw    , -- ,   : -- "->->->->" --       . end function love.draw() --       - --     love. love.graphics.print('Hello dear Love user!', 100, 100) end 

After running this code, you should feel enlightened and proceed to the next stage: something that remotely resembles something useful.

Already something like a case


Lua, by default, does not have a “normal OOP”, so this material will have a rather complicated construction for beginners from here , point 3.2, although if you are unfamiliar with the tables, you should read the entire third paragraph.

First of all, since we are doing the Asteroids, we want to get the boat, which is highly desirable and more steer.

Next, we want to shoot something and targets that can be reached.
Similarly, I would like to count points and manipulate everything in a row.

Further there will be a lot of code, but I hope the comments will be quite informative.

 --      ,  , --       . local Ship, Bullet, Asteroid, Field Ship = {} --   ,    ship, --       ship. Ship.__index = Ship --       ,     Ship.type = 'ship' --  -        'self'. function Ship:new(field, x, y) -- ,   self,   Ship. --  self   , self   Ship   . self = setmetatable({}, self) --       ,   . self.field = field -- : self.x = x or 100 -- 100 -  self.y = y or 100 --   : self.angle = 0 --    : --  : self.vx = 0 self.vy = 0 -- , /: self.acceleration = 200 --  : self.rotation = math.pi --   : self.shoot_timer = 0 self.shoot_delay = 0.3 -- ,  : self.radius = 30 --   ,    : self.vertexes = {0, -30, 30, 30, 0, 20, -30, 30} --[[  - ,   : /\ / \ /_/\_\ ]] --   . return self end function Ship:update(dt) --  ,   ,     , ? -- dt -  ,      . self.shoot_timer = self.shoot_timer - dt -- : -- "     " -   . if love.keyboard.isDown('x') and self.shoot_timer < 0 then self.field:spawn(Bullet:new(self.field, self.x, self.y, self.angle)) --   ,         , --    . self.shoot_timer = self.shoot_delay end if love.keyboard.isDown('left') then --  ,   dt -   1, -- ,  ,     Pi, --   -  ,    . self.angle = self.angle - self.rotation * dt end if love.keyboard.isDown('right') then self.angle = self.angle + self.rotation * dt end if love.keyboard.isDown('up') then --   ,      . local vx_dt = math.cos(self.angle) * self.acceleration * dt local vy_dt = math.sin(self.angle) * self.acceleration * dt --      . self.vx = self.vx + vx_dt self.vy = self.vy + vy_dt end --         . self.x = self.x + self.vx * dt self.y = self.y + self.vy * dt --    ,       : --    ,   . --    - --   ,   . self.vx = self.vx - self.vx * dt self.vy = self.vy - self.vy * dt --      : --       , --       . local screen_width, screen_height = love.graphics.getDimensions() if self.x < 0 then self.x = self.x + screen_width end if self.y < 0 then self.y = self.y + screen_height end if self.x > screen_width then self.x = self.x - screen_width end if self.y > screen_height then self.y = self.y - screen_height end end function Ship:draw() --   , --        . love.graphics.setColor(255,255,255) --     , --  ,     . --     . love.graphics.push() --       . love.graphics.translate (self.x, self.y) --      . --  Pi/2 ,      --      , ,   --       . love.graphics.rotate (self.angle + math.pi/2) --   , line - , fill -  . love.graphics.polygon('line', self.vertexes) -- , ,      -- ( love.graphics.push()). love.graphics.pop() --    , --  /  : --     ,     --      /. --          . --       , end -- "!   ! ? ,   !" --    . --  ,   ,    . --        : Bullet = {} Bullet.__index = Bullet --  -      , --         , --      : Bullet.type = 'bullet' Bullet.speed = 300 function Bullet:new(field, x, y, angle) self = setmetatable({}, self) --    self.field = field self.x = x self.y = y self.radius = 3 --   self.life_time = 5 --     --       : self.vx = math.cos(angle) * self.speed self.vy = math.sin(angle) * self.speed --     self   speed, --        -- __index   return self end function Bullet:update(dt) --   : self.life_time = self.life_time - dt if self.life_time < 0 then --      , --    . self.field:destroy(self) return end --    self.x = self.x + self.vx * dt self.y = self.y + self.vy * dt --         local screen_width, screen_height = love.graphics.getDimensions() if self.x < 0 then self.x = self.x + screen_width end if self.y < 0 then self.y = self.y + screen_height end if self.x > screen_width then self.x = self.x - screen_width end if self.y > screen_height then self.y = self.y - screen_height end end function Bullet:draw() love.graphics.setColor(255,255,255) --    . -- , ,      love.graphics.circle('fill', self.x, self.y, self.radius) end --   ?   , . Asteroid = {} Asteroid.__index = Asteroid Asteroid.type = 'asteroid' function Asteroid:new(field, x, y, size) self = setmetatable({}, self) --   . --      , --        . self.field = field self.x = x self.y = y --     1-N. self.size = size or 3 --    -   . self.vx = math.random(-20, 20) self.vy = math.random(-20, 20) self.radius = size * 15 --   --    , --       --   .    . --   ,      : self.hp = size + math.random(2) --      . self.color = {math.random(255), math.random(255), math.random(255)} return self end --   ,     function Asteroid:applyDamage(dmg) --     -   dmg = dmg or 1 self.hp = self.hp - 1 if self.hp < 0 then --   -   self.field.score = self.field.score + self.size * 100 self.field:destroy(self) if self.size > 1 then --    . for i = 1, 1 + math.random(3) do self.field:spawn(Asteroid:new(self.field, self.x, self.y, self.size - 1)) end end --    ,  true,     . return true end end --         local function collide(x1, y1, r1, x2, y2, r2) --       : local distance = (x2 - x1) ^ 2 + (y2 - y1) ^ 2 --        -  . --          . local rdist = (r1 + r2) ^ 2 return distance < rdist end function Asteroid:update(dt) self.x = self.x + self.vx * dt self.y = self.y + self.vy * dt --          , --        : for object in pairs(self.field:getObjects()) do --      . if object.type == 'bullet' then if collide(self.x, self.y, self.radius, object.x, object.y, object.radius) then self.field:destroy(object) --    -  true. if self:applyDamage() then --     -    return end end elseif object.type == 'ship' then if collide(self.x, self.y, self.radius, object.x, object.y, object.radius) then --  messagebox   . --   ,     . local head = 'You loose!' local body = 'Score is: '..self.field.score..'\nRetry?' local keys = {"Yea!", "Noo!"} local key_pressed = love.window.showMessageBox(head, body, keys) --     "Noo!": if key_pressed == 2 then love.event.quit() end self.field:init() return end end end --   - ,    ! local screen_width, screen_height = love.graphics.getDimensions() if self.x < 0 then self.x = self.x + screen_width end if self.y < 0 then self.y = self.y + screen_height end if self.x > screen_width then self.x = self.x - screen_width end if self.y > screen_height then self.y = self.y - screen_height end end function Asteroid:draw() --    : love.graphics.setColor(self.color) -- , ,      love.graphics.circle('line', self.x, self.y, self.radius) end -- ,      : Field = {} Field.type = 'Field' --   ,       , --    __index  ,    , --      . --   /  -   . function Field:init() self.score = 0 --       self.objects = {} local ship = Ship:new(self, 100, 200) print(ship) self:spawn(ship) end function Field:spawn(object) --     : --        . self.objects[object] = object end function Field:destroy(object) --   . self.objects[object] = nil end function Field:getObjects() return self.objects end function Field:update(dt) --     ,    . --      . local asteroids_count = 0 for object in pairs(self.objects) do --     if object.update then object:update(dt) end if object.type == 'asteroid' then asteroids_count = asteroids_count + 1 end end if asteroids_count == 0 then for i = 1, 3 do --       local y = math.random(love.graphics.getHeight()) self:spawn(Asteroid:new(self, 0, y, 3)) end end end function Field:draw() for object in pairs(self.objects) do if object.draw then object:draw() end end love.graphics.print('\n Score: '..self.score) end --  :        : function love.load() Field:init() end function love.update(dt) Field:update(dt) end function love.draw() Field:draw() end 

When trying to copy-paste and first run the above sheet, we can get something similar to the classic asteroids.

image

It looks good, but you can do better:

1. Spatial indexing, to speed up the calculation of objects;
2. Better organization of the manager, with key identifiers;
3. All the same, apply inheritance in classes of game objects, inherit them from the “spherical in vacuum” (literally) object having coordinates and radius, etc.

The implementation of these items will remain the homework of those who still decide to unearth the sheet and slightly deepen.

Yes, this material was written for version LÖVE 0.10.2.
For people from the future who will find versions 0.11.X and older: in this source code, you need to correct the color table by changing the values ​​from the range 0-255 to the corresponding proportions 0-1, i.e. eg:

  --   : color = {0, 127, 255} --   -  : color = {0, 0.5, 1} 

PS: I will be happy with feedback and answers to the topic “will the articles about the creation of small toys and / or tools for this framework be valuable”.

Source: https://habr.com/ru/post/346136/


All Articles