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

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