动画循环

深入理解Canvas动画循环的设计模式,掌握游戏循环、状态管理和对象池技术。动画循环是Canvas动画的核心架构,良好的循环设计能确保动画流畅、可控和高效。

什么是动画循环

动画循环是一个持续运行的过程,不断执行"更新状态"和"渲染画面"两个步骤。

基本结构

function gameLoop(timestamp) {
  update(timestamp)    // 更新游戏状态
  render()             // 渲染画面
  requestAnimationFrame(gameLoop)
}

requestAnimationFrame(gameLoop)

循环的三个阶段

阶段说明职责
输入处理处理用户输入键盘、鼠标、触摸
状态更新更新游戏逻辑位置、速度、碰撞
画面渲染绘制当前状态清除、绘制、显示

循环模式

1. 简单循环

最基础的动画循环,适合简单场景。

function animate() {
  update()
  draw()
  requestAnimationFrame(animate)
}

requestAnimationFrame(animate)

2. 时间步长循环

使用固定时间步长确保动画一致性。

let lastTime = 0
const FIXED_TIMESTEP = 1000 / 60  // 60 FPS

function gameLoop(currentTime) {
  const deltaTime = currentTime - lastTime
  lastTime = currentTime
  
  update(deltaTime)
  render()
  
  requestAnimationFrame(gameLoop)
}

3. 固定时间步长循环

确保物理模拟的稳定性。

const FIXED_TIMESTEP = 1000 / 60
let accumulator = 0
let lastTime = 0

function gameLoop(currentTime) {
  const frameTime = currentTime - lastTime
  lastTime = currentTime
  accumulator += frameTime
  
  while (accumulator >= FIXED_TIMESTEP) {
    update(FIXED_TIMESTEP)
    accumulator -= FIXED_TIMESTEP
  }
  
  render(accumulator / FIXED_TIMESTEP)
  requestAnimationFrame(gameLoop)
}

固定时间步长演示

固定时间步长循环

4. 可变时间步长循环

根据实际帧间隔调整更新。

let lastTime = 0

function gameLoop(currentTime) {
  const deltaTime = currentTime - lastTime
  lastTime = currentTime
  
  // 限制最大时间差,避免跳帧
  const clampedDelta = Math.min(deltaTime, 100)
  
  update(clampedDelta)
  render()
  
  requestAnimationFrame(gameLoop)
}

5. 分离更新和渲染

将更新逻辑和渲染逻辑分离。

const UPDATE_INTERVAL = 1000 / 60
const RENDER_INTERVAL = 1000 / 60

let lastUpdateTime = 0
let lastRenderTime = 0

function gameLoop(currentTime) {
  // 更新逻辑
  if (currentTime - lastUpdateTime >= UPDATE_INTERVAL) {
    update()
    lastUpdateTime = currentTime
  }
  
  // 渲染逻辑
  if (currentTime - lastRenderTime >= RENDER_INTERVAL) {
    render()
    lastRenderTime = currentTime
  }
  
  requestAnimationFrame(gameLoop)
}

状态管理

游戏状态机

const GameState = {
  MENU: 'menu',
  PLAYING: 'playing',
  PAUSED: 'paused',
  GAME_OVER: 'gameOver'
}

class Game {
  constructor() {
    this.state = GameState.MENU
    this.score = 0
    this.lives = 3
  }
  
  setState(newState) {
    this.state = newState
    this.onStateChange(newState)
  }
  
  onStateChange(state) {
    switch (state) {
      case GameState.MENU:
        this.showMenu()
        break
      case GameState.PLAYING:
        this.startGame()
        break
      case GameState.PAUSED:
        this.pauseGame()
        break
      case GameState.GAME_OVER:
        this.endGame()
        break
    }
  }
  
  update(deltaTime) {
    switch (this.state) {
      case GameState.PLAYING:
        this.updatePlaying(deltaTime)
        break
      case GameState.MENU:
        this.updateMenu(deltaTime)
        break
    }
  }
  
  render() {
    switch (this.state) {
      case GameState.PLAYING:
        this.renderPlaying()
        break
      case GameState.MENU:
        this.renderMenu()
        break
      case GameState.PAUSED:
        this.renderPaused()
        break
      case GameState.GAME_OVER:
        this.renderGameOver()
        break
    }
  }
}

状态管理演示

游戏状态管理(点击切换状态)

对象池

对象池可以避免频繁创建和销毁对象,提升性能。

基本实现

class ObjectPool {
  constructor(createFn, initialSize = 10) {
    this.createFn = createFn
    this.pool = []
    this.active = []
    
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(this.createFn())
    }
  }
  
  get() {
    let obj = this.pool.pop()
    if (!obj) {
      obj = this.createFn()
    }
    this.active.push(obj)
    return obj
  }
  
  release(obj) {
    const index = this.active.indexOf(obj)
    if (index !== -1) {
      this.active.splice(index, 1)
      this.pool.push(obj)
    }
  }
  
  releaseAll() {
    while (this.active.length > 0) {
      this.pool.push(this.active.pop())
    }
  }
}

粒子系统示例

class Particle {
  constructor() {
    this.reset()
  }
  
  reset() {
    this.x = 0
    this.y = 0
    this.vx = 0
    this.vy = 0
    this.life = 0
    this.maxLife = 0
    this.color = '#fff'
    this.size = 5
    this.active = false
  }
  
  init(x, y, options = {}) {
    this.x = x
    this.y = y
    this.vx = options.vx || (Math.random() - 0.5) * 4
    this.vy = options.vy || (Math.random() - 0.5) * 4
    this.life = options.life || 1
    this.maxLife = this.life
    this.color = options.color || `hsl(${Math.random() * 360}, 70%, 60%)`
    this.size = options.size || Math.random() * 4 + 2
    this.active = true
  }
  
