пятница, 20 февраля 2015 г.

Top-down shooter with LÖVE

Эта же запись в русском переводе.
Tutorial how to create simple top-down shooter with LÖVE framework. Top-down camera (view) used in GTA 1,2, Crimsonland, Hotline Miami, etc.

Tutorial covers:
  • physics and collisions
  • class-like tables in Lua & LÖVE (will used for creating enemies and bullets)
  • Anim8 LÖVE-library for easy animations creating
  • and, of course, top-down shooter creating :)
Required:

If you don't familiarity with LÖVE then read Getting started with LÖVE first.

Create main.lua

-------------------------------------------------------------------------------
-- Main callbacks 
-------------------------------------------------------------------------------
function love.load()
    --
end

function love.update(dt)
    --
end

function love.draw()
    --
end

and conf.lua
function love.conf(t)
    t.window.width = 1024
    t.window.height = 768
end

Add physic world to main.lua

-------------------------------------------------------------------------------
-- Main callbacks 
-------------------------------------------------------------------------------
function love.load()
 -- Create world and set callbacks for it
 world = love.physics.newWorld(0, 0, true)
 world:setCallbacks(beginContact, endContact, preSolve, postSolve)
end

function love.update(dt)
 -- Update world
 world:update(dt)
end

function love.draw()
    -- 
end

-------------------------------------------------------------------------------
-- Physics world callbacks
-------------------------------------------------------------------------------
function beginContact(a, b, coll)
    --
end

function endContact(a, b, coll)
    --
end

function preSolve(a, b, coll)
    --
end

function postSolve(a, b, coll, normalimpulse1, tangentimpulse1, normalimpulse2, tangentimpulse2)
    --
end

Create player in player.lua. There nothing difficult. Just read code comments attentively.

player = {}

-- Create player
function player.create()
 -- Sprite with player
 player.img = love.graphics.newImage("player.png")

 -- Coordinates X & Y - center of screen
 local x, y = love.graphics.getDimensions()
 x = x / 2
 y = y /2

 -- Shape & body
 player.shape = love.physics.newCircleShape(24)  -- circle with radius 24
 player.body = love.physics.newBody(world, x, y, "kinematic")

 -- Fixture shape with body and set mass 5
 player.fix = love.physics.newFixture(player.body, player.shape, 5)
end

-- Draw player, this function will be called from love.draw()
function player.draw()
 -- Local image point 0,0 is in left-top corner, 
 -- that mean it's need to move image left to 1/4 width and move down to 1/2 
 -- height. In this way center of human on image will be at center of player 
 -- body. 
 local draw_x, draw_y = player.body:getWorldPoint(-21.25, -26.5)
 -- When drawing angle image like body's angle
 love.graphics.draw(player.img, draw_x, draw_y, player.body:getAngle())
end

