Как рассчитать переход на основе дельта-времени?

#javascript #math #canvas #physics

#javascript #математика #холст #физика

Вопрос:

Я пытаюсь создать небольшую игру на JavaScript (без движка) и хочу избавиться от анимации на основе кадров.

Я успешно добавил дельта-время для горизонтальных перемещений (отлично работает при 60 или 144 кадрах в секунду).

Но я не могу заставить это работать с прыжком, высота (или сила) не всегда одинакова, и я не знаю почему.

Я уже пробовал (и все еще сталкивался с точно такой же проблемой):

  • Прохождение дельта-времени в конце update() : x = Math.round(dx * dt)
  • Изменение Date.now() на performance.now()
  • Без округления DeltaY
  • Фиксирующая высота прыжка

Я создал упрощенный пример с двумя типами прыжков, прыжком с фиксированной высотой и обычным прыжком (не знаю, как это назвать). У обоих одна и та же проблема.

 const canvas  = document.getElementById('canvas'),
      ctx     = canvas.getContext('2d'),
      canvas2 = document.getElementById('canvas2'),
      ctx2    = canvas2.getContext('2d');



// CLASS PLAYER ------------------------

class Actor {
  constructor(color, ctx, j) {
    this.c     = ctx
  
    this.w     = 20
    this.h     = 40
    this.x     = canvas.width /2 - this.w/2
    this.y     = canvas.height/2 - this.h/2
    this.color = color

    // Delta
    this.dy = 0

    // Movement
    this.gravity   =  25/1000
    this.maxSpeed  = 600/1000
    
    // Jump Height lock
    this.jumpType = (j) ? 'capedJump' : 'normalJump'
    this.jumpHeight = -50

    // Booleans
    this.isOnFloor = false
  }
  
  
 // Normal jump
 normalJump(max) {
   if(!this.isOnFloor) return
   
   this.dy        = -max
   this.isOnFloor = false
 }
 
  
 // Jump lock (locked max height)
 capedJump(max) {
     const jh = this.jumpHeight;
     if(jh >= 0) return
     
     this.dy  = -max/15
     if(jh - this.dy >= 0) {
       this.dy = (jh - this.dy)   jh
       this.jumpHeight = 0
     } else {
       this.jumpHeight  = -this.dy
     }
 }
 
 
 
 update(dt) {
   const max     = this.maxSpeed*dt,
         gravity = this.gravity*dt;
   
   // JUMP
   this[this.jumpType](max)
  
   // GRAVITY
   this.dy  = gravity
   
   
  // TOP AND DOWN COLLISION (CANVAS BORDERS)
  const y = this.y   this.dy,
        h = y        this.h;
  
  if (y <= 0) this.y = this.dy = 0
  else if (h >= canvas.height) {
    this.y          = canvas.height - this.h
    this.dy         = 0
    this.isOnFloor  = true
    this.jumpHeight = -50
  }
  
  // Update Y
  this.y  = Math.round(this.dy)
 }
 
 
 draw() {
 const ctx = this.c
  ctx.fillStyle = this.color
  ctx.fillRect(this.x, this.y, this.w, this.h)
 }
}
const Player  = new Actor('brown', ctx,  false)
const Player2 = new Actor('blue',  ctx2, true)



// ANIMATE -----------------------------

let lastRender = 0
let currRender = Date.now()
function animate() {
  // Set Delta Time
  lastRender = currRender
  currRender = Date.now()
  let dt     = currRender - lastRender
  
  // CANVAS #1 (LEFT)
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  background(ctx)
  
  Player.update(dt)
  Player.draw()
  
  
  // CANVAS #2 (RIGHT)
  ctx2.clearRect(0, 0, canvas2.width, canvas2.height)
  background(ctx2)
  
  Player2.update(dt)
  Player2.draw()
  
  window.requestAnimationFrame(animate)
}
animate()



// EVENT LISTENERS -----------------------

window.addEventListener('keydown', (e) => {
  e.preventDefault()
  if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = true
})

window.addEventListener('keyup', (e) => {
  e.preventDefault()
  if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = false
})


// Just a function to draw Background nothing to see here
function background(c) {
	const lineNumber = Math.floor(canvas.height/10)
  
  c.fillStyle = 'gray'
	for(let i = 0; i < lineNumber; i  ) {
  	c.fillRect(0, lineNumber*i, canvas.width, 1)
  }
}  
 div {
  display: inline-block;
  font-family: Arial;
}

canvas {
  border: 1px solid black;
}

span {
  display: block;
  color: gray;
  text-align: center;
}  
 <div>
<canvas width="100" height="160" id="canvas"></canvas>
<span>Normal</span>
</div>

<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>  

Комментарии:

1. gamedev.stackexchange.com/questions/29617/… Вы получаете странные результаты, потому что ваша математика неверна в двух словах.

2. Извините, возможно, я что-то пропустил, но я не увидел никакой разницы между моим кодом и кодом в сообщении, которое вы только что отправили.

3. Вам следует взглянуть на эту книгу barnesandnoble.com/p / … всего 3 доллара, используется для 50 . Это то, что мы использовали в колледже. Это хорошо, но весь пример кода написан на c . Код достаточно прост и на самом деле не имеет значения. Какое значение имеет объяснение математики. Вы действительно должны использовать векторы.

