【问题标题】:Implementing player movement with setInterval使用 setInterval 实现玩家移动
【发布时间】:2018-06-15 09:32:21
【问题描述】:

如果您对问题投了赞成票或反对票,请评论原因。我愿意改变它,但只有你告诉我哪里出了问题,我才能这样做。

我目前正在尝试为我正在编写的类似轰炸机的游戏实现玩家移动。布局和这个很相似:

运动方向优先级
基本上,当您按下其中一个箭头键时,玩家应该开始朝那个方向移动,直到他击中一个方块。 (这已经有效)

但它更复杂。例如,当你按住left,然后又按住up,玩家应该移动up直到他碰到一个方块,然后他应该尝试去left直到他可以再次去up或者碰到一个方块.

所以最后一个键总是具有最高优先级,而前一个键具有第二高优先级,依此类推。 (我已经编写了代码来跟踪优先级,但是由于以下问题,它不起作用。)

浮点位置

玩家不会简单地从场地 (0|0) 移动到场地 (0|1)。玩家在变量中配置了固定速度(默认为每秒 1 个字段),并且他的位置将每 10 毫秒更新一次。滞后可能会导致位置更新延迟数秒。

所以位置几乎从来不是一个整数,几乎总是一个浮点数。

碰撞问题

玩家的宽度和高度与游戏中的所有其他元素完全相同。 这意味着,为了从 (0|2.0001312033) 到 (1|2),您首先必须准确地到达 (0|2),这样玩家才不会与 (1|1) 上的块发生碰撞并且(1|3),然后你才能真正通过到达(1|2)。

问题在于玩家几乎从未真正到达过如此理想的整数位置,因为该位置仅每约 10 毫秒更新一次。

字段跳过

另一个明显的问题是,如果延迟导致位置更新延迟一秒,玩家可能会跳过一个字段,从而导致玩家跳过墙壁、物品和爆炸。


总结 如果自上次位置更新后玩家移动了多个区域,他将跳过一个区域,并且几乎不可能绕过拐角,因为玩家必须处于完美的位置才能在转弯时不与任何块发生碰撞。

我只是想不出一种解决这些问题的好方法,而不会创建大量不可读的代码。我真的很想保持它的整洁,并且当我将来再次查看它时能够理解代码。

是否有可以提供帮助的游戏动作库?还有其他想法吗?

我目前的代码

这些是我当前代码的关键部分。我试图删除所有不相关的部分。

"use strict";
class Player {
    constructor(gameField, pos) { // gameField is an object containing lots of methods and properties like the game field size and colision detection fucntions
        // this._accuratePos is the floating point position
        this._accuratePos = JSON.parse(JSON.stringify(pos));
        // this.pos is the integer posision 
        this.pos = JSON.parse(JSON.stringify(pos));
        // this.moveSpeed is the movement speed of the player
        this.moveSpeed = 3;
        // this.activeMoveActions will contain the currently pressed arrow keys, sorted by priority. (last pressed, highest prio)
        this.activeMoveActions = []
        // this.moveInterval will contain an interval responsible for updating the player position
        this.moveInterval;
    }