-- Update player, function will be called from love.update(dt)
function player.update(dt)
 
 -- Local vars for easy access to functions
 local is_down_kb = love.keyboard.isDown
 local is_down_m = love.mouse.isDown

 -- Get current player's position
 local x, y = player.body:getPosition()

 -- Moving
 -- Because local center of player placed in center of player's body,
 -- limit for moving by X are (0 + player's width/2) and 
 -- (screen's width - player's width / 2)
 if is_down_kb("a") and (x > 26) then
  x = x - 100*dt  -- left
 elseif is_down_kb("d") and (x < love.graphics.getWidth() - 26) then
  x = x + 100*dt  -- right
 end

 if is_down_kb("w") and (y > 42) then
  y = y - 100*dt  -- up
 elseif is_down_kb("s") and (y < love.graphics.getHeight() - 42) then
  y = y + 100*dt  -- down
 end

 -- Update position
 player.body:setPosition(x, y)

 -- Angle player to mouse cursor position
 local direction = math.atan2(love.mouse.getY() - y, love.mouse.getX() - x)
 player.body:setAngle(direction)

 ---------------------------------------
 -- Later here will be added shooting --
 ---------------------------------------
end

And update main.lua: add require("player") to file beginning, player.create() to love.load(), player.update(dt) to love.update() and player.draw() to love.draw().

require("player")

-------------------------------------------------------------------------------
-- Main callbacks 
-------------------------------------------------------------------------------
function love.load()
 -- Create world and set callbacks for it
 world = love.physics.newWorld(0, 0, true)
 world:setCallbacks(beginContact, endContact, preSolve, postSolve)

 -- Create player
 player.create()
end

function love.update(dt)
 -- Update world
 world:update(dt)

 -- Update player
 player.update(dt)
end

function love.draw()
 -- Draw player
 player.draw()
end

-------------------------------------------------------------------------------
-- Physics world callbacks
-------------------------------------------------------------------------------
function beginContact(a, b, coll)
    --
end

function endContact(a, b, coll)
    --
end

function preSolve(a, b, coll)
    --
end

function postSolve(a, b, coll, normalimpulse1, tangentimpulse1, normalimpulse2, tangentimpulse2)
    --
end

Run game

By W S A D pressing player moves and don't left screen borders. Player always angle to mouse position.

Next step - shooting.
On shooting must creating new object - bullet. Easiest way for creating multiple similar objects as I think is using class-like tables.
I thinks it is possible to found few different ways how to create something class-like in Lua. I prefer this way.

Add to beginning of player.lua new table for bullets as next line by player's table.

player = {}
bullets = {}

Now create bullet's class in player.lua
-- Bullet
BulletClass = {}
-- At object creating it got attributes x & y with position and attributes
-- lx & ly with coordinates for vector of bullet's moving direction 
function BulletClass:new(x, y, lx, ly)
 local new_obj = {x = x, y = y, lx = lx, ly = ly}
 self.__index = self
 return setmetatable(new_obj, self) 
end

-- In this method creates body, shape and fixture
function BulletClass:create()
 -- Body
 self.body = love.physics.newBody(world, self.x, self.y, "dynamic")
 self.body:setBullet(true)  -- mark that is's bullet

 -- Shape
 self.shape = love.physics.newCircleShape(5)

 -- Fixture body with shape and set mass 0.1 
 self.fix = love.physics.newFixture(self.body, self.shape, 0.1)
 -- Set fixture's user data "bullet"
 self.fix:setUserData("bullet")

 -- Start bullet's moving by setting it's linear velocity
 self.body:setLinearVelocity(self.lx, self.ly)
end

-- This method will be called from love.update()
function BulletClass:update(dt)

 -- Bullet position
 local x, y = self.body:getPosition()

 -- If bullet leave screen or collides with other body, delete it

 -- Because bullet's local point 0,0 in center of body, in 0,0 point half of 
 -- bullet will be still on screen. Bullet fully leave screen
 -- at -(bullet's radius) or (screen width/height + bullet's radius) point,
 -- bullet's radius is 5. 
 if x < - 5 or x > (love.graphics.getWidth() + 5) or 
  y < -5 or y > (love.graphics.getHeight() + 5) or
  not self.fix:getUserData() then
   self:destroy()
 end

end

-- This method will be called from love.draw()
function BulletClass:draw()
 -- Draw filled circle
 love.graphics.circle("fill", self.body:getX(), self.body:getY(), 
                         self.shape:getRadius())
end

-- Destroy bullet
function BulletClass:destroy()
 -- Make object = nil will destroy object
 -- Using "for" loop with step = 1, because it's work faster then ipairs
 for i = 1, #bullets, 1 do
  if self == bullets[i] then
   bullets[i] = nil
  end
 end
end

Add shoot function to player.lua (for example, before player.update(dt)).
-- Shooting
function player.shoot()
 -- Man on player sprite angle to right. 
 -- Get point that will be little right and below man's center
 local x, y = player.body:getWorldPoint(65, 5)

 -- Bullet's direction vector coordinates
 local lx, ly = player.body:getWorldVector(400, 0)

 -- Index for new bullet in bullets table
 local i = #bullets + 1

 -- Create bullet
 bullets[i] = BulletClass:new(x, y, lx, ly)
 bullets[i]:create()
end

Add this to end of player.create()
 -- This time will be used for delay between shots 
 player.shoot_time = love.timer.getTime()

And add this to end of player.update()
 -- Shooting
 if is_down_m("l") then
  -- If last show was second ago or more then shoot
  if math.floor(love.timer.getTime() - player.shoot_time) >= 1 then
   player.shoot()
   player.shoot_time = love.timer.getTime()
  end
 end

Now player.lua look

player = {}
bullets = {}

-- Create player
function player.create()
 -- Sprite with player
 player.img = love.graphics.newImage("player.png")

 -- Coordinates X & Y - center of screen
 local x, y = love.graphics.getDimensions()
 x = x / 2
 y = y /2

 -- Shape & body
 player.shape = love.physics.newCircleShape(24)  -- circle with radius 24
 player.body = love.physics.newBody(world, x, y, "kinematic")

 -- Fixture shape with body and set mass 5
 player.fix = love.physics.newFixture(player.body, player.shape, 5)

 -- This time will be used for delay between shots 
 player.shoot_time = love.timer.getTime()
end

-- Draw player, this function will be called from love.draw()
function player.draw()
 -- Local image point 0,0 is in left-top corner, 
 -- that mean it's need to move image left to 1/4 width and move down to 1/2 
 -- height. In this way center of human on image will be at center of player 
 -- body. 
 local draw_x, draw_y = player.body:getWorldPoint(-21.25, -26.5)
 -- When drawing angle image like body's angle
 love.graphics.draw(player.img, draw_x, draw_y, player.body:getAngle())
end

-- Shooting
function player.shoot()
 -- Man on player sprite angle to right. 
 -- Get point that will be little right and below man's center
 local x, y = player.body:getWorldPoint(65, 5)

 -- Bullet's direction vector coordinates
 local lx, ly = player.body:getWorldVector(400, 0)

 -- Index for new bullet in bullets table
 local i = #bullets + 1

 -- Create bullet
 bullets[i] = BulletClass:new(x, y, lx, ly)
 bullets[i]:create()
end

-- Update player, function will be called from love.update(dt)
function player.update(dt)
 
 -- Local vars for easy access to functions
 local is_down_kb = love.keyboard.isDown
 local is_down_m = love.mouse.isDown

 -- Get current player's position
 local x, y = player.body:getPosition()

 -- Moving
 -- Because local center of player placed in center of player's body,
 -- limit for moving by X are (0 + player's width/2) and 
 -- (screen's width - player's width / 2)
 if is_down_kb("a") and (x > 26) then
  x = x - 100*dt  -- left
 elseif is_down_kb("d") and (x < love.graphics.getWidth() - 26) then
  x = x + 100*dt  -- right
 end

 if is_down_kb("w") and (y > 42) then
  y = y - 100*dt  -- up
 elseif is_down_kb("s") and (y < love.graphics.getHeight() - 42) then
  y = y + 100*dt  -- down
 end

 -- Update position
 player.body:setPosition(x, y)

 -- Angle player to mouse cursor position
 local direction = math.atan2(love.mouse.getY() - y, love.mouse.getX() - x)
 player.body:setAngle(direction)

 -- Shooting
 if is_down_m("l") then
  -- If last show was second ago or more then shoot
  if math.floor(love.timer.getTime() - player.shoot_time) >= 1 then
   player.shoot()
   player.shoot_time = love.timer.getTime()
  end
 end
end

-- Bullet
BulletClass = {}
-- At object creating it got attributes x & y with position and attributes
-- lx & ly with coordinates for vector of bullet's moving direction 
function BulletClass:new(x, y, lx, ly)
 local new_obj = {x = x, y = y, lx = lx, ly = ly}
 self.__index = self
 return setmetatable(new_obj, self) 
end

-- In this method creates body, shape and fixture
function BulletClass:create()
 -- Body
 self.body = love.physics.newBody(world, self.x, self.y, "dynamic")
 self.body:setBullet(true)  -- mark that is's bullet

 -- Shape
 self.shape = love.physics.newCircleShape(5)

 -- Fixture body with shape and set mass 0.1 
 self.fix = love.physics.newFixture(self.body, self.shape, 0.1)
 -- Set fixture's user data "bullet"
 self.fix:setUserData("bullet")

 -- Start bullet's moving by setting it's linear velocity
 self.body:setLinearVelocity(self.lx, self.ly)
end

-- This method will be called from love.update()
function BulletClass:update(dt)

 -- Bullet position
 local x, y = self.body:getPosition()

 -- If bullet leave screen or collides with other body, delete it

 -- Because bullet's local point 0,0 in center of body, in 0,0 point half of 
 -- bullet will be still on screen. Bullet fully leave screen
 -- at -(bullet's radius) or (screen width/height + bullet's radius) point,
 -- bullet's radius is 5. 
 if x < - 5 or x > (love.graphics.getWidth() + 5) or 
  y < -5 or y > (love.graphics.getHeight() + 5) or
  not self.fix:getUserData() then
   self:destroy()
 end

end

-- This method will be called from love.draw()
function BulletClass:draw()
 -- Draw filled circle
 love.graphics.circle("fill", self.body:getX(), self.body:getY(), 
                         self.shape:getRadius())
end

-- Destroy bullet
function BulletClass:destroy()
 -- Make object = nil will destroy object
 -- Using "for" loop with step = 1, because it's work faster then ipairs
 for i = 1, #bullets, 1 do
  if self == bullets[i] then
   bullets[i] = nil
  end
 end
end

And we also need to modify main.lua too

require("player")

-------------------------------------------------------------------------------
-- Main callbacks 
-------------------------------------------------------------------------------
function love.load()
 -- Create world and set callbacks for it
 world = love.physics.newWorld(0, 0, true)
 world:setCallbacks(beginContact, endContact, preSolve, postSolve)

 -- Create player
 player.create()
end

function love.update(dt)
 -- Update world
 world:update(dt)

 -- Update player
 player.update(dt)

 -- Update bullets
 for _, bullet in pairs(bullets) do
  bullet:update(dt)
 end
end

function love.draw()
 -- Draw player
 player.draw()

 -- Draw bullets 
 for _, bullet in pairs(bullets) do
  bullet:draw()
 end
end

-------------------------------------------------------------------------------
-- Physics world callbacks
-------------------------------------------------------------------------------
function beginContact(a, b, coll)
    --
end

function endContact(a, b, coll)
    --
end

function preSolve(a, b, coll)
    --
end

function postSolve(a, b, coll, normalimpulse1, tangentimpulse1, normalimpulse2, tangentimpulse2)
    --
end

Now by left mouse click player will shot with 1 second delay between shots.
Next step - enemies. 
Create new file enemies.lua

-- Import anim8
local anim8 = require("anim8")

-- Load zombie sprite
enemy_sprite = love.graphics.newImage("zombie-animation.png")
-- Create animation grid with frame's width 123, frame's height 80 and 
-- grid's width/height as sprite's width/height
enemy_grid = anim8.newGrid(123, 80, enemy_sprite:getWidth(), 
         enemy_sprite:getHeight())

enemies = {}

-- Enemy class
EnemyClass = {}
function EnemyClass:new()
 local new_obj = {}
 self.__index = self
 return setmetatable(new_obj, self)
end

function EnemyClass:create()
 -- Position
 local x, y

 -- We need to check distance between zomie and player becase
 -- zombie must be created not too close to player
 local near_player = true
 while near_player do
  -- Random coordinates
  x = love.math.random(0, love.graphics.getWidth())
  y = love.math.random(0, love.graphics.getHeight())

  -- Distance between player and zombie by X
  local dist_x = math.abs(player.body:getX() - x)

  -- Distance between player and zombie by Y
  local dist_y = math.abs(player.body:getY() - y)

  -- If distance > 100 by X and Y then quit loop 
  if dist_x > 100 and dist_y > 100 then
   near_player = false
  end
 end

 -- Body, shape, fixture
 self.body = love.physics.newBody(world, x, y, "dynamic")
 self.shape = love.physics.newCircleShape(25)
 self.fix = love.physics.newFixture(self.body, self.shape, 5)
 self.fix:setUserData("enemy")

 -- Self-destroy function. Declared as table element (not as method).
 -- When you create animation you can set which function will be executed
 -- on animation playing end. With method (self:method()) you will get 
 -- an error, but with function as table element it work fine. 
 self.destroy = function ()
  for i = 1, #enemies, 1 do
   if self == enemies[i] then
    enemies[i] = nil
   end
  end
 end

 -- Enemy animation
 -- Moving - set grid and frames from 1 to 6 in 1st row with 
 -- playing speed 0.2
 self.anm_walk = anim8.newAnimation(enemy_grid("1-6", 1), 0.2)

 -- Enemy death - set grid and frames from 1 to 6 in 2nd row with playing
 -- speed 0.2. On animation playing end will be executed self.destroy()
 self.anm_death = anim8.newAnimation(enemy_grid("1-6", 2), 0.2, self.destroy)

 -- Set moving as default animation
 self.anm = self.anm_walk
end

function EnemyClass:draw()
 -- Because we need frame's with body's center in one point 
 -- get coordinates moved above and left  to half of frame width or
 -- height
 local draw_x, draw_y = self.body:getWorldPoint(-40, -40)
 -- Draw animation
 self.anm:draw(enemy_sprite, draw_x, draw_y, self.body:getAngle())
end

function EnemyClass:update(dt)
 -- Position
 local x, y = self.body:getPosition()

 -- Play animation
 self.anm:update(dt)

 -- If fixture's userData is false, 
 -- enemy was killed, show death animation.
 if not self.fix:getUserData() then
  self.anm = self.anm_death
 -- Else move enemy to player
 else
  -- X coordinate
  -- Coordinates are float (not integer), we need to floor (round)
  -- it, else enemy can do "convulsice tweeches" because to the 
  -- difference in tenths
  if math.floor(player.body:getX() - x) ~= 0 then
   -- If difference < 0 than player to the left of enemy
   -- Move enemy to left
   if (player.body:getX() - x) < 0 then
    x = x - 20*dt
   -- Else move enemy to right
   else
    x = x + 20*dt
   end
  end

  -- Y coordinate
  if math.floor(player.body:getY() - y) ~= 0 then
   -- If difference < 0 than, move enemy to top
   if (player.body:getY() - y) < 0 then
    y = y - 20*dt
   -- Else move enemy to bottom
   else
    y = y + 20*dt
   end
  end

  -- Angle enemy to player
  local direction = math.atan2(player.body:getY() - y, 
          player.body:getX() - x)
  self.body:setAngle(direction)
  -- Update enemy position
  self.body:setPosition(x, y)
 end
end

Update main.lua. Create new enemy per 5 seconds.

require("player")
require("enemies")

-------------------------------------------------------------------------------
-- Main callbacks 
-------------------------------------------------------------------------------
function love.load()
 -- Create world and set callbacks for it
 world = love.physics.newWorld(0, 0, true)
 world:setCallbacks(beginContact, endContact, preSolve, postSolve)

 -- Create player
 player.create()

 -- Time for calculating delay betweeb enemies creation
 -- We will create new enemy per 5 seconds
 time = love.timer.getTime()
end

function love.update(dt)
 -- Update world
 world:update(dt)

 -- Update player
 player.update(dt)

 -- Create new enemy per 5 seconds
 if math.floor(love.timer.getTime() - time) >= 5 then
  -- Add enemy to enimes table with index = table length + 1
  enemies[#enemies + 1] = EnemyClass:new()
  -- Once enemy added to table he has last index in table = table length
  enemies[#enemies]:create()
  -- Update time for calculation new 5 seconds delay
  time = love.timer.getTime()
 end

 -- Update enemies
 for _, enemy in pairs(enemies) do
  enemy:update(dt)
 end

 -- Update bullets
 for _, bullet in pairs(bullets) do
  bullet:update(dt)
 end
end

function love.draw()
 -- Draw player
 player.draw()

 -- Draw enemies
 for _, enemy in pairs(enemies) do
  enemy:draw()
 end
 -- Draw bullets 
 for _, bullet in pairs(bullets) do
  bullet:draw()
 end
end

-------------------------------------------------------------------------------
-- Physics world callbacks
-------------------------------------------------------------------------------
function beginContact(a, b, coll)
    --
end

function endContact(a, b, coll)
    --
end

function preSolve(a, b, coll)
    --
end

function postSolve(a, b, coll, normalimpulse1, tangentimpulse1, normalimpulse2, tangentimpulse2)
    --
end

Enemy creates per 5 seconds and going to player.

Now add collisions processing between bullets and enemies. If bullet collides with enemy, then destroy bullet, play death animation and destroy enemy. And add score of enemies killed by player.
Collision processing add to beginContact callback. For score just declare new variable in love.load() and draw it in love.draw().

Update main.lua

require("player")
require("enemies")

-------------------------------------------------------------------------------
-- Main callbacks 
-------------------------------------------------------------------------------
function love.load()
 -- Create world and set callbacks for it
 world = love.physics.newWorld(0, 0, true)
 world:setCallbacks(beginContact, endContact, preSolve, postSolve)

 -- Create player
 player.create()

 -- Time for calculating delay betweeb enemies creation
 -- We will create new enemy per 5 seconds
 time = love.timer.getTime()

 -- Killed enemies
 killed = 0
end

function love.update(dt)
 -- Update world
 world:update(dt)

 -- Update player
 player.update(dt)

 -- Create new enemy per 5 seconds
 if math.floor(love.timer.getTime() - time) >= 5 then
  -- Add enemy to enimes table with index = table length + 1
  enemies[#enemies + 1] = EnemyClass:new()
  -- Once enemy added to table he has last index in table = table length
  enemies[#enemies]:create()
  -- Update time for calculation new 5 seconds delay
  time = love.timer.getTime()
 end

 -- Update enemies
 for _, enemy in pairs(enemies) do
  enemy:update(dt)
 end

 -- Update bullets
 for _, bullet in pairs(bullets) do
  bullet:update(dt)
 end
end

function love.draw()
 -- Draw player
 player.draw()

 -- Draw enemies
 for _, enemy in pairs(enemies) do
  enemy:draw()
 end
 -- Draw bullets 
 for _, bullet in pairs(bullets) do
  bullet:draw()
 end

 -- Print how much enemies were killed
 love.graphics.print("Killed: "..killed, 30, 30)
end

-------------------------------------------------------------------------------
-- Physics world callbacks
-------------------------------------------------------------------------------
function beginContact(a, b, coll)

 local enemy, bullet

 -- Check what objects colliding
 if a:getUserData() == "enemy" then
  enemy = a
 elseif b:getUserData() == "enemy" then
  enemy = b
 end

 if a:getUserData() == "bullet" then
  bullet = a
 elseif b:getUserData() == "bullet" then
  bullet = b
 end

 -- If enemy collides with bullet
 if bullet and enemy then
  -- Set false to both userDatas. That will execute self:destroy() in
  -- bullet object and set animation to self.anm_death in enemy object, 
  -- after animation playing end will executed self.destroy() for enemy
  bullet:setUserData(false)
  enemy:setUserData(false)
  killed = killed + 1
 end
 -- Else one of variables will nil and colliding will not processed
end

function endContact(a, b, coll)
    --
end

function preSolve(a, b, coll)
    --
end

function postSolve(a, b, coll, normalimpulse1, tangentimpulse1, normalimpulse2, tangentimpulse2)
    --
end

Result
If bullet hit enemy, enemy dying and plays death animation, after that enemy hides from screen (he destroys, bullet destroys too).

I used only one world's callback - beginContact. In this sample it's enough. But, of course, in more complex projects need to be used other world callbacks too.

Full source code (download love archive)
conf.lua

function love.conf(t)
    t.window.width = 1024
    t.window.height = 768
end

main.lua

require("player")
require("enemies")

-------------------------------------------------------------------------------
-- Main callbacks 
-------------------------------------------------------------------------------
function love.load()
 -- Create world and set callbacks for it
 world = love.physics.newWorld(0, 0, true)
 world:setCallbacks(beginContact, endContact, preSolve, postSolve)

 -- Create player
 player.create()

 -- Time for calculating delay betweeb enemies creation
 -- We will create new enemy per 5 seconds
 time = love.timer.getTime()

 -- Killed enemies
 killed = 0
end

function love.update(dt)
 -- Update world
 world:update(dt)

 -- Update player
 player.update(dt)

 -- Create new enemy per 5 seconds
 if math.floor(love.timer.getTime() - time) >= 5 then
  -- Add enemy to enimes table with index = table length + 1
  enemies[#enemies + 1] = EnemyClass:new()
  -- Once enemy added to table he has last index in table = table length
  enemies[#enemies]:create()
  -- Update time for calculation new 5 seconds delay
  time = love.timer.getTime()
 end

 -- Update enemies
 for _, enemy in pairs(enemies) do
  enemy:update(dt)
 end

 -- Update bullets
 for _, bullet in pairs(bullets) do
  bullet:update(dt)
 end
end

function love.draw()
 -- Draw player
 player.draw()

 -- Draw enemies
 for _, enemy in pairs(enemies) do
  enemy:draw()
 end
 -- Draw bullets 
 for _, bullet in pairs(bullets) do
  bullet:draw()
 end

 -- Print how much enemies were killed
 love.graphics.print("Killed: "..killed, 30, 30)
end

-------------------------------------------------------------------------------
-- Physics world callbacks
-------------------------------------------------------------------------------
function beginContact(a, b, coll)

 local enemy, bullet

 -- Check what objects colliding
 if a:getUserData() == "enemy" then
  enemy = a
 elseif b:getUserData() == "enemy" then
  enemy = b
 end

 if a:getUserData() == "bullet" then
  bullet = a
 elseif b:getUserData() == "bullet" then
  bullet = b
 end

 -- If enemy collides with bullet
 if bullet and enemy then
  -- Set false to both userDatas. That will execute self:destroy() in
  -- bullet object and set animation to self.anm_death in enemy object, 
  -- after animation playing end will executed self.destroy() for enemy
  bullet:setUserData(false)
  enemy:setUserData(false)
  killed = killed + 1
 end
 -- Else one of variables will nil and colliding will not processed
end

function endContact(a, b, coll)
    --
end

function preSolve(a, b, coll)
    --
end

function postSolve(a, b, coll, normalimpulse1, tangentimpulse1, normalimpulse2, tangentimpulse2)
    --
end

player.lua

player = {}
bullets = {}

-- Create player
function player.create()
 -- Sprite with player
 player.img = love.graphics.newImage("player.png")

 -- Coordinates X & Y - center of screen
 local x, y = love.graphics.getDimensions()
 x = x / 2
 y = y /2

 -- Shape & body
 player.shape = love.physics.newCircleShape(24)  -- circle with radius 24
 player.body = love.physics.newBody(world, x, y, "kinematic")

 -- Fixture shape with body and set mass 5
 player.fix = love.physics.newFixture(player.body, player.shape, 5)

 -- This time will be used for delay between shots 
 player.shoot_time = love.timer.getTime()
end

-- Draw player, this function will be called from love.draw()
function player.draw()
 -- Local image point 0,0 is in left-top corner, 
 -- that mean it's need to move image left to 1/4 width and move down to 1/2 
 -- height. In this way center of human on image will be at center of player 
 -- body. 
 local draw_x, draw_y = player.body:getWorldPoint(-21.25, -26.5)
 -- When drawing angle image like body's angle
 love.graphics.draw(player.img, draw_x, draw_y, player.body:getAngle())
end

-- Shooting
function player.shoot()
 -- Man on player sprite angle to right. 
 -- Get point that will be little right and below man's center
 local x, y = player.body:getWorldPoint(65, 5)

 -- Bullet's direction vector coordinates
 local lx, ly = player.body:getWorldVector(400, 0)

 -- Index for new bullet in bullets table
 local i = #bullets + 1

 -- Create bullet
 bullets[i] = BulletClass:new(x, y, lx, ly)
 bullets[i]:create()
end

-- Update player, function will be called from love.update(dt)
function player.update(dt)
 
 -- Local vars for easy access to functions
 local is_down_kb = love.keyboard.isDown
 local is_down_m = love.mouse.isDown

 -- Get current player's position
 local x, y = player.body:getPosition()

 -- Moving
 -- Because local center of player placed in center of player's body,
 -- limit for moving by X are (0 + player's width/2) and 
 -- (screen's width - player's width / 2)
 if is_down_kb("a") and (x > 26) then
  x = x - 100*dt  -- left
 elseif is_down_kb("d") and (x < love.graphics.getWidth() - 26) then
  x = x + 100*dt  -- right
 end

 if is_down_kb("w") and (y > 42) then
  y = y - 100*dt  -- up
 elseif is_down_kb("s") and (y < love.graphics.getHeight() - 42) then
  y = y + 100*dt  -- down
 end

 -- Update position
 player.body:setPosition(x, y)

 -- Angle player to mouse cursor position
 local direction = math.atan2(love.mouse.getY() - y, love.mouse.getX() - x)
 player.body:setAngle(direction)

 -- Shooting
 if is_down_m("l") then
  -- If last show was second ago or more then shoot
  if math.floor(love.timer.getTime() - player.shoot_time) >= 1 then
   player.shoot()
   player.shoot_time = love.timer.getTime()
  end
 end
end

-- Bullet
BulletClass = {}
-- At object creating it got attributes x & y with position and attributes
-- lx & ly with coordinates for vector of bullet's moving direction 
function BulletClass:new(x, y, lx, ly)
 local new_obj = {x = x, y = y, lx = lx, ly = ly}
 self.__index = self
 return setmetatable(new_obj, self) 
end

-- In this method creates body, shape and fixture
function BulletClass:create()
 -- Body
 self.body = love.physics.newBody(world, self.x, self.y, "dynamic")
 self.body:setBullet(true)  -- mark that is's bullet

 -- Shape
 self.shape = love.physics.newCircleShape(5)

 -- Fixture body with shape and set mass 0.1 
 self.fix = love.physics.newFixture(self.body, self.shape, 0.1)
 -- Set fixture's user data "bullet"
 self.fix:setUserData("bullet")

 -- Start bullet's moving by setting it's linear velocity
 self.body:setLinearVelocity(self.lx, self.ly)
end

-- This method will be called from love.update()
function BulletClass:update(dt)

 -- Bullet position
 local x, y = self.body:getPosition()

 -- If bullet leave screen or collides with other body, delete it

 -- Because bullet's local point 0,0 in center of body, in 0,0 point half of 
 -- bullet will be still on screen. Bullet fully leave screen
 -- at -(bullet's radius) or (screen width/height + bullet's radius) point,
 -- bullet's radius is 5. 
 if x < - 5 or x > (love.graphics.getWidth() + 5) or 
  y < -5 or y > (love.graphics.getHeight() + 5) or
  not self.fix:getUserData() then
   self:destroy()
 end

end

-- This method will be called from love.draw()
function BulletClass:draw()
 -- Draw filled circle
 love.graphics.circle("fill", self.body:getX(), self.body:getY(), 
                         self.shape:getRadius())
end

-- Destroy bullet
function BulletClass:destroy()
 -- Make object = nil will destroy object
 -- Using "for" loop with step = 1, because it's work faster then ipairs
 for i = 1, #bullets, 1 do
  if self == bullets[i] then
   bullets[i] = nil
  end
 end
end

enemies.lua

-- Import anim8
local anim8 = require("anim8")

-- Load zombie sprite
enemy_sprite = love.graphics.newImage("zombie-animation.png")
-- Create animation grid with frame's width 123, frame's height 80 and 
-- grid's width/height as sprite's width/height
enemy_grid = anim8.newGrid(123, 80, enemy_sprite:getWidth(), 
         enemy_sprite:getHeight())

enemies = {}

-- Enemy class
EnemyClass = {}
function EnemyClass:new()
 local new_obj = {}
 self.__index = self
 return setmetatable(new_obj, self)
end

function EnemyClass:create()
 -- Position
 local x, y

 -- We need to check distance between zomie and player becase
 -- zombie must be created not too close to player
 local near_player = true
 while near_player do
  -- Random coordinates
  x = love.math.random(0, love.graphics.getWidth())
  y = love.math.random(0, love.graphics.getHeight())

  -- Distance between player and zombie by X
  local dist_x = math.abs(player.body:getX() - x)

  -- Distance between player and zombie by Y
  local dist_y = math.abs(player.body:getY() - y)

  -- If distance > 100 by X and Y then quit loop 
  if dist_x > 100 and dist_y > 100 then
   near_player = false
  end
 end

 -- Body, shape, fixture
 self.body = love.physics.newBody(world, x, y, "dynamic")
 self.shape = love.physics.newCircleShape(25)
 self.fix = love.physics.newFixture(self.body, self.shape, 5)
 self.fix:setUserData("enemy")

 -- Self-destroy function. Declared as table element (not as method).
 -- When you create animation you can set which function will be executed
 -- on animation playing end. With method (self:method()) you will get 
 -- an error, but with function as table element it work fine. 
 self.destroy = function ()
  for i = 1, #enemies, 1 do
   if self == enemies[i] then
    enemies[i] = nil
   end
  end
 end

 -- Enemy animation
 -- Moving - set grid and frames from 1 to 6 in 1st row with 
 -- playing speed 0.2
 self.anm_walk = anim8.newAnimation(enemy_grid("1-6", 1), 0.2)

 -- Enemy death - set grid and frames from 1 to 6 in 2nd row with playing
 -- speed 0.2. On animation playing end will be executed self.destroy()
 self.anm_death = anim8.newAnimation(enemy_grid("1-6", 2), 0.2, self.destroy)

 -- Set moving as default animation
 self.anm = self.anm_walk
end

function EnemyClass:draw()
 -- Because we need frame's with body's center in one point 
 -- get coordinates moved above and left  to half of frame width or
 -- height
 local draw_x, draw_y = self.body:getWorldPoint(-40, -40)
 -- Draw animation
 self.anm:draw(enemy_sprite, draw_x, draw_y, self.body:getAngle())
end

function EnemyClass:update(dt)
 -- Position
 local x, y = self.body:getPosition()

 -- Play animation
 self.anm:update(dt)

 -- If fixture's userData is false, 
 -- enemy was killed, show death animation.
 if not self.fix:getUserData() then
  self.anm = self.anm_death
 -- Else move enemy to player
 else
  -- X coordinate
  -- Coordinates are float (not integer), we need to floor (round)
  -- it, else enemy can do "convulsice tweeches" because to the 
  -- difference in tenths
  if math.floor(player.body:getX() - x) ~= 0 then
   -- If difference < 0 than player to the left of enemy
   -- Move enemy to left
   if (player.body:getX() - x) < 0 then
    x = x - 20*dt
   -- Else move enemy to right
   else
    x = x + 20*dt
   end
  end

  -- Y coordinate
  if math.floor(player.body:getY() - y) ~= 0 then
   -- If difference < 0 than, move enemy to top
   if (player.body:getY() - y) < 0 then
    y = y - 20*dt
   -- Else move enemy to bottom
   else
    y = y + 20*dt
   end
  end

  -- Angle enemy to player
  local direction = math.atan2(player.body:getY() - y, 
          player.body:getX() - x)
  self.body:setAngle(direction)
  -- Update enemy position
  self.body:setPosition(x, y)
 end
end


I hope this tutorial will be helpful for someone in LÖVE learning. 
Feedback are welcome. Feel free to contact with me in entry comments.


1 комментарий: