Bewegung

Entities in Bewegung bringen

Der Einsatz eines Kompasses

Was muss sich bewegen können

Es gibt nur ein paar Entities, die sich bewegen werden, das sind die Figuren, auch Actors genannt. Und es gibt Animationen, zum Beispiel vielleicht eine fliegende Taube oder ein anderes Tier.

Ich möchte also, dass eine Entity sich bewegen kann, indem sie eine ‘Move’-Klasse verwendet.

Wie denn?

Früher, als ich noch jung war haha 😉, habe ich solche Objekte bewegt, indem ich zum Beispiel die x- und y-Koordinaten mit einer bestimmten Geschwindigkeit erhöht oder verringert habe.

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

Wie du vielleicht schon gelesen hast, verwende ich eine Vector-Klasse.

Die Move-Klasse sieht jetzt so aus und nutzt Vektorberechnungen. Damit kann ich zum Beispiel ein paar Simulationen umsetzen, etwa wenn ein Spieler gegen den Wind laufen muss (etwas langsamer) oder einen Hügel hinauf oder hinunter.

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

Kompass

In der Klasse oben steht auch ein Teil über die Richtung, in die die Figur läuft. Es wird ’nur’ vier Fälle geben, nämlich nach vorne, nach hinten, nach links und nach rechts.

Wenn der Spieler die Figur nach rechts laufen lassen will, möchte ich das sofort wissen. Der Grund ist, dass ich in dem Moment ein anderes Frameset (walk_right) anzeige und die Bewegung starte.

Das mache ich mit einem Kompass, weil ich ihn als allgemeine Hilfsfunktion in engine3 haben will.

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

Damit kann ich über die generische Compass-Klasse bestimmen, in welche Richtung die Figur laufen will.

Im Spiel anwenden

Im Spiel kann ich jetzt die generischen Klassen aus engine3 verwenden, und zwar so (ich habe etwas Code weggelassen).

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

Mit dem Basiscode oben ist jetzt Folgendes möglich:

  • Eine Entity über den Bildschirm bewegen
  • Die Geschwindigkeit pro Entity einstellen
  • Die Richtung kennen, in die die Entity will
  • Eine eigene Funktion ausführen, wenn die Position erreicht ist
  • Erkennen, ob sich die Entity noch bewegt
  • Die ‘Move’-Klasse erweitern und überall einsetzen