Движение

Как заставить entities двигаться

Использование компаса

Что должно уметь двигаться

Двигаться будут лишь несколько entities, а именно персонажи, их ещё называют actors. Есть и анимации, например, может быть, летящий голубь или другое животное.

Поэтому я хочу, чтобы entity могла двигаться с помощью класса ‘Move’.

Как же?

Раньше, когда я был ещё молод, ха-ха 😉, я двигал такие объекты, например увеличивая или уменьшая координаты x и y с определённой скоростью.

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

Как вы, возможно, уже читали, я использую класс Vector.

Класс Move теперь выглядит так и использует векторные вычисления. Благодаря этому я могу сделать несколько симуляций, например когда игроку нужно идти против ветра (чуть медленнее) или подниматься на холм и спускаться с него.

---@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

Компас

В классе выше есть и часть о направлении, в котором идёт персонаж. Ситуаций будет ‘всего’ четыре: вперёд, назад, влево и вправо.

Если игрок хочет, чтобы персонаж пошёл вправо, я хочу знать об этом сразу. Дело в том, что в этот момент я показываю другой Frameset (walk_right) и запускаю движение.

Я делаю это с помощью компаса, потому что хочу, чтобы он был общей вспомогательной функцией в engine3.

---@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

С помощью универсального класса Compass я могу определить, в какую сторону хочет идти персонаж.

Применение в игре

Теперь в игре я могу использовать универсальные классы из engine3, вот так (часть кода я убрал).

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

С базовым кодом выше теперь можно:

  • Перемещать entity по экрану
  • Задавать скорость для каждой entity
  • Знать, в каком направлении хочет двигаться entity
  • Выполнять свою функцию, когда позиция достигнута
  • Определять, движется ли entity до сих пор
  • Расширять класс ‘Move’ и использовать его где угодно