Logo Signal From The Stars

Beweging

Het laten bewegen van entities

Martin avatar
  • Martin
  • 4 min read
Het gebruik van een compas

Wat moet kunnen bewegen

Er zijn maar een paar entities die gaan bewegen, dat zijn de characters of actors genoemd. En er zijn ook animaties zoals mischien een vliegende duif of ander dier.

Wat ik dus wil is dat een entity de mogelijkheid krijgt om te bewegen, door gebruik te maken van een ‘Move’ class.

Hoe dan?

Vroeger toen ik nog jong was haha πŸ˜‰ liet ik dergelijk objecten bewegen door bijv. de x en y coordinaten te verhogen/verlagen met een bepaalde snelheid.

-- not 100% wrong but check vector math first
local speed = 1
if go left then
  object.x += speed * dt
end

Zoals je wellicht eerder hebt gelezen maak ik gebruik van een Vector class.

De Move class ziet er nu zo uit en maakt gebruik van Vector berekeningen. Ik kan hierdoor bijv. een aantal simulaties toepassen, zoals bijv. wanneer een speler tegen de wind in moet lopen (iets trager gaan), of een heuvel op of omlaag moet lopen.

---@enum Move.UP Movement up direction.
---@enum Move.DOWN Movement down direction.
---@enum Move.LEFT Movement left direction.
---@enum Move.RIGHT Movement right direction.
Move.IDLE = nil
Move.UP = 1
Move.DOWN = 2
Move.LEFT = 3
Move.RIGHT = 4

...

---@param i integer
function Move:setSpeed(i)
    self.speed = i
end

function Move:getSpeed()
    return self.speed
end

--- Stop traveling to destination.
---@see function.afterStop
---@param ... any Multiple arguments passed into the callback
function Move:stop(...)
    self.vDestination = nil
    self:callback(self.callbackAfterStop, self.pObject, self, ...)
end

--- Move point to destination
---@param dt number DeltaTime
---@return integer? x,integer? y,boolean? stopped
function Move:updateToDestination(dt)
    if self.vDestination == nil then
        return
    end

    local dist = self.vPosition:dist(self.vDestination)
    local step = self.speed * dt
    local stopped = false
    if dist <= step then
        -- at destination
        -- This will stop the movement and set the position x,y
        self.vPosition.x = self.vDestination.x
        self.vPosition.y = self.vDestination.y
        self:stop()
        stopped = true
    else
        -- normalize vector (between the target and sprite)
        local nx = (self.vDestination.x - self.vPosition.x) / dist
        local ny = (self.vDestination.y - self.vPosition.y) / dist

        self.vPosition.x = self.vPosition.x + nx * step
        self.vPosition.y = self.vPosition.y + ny * step
    end

    return self.vPosition.x, self.vPosition.y, stopped
end

---@param x integer
---@param y integer
function Move:setDestinationTarget(x, y)
    self.vDestination = Vector(x, y)
end

function Move:isMoving()
    return self.vDestination and (self.vPosition.x ~= self.vDestination.x or self.vPosition.y ~= self.vDestination.y)
end

Compas

In de bovenstaande class heb ik ook een stukje staan over de richting waar de character naar toe loopt. Er zullen ‘maar’ vier situaties zijn namelijk naar voren, achter, links en rechts.

Indien de speler de character naar rechts wilt laten lopen, wil ik dat direct weten. De rede hiervoor is dat ik dan op dat moment een andere Frameset laat zien (walk_right) en start te movement.

Dit doe ik doormiddel van een compas, aangezien ik het als een algemene helper functie in engine3 wil hebben.

---@param directions table
function Move:getMovementDirection(directions)
    local octant = Compass.getOctant(self.vPosition, self.vDestination, true)

    -- default movement directions.
    -- It will use for up and down 1 extra octant.
    if directions == nil then
        directions = {
            {Compass.N, Compass.NNE, Compass.NW, Compass.NNW, Compass.NE}, -- up
            {Compass.S, Compass.SW, Compass.SE, Compass.SSW, Compass.SSE}, -- down
            {Compass.W, Compass.WSW, Compass.WNW}, -- left
            {Compass.E, Compass.ESE, Compass.ENE} -- right
        }
    end

    for moveConstant, direction in pairs(directions) do
        for _, octantConstant in pairs(direction) do
            if octantConstant == octant then
                return moveConstant
            end
        end
    end

    return Move.IDLE
end

Hiermee kan ik bepalen doormiddel van de generieke Compass class naar welke kant de character wil lopen.

Toepassen in het spel

In het spel kan ik nu vanuit de engine3 de generieke classes gebruiken. Op de volgende manier (ik heb wat code weggelaten).

local Move = require "lib.engine3.src.framework.move.move"
local Compass = require "lib.engine3.src.framework.helper.compass"

function Entity:new(data)

  ...
  self.frameset[Move.LEFT] = self.frameset[Move.RIGHT]
  self.movable = Move(self.activeFrameset:getX(), self.activeFrameset:getY(), self)

  self.movable.callbackAfterStop["default"] = function(s, movable)
      -- Set the latest x,y postition to make it more correct.
      s:setXY(movable.vPosition.x, movable.vPosition.y)
      s.lastMove = s.move
      s:show() -- pause set the frameset to the idle frame
  end
  ...
end

function Entity:walk(targetX, targetY)
  ...
  self.movable:setDestinationTarget(targetX, targetY)

  local oldMove = self.move
  local newMove =
      self.movable:getMovementDirection(
      {
          {Compass.N, Compass.NE, Compass.NW, Compass.NNW, Compass.NNE}, -- up
          {Compass.S, Compass.SW, Compass.SE, Compass.SSW, Compass.SSE}, -- down
          {Compass.W, Compass.WSW, Compass.WNW}, -- left
          {Compass.E, Compass.ESE, Compass.ENE} -- right
      }
  ...

  if oldMove ~= newMove then
      self.activeFrameset:stop()
  else
      setFrameKey = self.activeFrameset:getFrameKey()
  end

  self.activeFrameset = self.frameset[self.move]
  
  ...
end

---@private
---@param self Animation
---@param frameset Frameset
---@param update boolean
---@param currentFrame Frame
---@param dt number
Entity.framesetUpdate = function(self, frameset, update, currentFrame, dt)
  -- local oldX = self.movable.vPosition.x
  -- local oldY = self.movable.vPosition.y
  local x, y = self.movable:updateToDestination(dt)
  if x ~= nil and y ~= nil then
      -- Update each frameset, so no glitch is visible
      self:setXY(x, y)
  end
end

Met de bovenstaande basis code is het nu mogelijk:

  • Een entity over het scherm bewegen
  • Snelheid is per entity in te stellen
  • De richting waar de entity naar toe wil is bekend
  • Wanneer de positie is bereikt kan ik een custom functie uitvoeren
  • Detectie of de entity nog aan het bewegen is
  • De ‘Move’ class is uitbreidbaar en overal inzetbaar