【问题标题】:How to limit player's velocity only when it is accelerating?如何仅在加速时限制玩家的速度?
【发布时间】:2020-07-07 23:59:42
【问题描述】:

我正在开发一个使用 Unity 内置物理的 2.5D 太空射击游戏。一切都发生在 2D 空间中,但所有模型都是 3D。

玩家(太空船)可以使用控制器轴旋转,并且可以在按住按钮时加速(例如 xbox 控制器的 A 按钮)。

玩家可以移动的速度有一个限制(maxEngineSpeed),我在 FixedUpdate 中将 RigidBody 的速度限制为如下:

if (rb.velocity.magnitude > maxEngineSpeed)
{
    rb.velocity = Vector2.ClampMagnitude(rb.velocity, maxEngineSpeed);
}

现在的问题是,这会阻止速度达到高于 maxEngineSpeed 的值。

我想要一种仅在玩家加速时限制速度的行为。如果玩家以某种方式从盘绕或子弹击中获得更多速度,则不应限制速度。我们可以认为它就像宇宙飞船的引擎中没有足够的动力来更快地行驶。这类似于线性阻力,但仅在加速时(不加速时,船根本不会减速)。我有可以让玩家获得更大速度的道具,所以这很重要。

这将如何实施?我试图仅在玩家加速时限制速度,但随后它会立即将其钳制到指定值并且看起来不自然。加速时会慢慢减小幅度的协同程序会起作用吗?但是,它必须考虑玩家的方向和当前速度?

编辑:澄清:在实践中我想问一个刚体“如果我向你施加这个力当你的移动速度超过 maxEngineSpeed,它会增加你的速度吗?如果会,就不要用力,如果会降低你的速度,那就用力”。

编辑:将 maxSpeed 变量名称更改为 maxEngineSpeed 以更清晰。

