Chuyển động

Làm cho các entity chuyển động

Dùng la bàn

Cái gì cần di chuyển được

Chỉ có vài entity sẽ di chuyển, đó là các nhân vật, còn gọi là actor. Ngoài ra còn có hoạt ảnh, ví dụ như một con chim bồ câu đang bay hay một con vật khác.

Vì vậy điều tôi muốn là một entity có thể di chuyển bằng cách dùng lớp ‘Move’.

Làm thế nào?

Ngày xưa, khi tôi còn trẻ haha 😉, tôi làm những đối tượng như vậy di chuyển bằng cách, ví dụ, tăng hoặc giảm tọa độ x và y theo một tốc độ nhất định.

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

Như có thể bạn đã đọc trước đây, tôi dùng một lớp Vector.

Lớp Move giờ trông như thế này và dùng các phép tính vector. Nhờ đó tôi có thể áp dụng vài mô phỏng, chẳng hạn khi người chơi phải đi ngược gió (chậm hơn một chút), hoặc đi lên hay đi xuống đồi.

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

La bàn

Trong lớp ở trên cũng có một đoạn về hướng mà nhân vật đang đi. Sẽ ‘chỉ’ có bốn trường hợp: đi tới, đi lùi, sang trái và sang phải.

Nếu người chơi muốn nhân vật đi sang phải, tôi muốn biết ngay lập tức. Lý do là vào lúc đó tôi sẽ hiển thị một Frameset khác (walk_right) và bắt đầu chuyển động.

Tôi làm việc này bằng một la bàn, vì tôi muốn có nó như một hàm trợ giúp chung trong 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

Nhờ lớp chung Compass, tôi có thể xác định nhân vật muốn đi về hướng nào.

Áp dụng trong trò chơi

Trong trò chơi, giờ tôi có thể dùng các lớp chung từ engine3 theo cách sau (tôi đã bỏ bớt một ít mã).

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

Với đoạn mã cơ bản ở trên, giờ đã có thể:

  • Di chuyển một entity trên màn hình
  • Đặt tốc độ riêng cho từng entity
  • Biết hướng mà entity muốn đi
  • Chạy một hàm tùy chỉnh khi đã tới vị trí
  • Phát hiện entity có còn đang di chuyển hay không
  • Mở rộng lớp ‘Move’ và dùng ở mọi nơi