    // directionKey can be 'up', 'down', 'left' or 'right'
    // newState can be true if the key is pressed down or false if it has been released.
    moveAction(directionKey, newState=true) { // called when a key is pressed or released. e.g. moveAction('left', false) // left key released
        if (this.activeMoveActions.includes(directionKey)) // remove the key from the activeMoveActions array
            this.activeMoveActions = this.activeMoveActions.filter(current => current !== directionKey);
        if (newState) // if the key was pressed down
            this.activeMoveActions.unshift(directionKey); // push it to the first position of the array

        if (this.activeMoveActions.length === 0) { // if no direction key is pressed
            if (this.moveInterval) { // if there still is a moveInterval
                clearInterval(this.moveInterval); // remove the moveInterval
            return; // exit the function
        }

        let lastMoveTime = Date.now(); // store the current millisecond time in lastMoveTime
        let lastAccPos = JSON.parse(JSON.stringify(this.accuratePos)); // store a copy of this.accuratePos in lastAccPos

        this.moveInterval = setInterval(()=>{
            let now = Date.now(); // current time in milliseconds
            let timePassed = now-lastMoveTime; // time passed since the last interval iteration in milliseconds
            let speed = (this.moveSpeed*1)/1000; // the movement speed in fields per millisecond
            let maxDistanceMoved = timePassed*speed; // the maximum distance the player could have moved (limited by hitting a wall etc)
            // TODO: check if distance moved > 1 and if so check if user palyer went through blocks

            let direction = this.activeMoveActions[0]; // highest priority direction
            // this.activeMoveActions[1] would contain the second highest priority direction if this.activeMoveActions.length > 1

            let newAccPos = JSON.parse(JSON.stringify(lastAccPos)); // store a copy of lastAccPos in newAccPos
            // (newAccPos will not necessarily become the new player posision. only if it's a valid position.)
            if (direction === 'up') { // if the player pressed the arrow up key
                newAccPos.y -= maxDistanceMoved; // subtract the maxDistanceMoved from newAccPos.y 
            } else if (direction === 'down') {
                newAccPos.y += maxDistanceMoved;
            } else if (direction === 'left') {
                newAccPos.x -= maxDistanceMoved;
            } else if (direction === 'right') {
                newAccPos.x += maxDistanceMoved;
            }

            // if it is possible to move the plyer to the new position in stored in newAccPos 
            if (!this.gameField.posIntersectsMoveBlockingElement(newAccPos) && this.gameField.posIsOnField(newAccPos)) {
                this.accuratePos = JSON.parse(JSON.stringify(newAccPos)); // set the new player position to a copy of the modified newAccPos
            } else { // if the newly calculated position is not a possible position for the player
                this.accuratePos = JSON.parse(JSON.stringify(this.pos)); // overwrite the players accurate position with a copy of the last rounded position
            }

            realityCheck(); // handle colliding items and explosions
            lastMoveTime = now; // store the time recorded in the beginning of the interval function
            lastAccPos = JSON.parse(JSON.stringify(newAccPos)); // store a copy of the new position in lastAccPos
        }, 10); // run this function every 10 milliseconds
    }
    set accuratePos(newAccPos) {
        let newPos = { // convert to rounded position
            x: Math.round(newAccPos.x),
            y: Math.round(newAccPos.y)
        };
        if (this.gameField.posIsOnField(newPos)) { // if the posision is on the game field
            this._accuratePos = JSON.parse(JSON.stringify(newAccPos));
            this.pos = newPos; 
        }
    }
    get accuratePos() {
        return this._accuratePos;
    }
    realityCheck() { 
        // ignore this method, it simply checks if the current position collides with an items or an explosion
    }

}

【问题讨论】:

  • 不要使用setinterval 移动它不可靠且与按键不同步。每个keypress 都应该触发一个事件,即使被按住。您只需要回复每个keypress 事件
  • 是的,但是按住一个键应该不断移动玩家。没有办法绕过间隔。
  • 不,当按住一个键时,它会一遍又一遍地触发按键事件,因此通过使用该事件,您只需要担心响应单个按键事件。检查 keyCode 并相应移动。
  • 但不是以恒定的速率。请注意,我正在服务器端实现这一点。当您可以简单地发送一个按键事件来代替时,向服务器发送按键事件垃圾邮件将是非常浪费的。
  • 在模拟更新步骤中跟踪按钮状态并更新玩家位置。为了跟踪按钮状态,只需记录对该状态的更改。

标签: javascript game-engine 2d-games pixi.js topdown


【解决方案1】:

我认为您需要为您的游戏设置适当的架构,并简单地一一解决问题。

首先也是最重要的:介绍游戏循环http://gameprogrammingpatterns.com/game-loop.html。不要在游戏代码中发出任何“setIntervals”。对于游戏中的每一帧,请执行以下操作:

  • 读取控制器状态

  • 向底层游戏对象发出命令

  • 在所有游戏对象上调用 update(timeMillis) 以执行命令

作为游戏循环实现的一部分,您可能希望解决“滞后一秒”的问题。例如,通过将最小 timeMillis 设置为 100 毫秒(即,如果游戏低于 10 FPS,游戏本身就会变慢)。还有许多其他选项,只需阅读各种游戏循环方法即可。

然后攻击Controller实现。实现单独的定义良好的控制器对象,可能类似于(TypeScript):

class Controller {
  public update(timeMillis: number);
  public getPrimaryDirection(): Vector;
  public getSecondaryDirection(): Vector;
}

使用 console.log 在游戏循环中对其进行测试,然后将其放在一边。

然后专注于 LevelGrid 对象和您的碰撞问题。有很多方法可以解决您提到的碰撞和导航问题。以下是一些可能的解决方案:

