深入理解Canvas动画循环的设计模式,掌握游戏循环、状态管理和对象池技术。动画循环是Canvas动画的核心架构,良好的循环设计能确保动画流畅、可控和高效。
动画循环是一个持续运行的过程,不断执行"更新状态"和"渲染画面"两个步骤。
function gameLoop(timestamp) {
update(timestamp) // 更新游戏状态
render() // 渲染画面
requestAnimationFrame(gameLoop)
}
requestAnimationFrame(gameLoop)
| 阶段 | 说明 | 职责 |
|---|---|---|
| 输入处理 | 处理用户输入 | 键盘、鼠标、触摸 |
| 状态更新 | 更新游戏逻辑 | 位置、速度、碰撞 |
| 画面渲染 | 绘制当前状态 | 清除、绘制、显示 |
最基础的动画循环,适合简单场景。
function animate() {
update()
draw()
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
使用固定时间步长确保动画一致性。
let lastTime = 0
const FIXED_TIMESTEP = 1000 / 60 // 60 FPS
function gameLoop(currentTime) {
const deltaTime = currentTime - lastTime
lastTime = currentTime
update(deltaTime)
render()
requestAnimationFrame(gameLoop)
}
确保物理模拟的稳定性。
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)
}
根据实际帧间隔调整更新。
let lastTime = 0
function gameLoop(currentTime) {
const deltaTime = currentTime - lastTime
lastTime = currentTime
// 限制最大时间差,避免跳帧
const clampedDelta = Math.min(deltaTime, 100)
update(clampedDelta)
render()
requestAnimationFrame(gameLoop)
}
将更新逻辑和渲染逻辑分离。
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()
}
})
}
}
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)
}