【问题讨论】:

    标签: unity3d game-physics


    【解决方案1】:

    移除 FixedUpdate 中的限制。相反,请在添加 Velocity 的位置添加检查(您检测到 Xbox 控制器“A”被按下的位置)。

    类似:

    if(Input.GetButton("Xbox-A"))
    {
        if(rb.velocity.magnitude < scaledMaxSpeed)
        {
            rb.addForce(...);
        }
    }
    

    因此,如果您的速度超过您的最大速度,则飞船无法加速(通过自身动力)。

    【讨论】:

    • 是的,但是如果我想加速到船的相反方向怎么办?例如,如果从右侧被击中,我想加速到那个方向以抵消我的船从子弹中获得的动力。您的示例意味着,如果我的移动速度超过 maxSpeed,那么我无法减速,对吧?
    【解决方案2】:

    下拉和拖动

    有很多方法可以实现您想要的。下面我通过一个工作演示展示了两种可能的方法,让您对性能和不同之处有所了解。还可以在底部链接到另一个演示。

    下拉

    您可以通过定义最大超速和超速阻力系数来降低速度

    下拉方法

    定义设置

     float pullDown = 0.1f;  // dimensionless > 0, < 1 
     float maxOverSpeed = 5.0f;
     float maxSpeed = 4.0f
     float acceleration = 0.1f;
    

    每帧

     if (accelerate && speed < maxSpeed) { speed += acceleration }
    
     // clamp max over speed
     speed = speed > maxOverSpeed ? maxOverSpeed : speed;
    
     float speedAdjust = speed - maxSpeed;
    
     // pull speed down if needed
     speed -= speedAdjust > 0.0f ? speedAdjust * pullDown : 0.0f;
    
     // set the velocity magnitude to the new  speed
    

    我个人不喜欢这种方法,因为它是一个滑行模型,船可以加速并保持它,没有减速,但它确实可以更好地控制速度。

    拖动

    我首选的方法是使用简单的阻力系数。稍作修改以在超速时添加额外的绘制

    但是,这使得很难知道在某些加速度下的最大速度是多少。有一个公式可以给你一个阻力系数来匹配加速度的最大速度,或者加速度来匹配阻力系数的最大速度,但是我不记得了,因为我发现它已经有好几年了我需要使用它。

    我对它进行优化并定义一个近似值,对其进行测试并进行改进,直到我感觉正确为止。实际上,如果问玩家的最大速度是多少?我所知道的不是太快也不是太慢。 :P

    拖动方法

    定义

      float acceleration = 0.1f;
      float drag = 1.0f - 0.021f;
      float overSpeedDrag = 1.0f - 0.026f;
      float maxSpeed = 4;
    

    每帧

      // apply drag depending on speed
      speed *= speed > maxSpeed ? overSpeedDrag : drag;
      if (accelerate) { speed += acceleration }
                
      // set the velocity magnitude to the new current speed
    
     
    

    示例

    这些方法作为代码对实际结果没有太大的感觉,因此下面的 sn-p 实现了这两种方法,因此您可以看到并感受它们的工作原理。

    代码在顶部(在 JavaScript 中)两个不同的方法在函数 update() { 中标记为 PULL_DOWNDRAG

    • 船速以每秒像素 (Pps) 为单位
    • 两艘船的加速度常数相同,但 B 船(拖曳法)不以恒定速率加速。
    • A 船会滑行,B 船总是会停下来。
    • 点击颠簸来踢船的速度。

    const accelFunction = {
        get vel() { return new Vec2(0, 0) },
        speed: 0,
        acceleration: 0.1,
        maxSpeed: 4,
        
        // drag constants
        drag: 1 - 0.0241,
        overSpeedDrag: 1 - 0.0291,
    
        // pulldown constants;
        overSpeed: 5,
        pullDown: 0.1,
    
        update() {
            if (this.method === DRAG) { // Drag method
                this.speed *= this.speed > this.maxSpeed ? this.overSpeedDrag: this.drag;
                if (this.accelerate) { this.speed += this.acceleration }
    
    
            } else {  // Pull down method
                if (this.accelerate && this.speed < this.maxSpeed) { this.speed += this.acceleration }
                this.speed = this.speed > this.maxOverSpeed ? this.maxOverSpeed : this.speed;
                var speedAdjust = this.speed - this.maxSpeed;
                this.speed -= speedAdjust > 0 ? speedAdjust * this.pullDown : 0;
            }
    
            // move ship
            this.vel.length = this.speed;
            this.pos.add(this.vel);
        },
    }
    
    
    
    
    
    
    
    /* rest of code unrelated to anwser */
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    requestAnimationFrame(start);
    const ctx = canvas.getContext("2d");
    const PULL_DOWN = 0;
    const DRAG = 1;
    var shipA, shipB;
    var bgPos;
    function Ship(method, control, controlBump) {  // creates a Player ship
    
        control.addEventListener("mousedown",() => API.go());
        control.addEventListener("mouseup",() => API.coast());
        control.addEventListener("mouseout",() => API.coast());
        controlBump.addEventListener("click",() => API.bump());
        const API = {    
            ...accelFunction,
            pos: new Vec2(100, 50 + method * 50),
            method, // 0 drag method, 1 pulldown
            draw() {
                ctx.setTransform(1,0,0,1,this.pos.x - bgPos.x, this.pos.y)
                ctx.strokeStyle = "#FFF";
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.lineTo(20, 0);
                ctx.lineTo(-20, -20);
                ctx.lineTo(-20,  20);
                ctx.closePath();
                ctx.stroke();
                ctx.fillText(this.method ? "B" : "A", -11, 3);     
                ctx.fillText((this.speed * 60 | 0) + "Pps", 80, 3);
                if (this.accelerate) {
                    ctx.strokeStyle = "#FF0";
                    ctx.beginPath();
                    ctx.lineTo(-20, -10);
                    ctx.lineTo(-30 - Math.rand(0,10), 0);
                    ctx.lineTo(-20,  10);
                    ctx.stroke();
                }
                    
            
            },
            focus: false,
            reset() {
                this.focus = false;
                this.vel.zero();
                this.pos.init(100, 50 + this.method * 50);
                this.speed = 0;
                this.accelerate = false;
            },
            go() {
                this.accelerate = true;
                this.focus  = true;
                if (this.method === 1) { shipA.reset() }
                else { shipB.reset() }
            },
            coast() {
                this.accelerate = false;
            },    
            bump() {
                this.speed += 1;
            },
    
        };
        return API;
    }
    function start() {
       init();
       requestAnimationFrame(mainLoop);
    }
    
    function mainLoop() {
        ctx.setTransform(1,0,0,1,0,0);
        ctx.clearRect(0,0,500,170);
     
        shipA.update();
        shipB.update();
        
        bgPos.x = shipA.focus ? shipA.pos.x - 50 :  shipB.pos.x - 50 ;
        drawBG(bgPos);
        
        shipA.draw();
        shipB.draw();
        requestAnimationFrame(mainLoop);
    
    }
    
    function drawBG(bgPos) {
        ctx.fillStyle = "#FFF";
        ctx.beginPath();
        const bgX = -bgPos.x + 100000;
        for (const p of background) {
            x = (p.x + bgX) % 504 - 2;
            ctx.rect(x, p.y, 2, 2);
        } 
        ctx.fill();
    }
    
    
    const BG_COUNT = 200;
    const background = [];
    function init() {
        ctx.font = "16px arial";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
    
        bgPos = new Vec2();
        shipA = Ship(PULL_DOWN, goA, bumpA);
        shipB = Ship(DRAG, goB, bumpB);
        var i = BG_COUNT;
        while (i--) {
            background.push(new Vec2(Math.rand(0, 10000), Math.rand(-1, 170)));    
        }
    }
    
    
    
    
    
    
    /* Math LIB Vec2 and math extensions */
    Math.rand = (m, M) => Math.random() * (M - m) + m;
    function Vec2(x = 0, y = (temp = x, x === 0 ? (x = 0 , 0) : (x = x.x, temp.y))) { this.x = x; this.y = y }
    Vec2.prototype = {
        init(x, y = (temp = x, x = x.x, temp.y)) { this.x = x; this.y = y; return this }, 
        zero() { this.x = this.y = 0; return this },
        add(v, res = this) { res.x = this.x + v.x; res.y = this.y + v.y; return res },
        scale(val, res = this) { res.x = this.x * val; res.y = this.y * val; return res },
        get length() { return this.lengthSqr ** 0.5 },
        set length(l) { 
            const len = this.lengthSqr;
            len > 0 ? this.scale(l / len ** 0.5) : (this.x = l);
        },
        get lengthSqr() { return this.x * this.x + this.y * this.y },
    };
    canvas {background: #347;}
    div {
       position: absolute;
       top: 150px;
       left: 20px;
    }
    span { color: white; font-family: arial }
    <canvas id="canvas" width="500" height="170"></canvas>
    <div>
    <button id="goA">Go A</button>
    <button id="bumpA">Bump A</button>
    <button id="goB">Go B</button>
    <button id="bumpB">Bump B</button>
    <span> Methods: A = Pull down B = Drag </span>
    </div>

    没有限制

    这些方法有很多变体,并且有很多关于 SO 的示例(我在该主题中写了很多答案。例如,请参阅演示 sn-p(答案底部)以了解拖动方法修改的 example)。

    您使用哪种方法在很大程度上取决于您希望交互感觉如何,没有正确或错误的方法,因为游戏物理与真实物理有很大不同。

    【讨论】:

    • 哇,sn-p 代码真的很棒而且内容丰富。我认为我没有完全正确地解释这种情况,所以我会尝试澄清一下。我正在寻找的是“Ship A”,但是当船被颠簸时,速度会保持在颠簸设置的位置。然后,如果我尝试“Go A”,如果方向与“Bump A”中的方向相同,它应该什么都不做。但是如果我尝试向相反的方向“Go A”,那么速度会降低。所以引擎应该有效果,但他们不应该让船超过最大速度。其他效果可以使船超过最大速度。这是否澄清了我的问题?
    【解决方案3】:

    知道加速度 (a) 是速度 (Δv) 随时间变化 (Δt) 的变化,我会检查一下。

    也许有类似(伪)的东西:

    float lastVelocity = 0;
    bool isAccelerating = false;
    Update()
    {
        float currentVelocity = rb.velocity;
        if(currentVelocity > lastVelocity)
        {
            isAccelerating = true;
            lastVelocity = currentVelocity;
        }
        else
        {
            isAccelerating = false;
        }
    }
    

    现在您知道当您的“船”在加速时,降低速度的唯一方法是由外力(如重力或摩擦力)引起,根据您的设置,我将停用这些力,或更改物理材料这是造成摩擦的原因。

    【讨论】:

    • 只是 != 并不意味着你变得越来越快 ;)
    • 哦,你无论如何都想更新lastVelocity,否则想象变慢然后再次加速 -> 你必须超过最后的最大速度才能满足条件......仍然......它保持相当不可靠并且仍然存在边缘情况:想象用户被碰撞击中->他超过了最大速度,但在它之后他按下了某个键->被夹住。或者他在被击中时已经达到了最大速度......这感觉不对......我想实际上KYL3R's approach在这里会更好
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多