4. Наконец, не создавайте свой собственный игровой движок. Похоже, вы довольно новичок в программировании. Создавать игры СЛОЖНО. Создание игрового движка делает это простым. Используйте Phase или что-то подобное.

5. Спасибо за книгу, я посмотрю! Я не хочу использовать игровой движок, я хочу научиться и посмотреть, как далеко я могу продвинуться (я не создаю свою игру для реальных игровых целей)

Ответ №1:

Вот как я бы реорганизовал код:

  • Не используйте dy как для скорости, так и для положения (что вы, похоже, делаете). Переименуйте его vy и используйте исключительно как вертикальную скорость.

  • Перейдите isOnFloor к функции, чтобы мы всегда могли проверять наличие столкновений с полом.

  • Отделите функции перехода от фактических обновлений движения. Просто заставьте их установить вертикальную скорость, если игрок находится на полу.

  • Выполняйте проверку столкновения сверху / снизу отдельно в зависимости от направления движения.

  • Не округляйте DeltaY — это испортит небольшие движения.

С учетом этих изменений поведение движения является правильным и стабильным:

 const canvas1 = document.getElementById('canvas1'),
      ctx1    = canvas1.getContext('2d'),
      canvas2 = document.getElementById('canvas2'),
      ctx2    = canvas2.getContext('2d');

// Global physics variables
const GRAVITY   = 0.0015;
const MAXSPEED  = 0.6;
const MAXHEIGHT = 95;


// CLASS PLAYER ------------------------

class Actor {
  constructor(C, W, H, J) {
    // World size
    this.worldW = W;
    this.worldH = H;

    // Size amp; color
    this.w = 20;
    this.h = 40;
    this.color = C;

    // Speed
    this.vy = 0;

    // Position
    this.x = W/2 - this.w/2;
    this.y = H/2 - this.h/2;

    // Jump Height lock
    this.jumpCapped = J;
    this.jumpHeight = 0;
  }

  // move isOnFloor() to a function
  isOnFloor() {
    return this.y >= this.worldH - this.h;
  }

  // Normal jump
  normalJump() {
    if(!this.isOnFloor()) return;

    this.vy = -MAXSPEED;
  }

  // Jump lock (locked max height)
  cappedJump(max) {
    if(!this.isOnFloor()) return;

    this.vy = -MAXSPEED;
    this.jumpHeight = max;
  }

  update(dt) {
    // JUMP
    if (this.jumpCapped)
      this.cappedJump(MAXHEIGHT);
    else
      this.normalJump();

    // GRAVITY
    this.vy  = GRAVITY * dt;
    this.y  = this.vy * dt;
   
    // Bottom collision
    if (this.vy > 0) {
      if (this.isOnFloor()) {
        this.y = this.worldH  - this.h;
        this.vy = 0;
      }
    }
    else 
    // Top collision
    if (this.vy < 0) {
      const maxh = (this.jumpCapped) ? (this.worldH - this.jumpHeight) : 0;
      if (this.y < maxh) {
        this.y = maxh;
        this.vy = 0;
      }
    }
  }

  draw(ctx) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.w, this.h);
  }
}

const Player1 = new Actor('brown', canvas1.width, canvas1.height, false);
const Player2 = new Actor('blue',  canvas2.width, canvas2.height, true);


// ANIMATE -----------------------------

let lastUpdate = 0;
let randomDT = 0;
function animate() {
  // Compute delta time
  let currUpdate = Date.now();
  let dt = currUpdate - lastUpdate;

  // Randomize the update time interval
  // to test the physics' stability
  if (dt > randomDT) {
     randomDT = 35 * Math.random()   5;
     Player1.update(dt);
     Player2.update(dt);
     lastUpdate = currUpdate;
  }

  // CANVAS #1 (LEFT)
  ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
  background(ctx1);
  Player1.draw(ctx1);

  // CANVAS #2 (RIGHT)
  ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
  background(ctx2);
  Player2.draw(ctx2);

  window.requestAnimationFrame(animate);
}

animate();


// EVENT LISTENERS -----------------------

window.addEventListener('keydown',
  (e) => {
    e.preventDefault();
    if (Player.keys.hasOwnProperty(e.code))
      Player.keys[e.code] = true;
  }
)

window.addEventListener('keyup',
  (e) => {
    e.preventDefault()
    if (Player.keys.hasOwnProperty(e.code))
      Player.keys[e.code] = false;
  }
)


// Just a function to draw Background nothing to see here
function background(c) {
  const lineNumber = Math.floor(canvas1.height/10)

  c.fillStyle = 'gray'
  for(let i = 0; i < lineNumber; i  ) {
    c.fillRect(0, lineNumber*i, canvas1.width, 1)
  }
}  
 div {
  display: inline-block;
  font-family: Arial;
}

canvas {
  border: 1px solid black;
}

span {
  display: block;
  color: gray;
  text-align: center;
}  
 <div>
<canvas width="100" height="160" id="canvas1"></canvas>
<span>Normal</span>
</div>

<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>