javascript - 使用 setInterval 实现玩家移动

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

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

我目前正在尝试为我正在编写的类似炸弹人的游戏实现玩家移动。布局与此非常相似:
enter image description here

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

但它更复杂。例如,当您按住left时然后还有up ,玩家应该移动up直到他碰到一个方 block ,然后他应该尝试去left直到他可以走up再次或撞到方 block 。

因此,最后一个键始终具有最高优先级,前面的键具有第二优先级,依此类推。 (我已经编写了代码来跟踪优先级,但由于以下问题而无法工作。)

浮点位置

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

所以位置几乎从来不是整数,而几乎总是 float 。

碰撞问题

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

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

字段跳过

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

<小时/>

摘要 如果玩家自上次位置更新以来移动了超过一个字段,他将跳过一个字段并且绕过拐 Angular 几乎是不可能的,因为玩家必须完美定位,以便在转弯时不与任何 block 碰撞。

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

是否有一个游戏 Action 库可以提供帮助?还有其他想法吗?

我的代码

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

"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
    }

}

最佳答案

我认为你需要为你的游戏建立适当的架构,并简单地一一解决问题。

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

  • 读取 Controller 状态

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

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

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

然后攻击 Controller 实现。实现单独的定义良好的 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游戏引擎,它会为你提供架构。

关于javascript - 使用 setInterval 实现玩家移动,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48115830/

相关文章:

javascript - 如何将onclick方法与keydown方法绑定(bind)

c++ - 如何断定一个单位的士兵是否在网格 map 上相连

2019年2D RTS游戏的Java游戏引擎

Java - 将扩展同一类的线程和其他对象放入 ArrayList

reactjs - 将 Phaser 与 React 结合使用

javascript - 将 Try/Catch 与 Promise.all 一起使用

javascript - Node Js - 使用正则表达式向路由添加多个参数

c++ - 使用 Cocos2d-x SEL_CallFunc

java - 哪种设计模式适合可数对象?

javascript - 发送电子邮件函数: error because of formulas in blank rows