  • 简单的“固定大小步长”方法。选择小的固定大小增量(例如 0.1 像素或 1 像素)。在你的游戏循环中创建一个子循环,它将以固定的步长将玩家移动到正确的方向,而 LevelGrid.canMoveTo(x, y, w, h) 返回 true。从剩余时间增量中减去 0.1 个像素所需的时间。当剩余时间增量小于零时,退出子循环。在实现 LevelGrid.canMoveTo 时正确检查“epsilon”距离,以免浮点不精确对您造成伤害。

  • “物理铸造”方法。制作 LevelGrid.castRectangle(x, y, w, h, dx, dy) 函数。它将计算以下内容:如果具有给定大小的矩形沿给定方向移动,它究竟会在哪里撞到第一面墙?您可以在许多物理库或游戏引擎中找到实现。

我建议你考虑选择好的JS游戏引擎,它会为你提供架构。

【讨论】:

  • 我不能让游戏运行得更慢。这是一个多人游戏,我提供的代码在服务器上运行。另外,我决定只在有任何变化时才绘制框架,否则我会浪费很多资源。由于我的游戏是多人游戏,所以我的游戏逻辑在 node.js 服务器上运行。在客户端,我使用 pixi.js 来渲染所有内容。我能找到的游戏库预计只能在客户端使用,因此我无法使用它们。
  • 1) 固定大小的步长可以在游戏循环中的服务器上正常工作。 PIXI 客户端只会按照自己的节奏渲染它可以赶上的任何东西,并根据需要进行插值。 2)我绝对建议您不要担心这一步的任何优化。只需在 requestAnimationFrame 中每秒重绘屏幕 60 次。当您向游戏屏幕添加动画时,几乎每一帧都会发生变化。
  • 啊——还有一件事。对原始游戏(如 Dyna Blaster)中的动作进行更多实验。他们所做的是将您的垂直位置“环绕”到中心线,如果您在给定的阈值内(例如,20 像素),请按下水平移动按钮并且在给定方向上没有墙壁。这确实解决了所有准确性问题。
【解决方案2】:

使用链接地图单元格。

与链接列表一样,链接地图使导航复杂的迷宫变得容易。

墙壁碰撞问题(包括跳墙)以及您列出的所有其他问题都不是问题,因为它们根本不会发生。

细胞。

首先,您定义每个地图单元格来保存单元格的位置,xy

对于每个玩家可能占据的空闲单元格,该单元格包含对玩家可以按名称移动到的单元格的引用,左、右、上、下。

因此一个单元格可能看起来像

cell = {
    x,y, // the cell pos
    left : null, // left is blocked
    right : cell, // a cell to the right that can be moved to 
    up    : cell, // a cell above that can be moved to 
    down  : cell, // a cell below that can be moved to 
}

左边的单元格也必须指向右边的单元格。因此cell.left.right === cell。或者左右两步cell.left.left.right.right === cell

播放器

玩家持有它下面的单元格(总是直接从单元格开始)player.cell = ?

很容易检查玩家是否可以朝某个方向移动。假设按下了左控件,只需检查if(keyboard.left && player.cell.left) { //can move left。并且要移动的位置存储在单元格中

由于您不希望玩家立即移动到下一个单元格,因此您保存该单元格并使用计数器开始移动到新单元格。计数器值从 0 到 1,0 表示当前单元格,1 表示下一个单元格。

当计数达到1时,将播放器下的单元格设置为下一个单元格,并将计数器设置为0,并重复该过程。检查输入,然后在该方向检查单元格,如果是,则移动。

如果玩家反转方向,只需将计数器减少到零。

优先考虑移动方向

通过跟踪最后移动的方向,您可以使用数组来确定下一个移动的优先级。

const checkMoveOrder = {
   left : ["up","down","right","left"],
   right : ["up","down","left","right"],
   up : ["left","right","down","up"],
   down : ["left","right","up","down"],
}

因此,如果玩家最后一步是离开的,那么在下一个单元格中,按键会按上、下、右、左的顺序进行检查。如果按下侧方向键,玩家将始终转动。

更多好处。

  • 完全消除了玩家地图碰撞测试的需要。

  • 解决玩家对齐问题,因为您确保始终有一个对齐的单元格以及您从中移动的距离

  • 使及时预测球员位置变得非常容易。只需跟随播放器下方单元格中的单元格即可。

  • 除了玩家移动外,它还有助于非玩家角色,大大简化了寻路解决方案。

  • 您可以使用固定时间步长或随机时间步长,只要您只从一个单元格到另一个单元格。

  • 玩家永远不会被卡住,因为你无法移动到没有出路的地方。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-02-02
    • 2023-02-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-21
    相关资源
    最近更新 更多