0

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

我目前正在尝试为我正在编写的类似轰炸机的游戏实现玩家运动。布局与此非常相似:
在此处输入图像描述

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

但它更复杂。例如,当你按住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
    }

}
4

2 回答 2

3

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

首先也是最重要的:介绍游戏循环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 游戏引擎,它会为你提供架构。

于 2018-01-10T09:22:02.470 回答
1

使用链接的地图单元格。

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

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

细胞。

首先,您定义每个地图单元格以保存单元格的位置x, y

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

因此一个细胞可能看起来像

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

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

还有很多好处。

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

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

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

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

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

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

于 2018-01-29T22:17:40.473 回答