  update(deltaTime) {
    if (!this.active) return
    
    this.x += this.vx
    this.y += this.vy
    this.life -= deltaTime / 1000
    
    if (this.life <= 0) {
      this.active = false
    }
  }
  
  draw(ctx) {
    if (!this.active) return
    
    ctx.globalAlpha = this.life / this.maxLife
    ctx.fillStyle = this.color
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
    ctx.fill()
    ctx.globalAlpha = 1
  }
}

class ParticleSystem {
  constructor(maxParticles = 100) {
    this.pool = new ObjectPool(() => new Particle(), maxParticles)
    this.particles = []
  }
  
  emit(x, y, count = 1, options = {}) {
    for (let i = 0; i < count; i++) {
      const particle = this.pool.get()
      particle.init(x, y, options)
      this.particles.push(particle)
    }
  }
  
  update(deltaTime) {
    for (let i = this.particles.length - 1; i >= 0; i--) {
      const particle = this.particles[i]
      particle.update(deltaTime)
      
      if (!particle.active) {
        this.pool.release(particle)
        this.particles.splice(i, 1)
      }
    }
  }
  
  draw(ctx) {
    this.particles.forEach(p => p.draw(ctx))
  }
}

对象池演示

粒子系统(对象池)

实体组件系统

ECS(Entity-Component-System)是一种灵活的游戏架构模式。

基本结构

// 组件
class PositionComponent {
  constructor(x = 0, y = 0) {
    this.x = x
    this.y = y
  }
}

class VelocityComponent {
  constructor(vx = 0, vy = 0) {
    this.vx = vx
    this.vy = vy
  }
}

class RenderComponent {
  constructor(color = '#fff', size = 10) {
    this.color = color
    this.size = size
  }
}

// 实体
class Entity {
  constructor(id) {
    this.id = id
    this.components = new Map()
  }
  
  addComponent(component) {
    this.components.set(component.constructor, component)
    return this
  }
  
  getComponent(ComponentClass) {
    return this.components.get(ComponentClass)
  }
  
  hasComponent(ComponentClass) {
    return this.components.has(ComponentClass)
  }
}

// 系统
class MovementSystem {
  update(entities, deltaTime) {
    entities.forEach(entity => {
      if (entity.hasComponent(PositionComponent) && 
          entity.hasComponent(VelocityComponent)) {
        const pos = entity.getComponent(PositionComponent)
        const vel = entity.getComponent(VelocityComponent)
        
        pos.x += vel.vx * deltaTime
        pos.y += vel.vy * deltaTime
      }
    })
  }
}

class RenderSystem {
  constructor(ctx) {
    this.ctx = ctx
  }
  
  update(entities) {
    entities.forEach(entity => {
      if (entity.hasComponent(PositionComponent) && 
          entity.hasComponent(RenderComponent)) {
        const pos = entity.getComponent(PositionComponent)
        const render = entity.getComponent(RenderComponent)
        
        this.ctx.fillStyle = render.color
        this.ctx.beginPath()
        this.ctx.arc(pos.x, pos.y, render.size, 0, Math.PI * 2)
        this.ctx.fill()
      }
    })
  }
}

ECS使用示例

class GameWorld {
  constructor(ctx) {
    this.entities = []
    this.systems = []
    this.ctx = ctx
    this.lastTime = 0
    
    this.addSystem(new MovementSystem())
    this.addSystem(new RenderSystem(ctx))
  }
  
  addEntity(entity) {
    this.entities.push(entity)
  }
  
  addSystem(system) {
    this.systems.push(system)
  }
  
  update(currentTime) {
    const deltaTime = (currentTime - this.lastTime) / 1000
    this.lastTime = currentTime
    
    this.systems.forEach(system => {
      system.update(this.entities, deltaTime)
    })
    
    requestAnimationFrame(t => this.update(t))
  }
  
  start() {
    this.lastTime = performance.now()
    requestAnimationFrame(t => this.update(t))
  }
}

// 使用
const world = new GameWorld(ctx)

const ball = new Entity('ball')
  .addComponent(new PositionComponent(100, 100))
  .addComponent(new VelocityComponent(50, 30))
  .addComponent(new RenderComponent('#3498db', 20))

world.addEntity(ball)
world.start()

循环优化

避免在循环中创建对象

// 不好的做法
function animate() {
  const point = { x: 0, y: 0 }  // 每帧创建新对象
  draw(point)
  requestAnimationFrame(animate)
}

// 好的做法
const point = { x: 0, y: 0 }  // 复用对象

function animate() {
  draw(point)
  requestAnimationFrame(animate)
}

减少状态切换

// 不好的做法
function draw() {
  objects.forEach(obj => {
    ctx.fillStyle = obj.color  // 频繁切换
    ctx.fillRect(obj.x, obj.y, obj.w, obj.h)
  })
}

// 好的做法:按颜色分组
function draw() {
  const groups = groupByColor(objects)
  
  Object.entries(groups).forEach(([color, objs]) => {
    ctx.fillStyle = color  // 每种颜色只设置一次
    objs.forEach(obj => {
      ctx.fillRect(obj.x, obj.y, obj.w, obj.h)
    })
  })
}

使用脏矩形

const dirtyRects = []

function addDirtyRect(x, y, w, h) {
  dirtyRects.push({ x, y, w, h })
}

function clearDirtyRects() {
  dirtyRects.forEach(rect => {
    ctx.clearRect(rect.x, rect.y, rect.w, rect.h)
  })
  dirtyRects.length = 0
}

function animate() {
  clearDirtyRects()
  update()
  render()
  requestAnimationFrame(animate)
}