学习Canvas物理模拟技术,掌握物理引擎基础、运动学和动力学模拟方法。物理模拟基于物理规律计算运动,创造真实的动画效果,是游戏开发的核心技术。
运动学描述物体的运动,不考虑力的作用。
class Kinematics {
constructor() {
this.position = { x: 0, y: 0 }
this.velocity = { x: 0, y: 0 }
this.acceleration = { x: 0, y: 0 }
}
update(dt) {
this.velocity.x += this.acceleration.x * dt
this.velocity.y += this.acceleration.y * dt
this.position.x += this.velocity.x * dt
this.position.y += this.velocity.y * dt
}
}
动力学考虑力的作用,使用牛顿运动定律。
class Dynamics {
constructor(mass = 1) {
this.position = { x: 0, y: 0 }
this.velocity = { x: 0, y: 0 }
this.acceleration = { x: 0, y: 0 }
this.force = { x: 0, y: 0 }
this.mass = mass
}
applyForce(fx, fy) {
this.force.x += fx
this.force.y += fy
}
update(dt) {
this.acceleration.x = this.force.x / this.mass
this.acceleration.y = this.force.y / this.mass
this.velocity.x += this.acceleration.x * dt
this.velocity.y += this.acceleration.y * dt
this.position.x += this.velocity.x * dt
this.position.y += this.velocity.y * dt
this.force.x = 0
this.force.y = 0
}
}
class PhysicsBody {
constructor(x, y, options = {}) {
this.x = x
this.y = y
this.vx = options.vx || 0
this.vy = options.vy || 0
this.ax = 0
this.ay = 0
this.mass = options.mass || 1
this.restitution = options.restitution || 0.8 // 弹性系数
this.friction = options.friction || 0.1 // 摩擦系数
this.width = options.width || 20
this.height = options.height || 20
this.type = options.type || 'dynamic' // dynamic, static, kinematic
}
applyForce(fx, fy) {
if (this.type === 'dynamic') {
this.ax += fx / this.mass
this.ay += fy / this.mass
}
}
applyImpulse(ix, iy) {
if (this.type === 'dynamic') {
this.vx += ix / this.mass
this.vy += iy / this.mass
}
}
update(dt) {
if (this.type !== 'dynamic') return
this.vx += this.ax * dt
this.vy += this.ay * dt
this.x += this.vx * dt
this.y += this.vy * dt
this.ax = 0
this.ay = 0
}
}
class GravityWorld {
constructor(gravity = 0.5) {
this.gravity = gravity
this.bodies = []
}
addBody(body) {
this.bodies.push(body)
}
removeBody(body) {
const index = this.bodies.indexOf(body)
if (index !== -1) {
this.bodies.splice(index, 1)
}
}
update(dt) {
this.bodies.forEach(body => {
if (body.type === 'dynamic') {
body.applyForce(0, this.gravity * body.mass)
}
body.update(dt)
})
}
}
const world = new GravityWorld(0.5)
const ball = new PhysicsBody(200, 50, { mass: 1, type: 'dynamic' })
world.addBody(ball)
class Projectile {
constructor(x, y, angle, speed, gravity = 0.5) {
this.x = x
this.y = y
this.vx = Math.cos(angle) * speed
this.vy = Math.sin(angle) * speed
this.gravity = gravity
this.trail = []
}
update() {
this.trail.push({ x: this.x, y: this.y })
if (this.trail.length > 20) {
this.trail.shift()
}
this.vy += this.gravity
this.x += this.vx
this.y += this.vy
}
draw(ctx) {
ctx.strokeStyle = 'rgba(52, 152, 219, 0.5)'
ctx.lineWidth = 2
ctx.beginPath()
this.trail.forEach((p, i) => {
if (i === 0) ctx.moveTo(p.x, p.y)
else ctx.lineTo(p.x, p.y)
})
ctx.stroke()
ctx.fillStyle = '#e74c3c'
ctx.beginPath()
ctx.arc(this.x, this.y, 8, 0, Math.PI * 2)
ctx.fill()
}
}
function applyFriction(body, friction) {
const speed = Math.sqrt(body.vx * body.vx + body.vy * body.vy)
if (speed > 0) {
const frictionForce = friction * body.mass
const fx = -(body.vx / speed) * frictionForce
const fy = -(body.vy / speed) * frictionForce
body.applyForce(fx, fy)
}
}
function applyAirResistance(body, drag = 0.01) {
const speed = Math.sqrt(body.vx * body.vx + body.vy * body.vy)
const dragForce = drag * speed * speed
if (speed > 0) {
body.applyForce(
-(body.vx / speed) * dragForce,
-(body.vy / speed) * dragForce
)
}
}
class Spring {
constructor(bodyA, bodyB, restLength, stiffness, damping) {
this.bodyA = bodyA
this.bodyB = bodyB
this.restLength = restLength
this.stiffness = stiffness
this.damping = damping
}
apply() {
const dx = this.bodyB.x - this.bodyA.x
const dy = this.bodyB.y - this.bodyA.y
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance === 0) return
const stretch = distance - this.restLength
const force = this.stiffness * stretch
const nx = dx / distance
const ny = dy / distance
const relVx = this.bodyB.vx - this.bodyA.vx
const relVy = this.bodyB.vy - this.bodyA.vy
const relVn = relVx * nx + relVy * ny
const dampingForce = this.damping * relVn
const totalForce = force + dampingForce
this.bodyA.applyForce(nx * totalForce, ny * totalForce)
this.bodyB.applyForce(-nx * totalForce, -ny * totalForce)
}
}
class Rope {
constructor(x, y, segments, segmentLength) {
this.points = []
for (let i = 0; i <= segments; i++) {
this.points.push({
x: x,
y: y + i * segmentLength,
oldX: x,
oldY: y + i * segmentLength,
pinned: i === 0
})
}
this.segmentLength = segmentLength
}
update(gravity) {
this.points.forEach(point => {
if (point.pinned) return
const vx = (point.x - point.oldX) * 0.99
const vy = (point.y - point.oldY) * 0.99
point.oldX = point.x
point.oldY = point.y
point.x += vx
point.y += vy + gravity
})
for (let i = 0; i < 5; i++) {
this.constrain()
}
}
constrain() {
for (let i = 0; i < this.points.length - 1; i++) {
const p1 = this.points[i]
const p2 = this.points[i + 1]
const dx = p2.x - p1.x
const dy = p2.y - p1.y
const distance = Math.sqrt(dx * dx + dy * dy)
const diff = (this.segmentLength - distance) / distance
if (!p1.pinned && !p2.pinned) {
p1.x -= dx * diff * 0.5
p1.y -= dy * diff * 0.5
p2.x += dx * diff * 0.5
p2.y += dy * diff * 0.5
} else if (!p1.pinned) {
p1.x -= dx * diff
p1.y -= dy * diff
} else if (!p2.pinned) {
p2.x += dx * diff
p2.y += dy * diff
}
}
}
draw(ctx) {
ctx.strokeStyle = '#8b4513'
ctx.lineWidth = 3
ctx.beginPath()
ctx.moveTo(this.points[0].x, this.points[0].y)
this.points.forEach(p => {
ctx.lineTo(p.x, p.y)
})
ctx.stroke()
ctx.fillStyle = '#333'
this.points.forEach(p => {
ctx.beginPath()
ctx.arc(p.x, p.y, p.pinned ? 6 : 4, 0, Math.PI * 2)
ctx.fill()
})
}
}
class Cloth {
constructor(x, y, width, height, resolution) {
this.points = []
this.resolution = resolution
this.spacing = width / resolution
for (let j = 0; j <= resolution; j++) {
for (let i = 0; i <= resolution; i++) {
this.points.push({
x: x + i * this.spacing,
y: y + j * this.spacing,
oldX: x + i * this.spacing,
oldY: y + j * this.spacing,
pinned: j === 0 && (i === 0 || i === resolution)
})
}
}
}
update(gravity) {
this.points.forEach(point => {
if (point.pinned) return
const vx = (point.x - point.oldX) * 0.99
const vy = (point.y - point.oldY) * 0.99
point.oldX = point.x
point.oldY = point.y
point.x += vx
point.y += vy + gravity
})
for (let i = 0; i < 3; i++) {
this.constrain()
}
}
constrain() {
const res = this.resolution
for (let j = 0; j <= res; j++) {
for (let i = 0; i <= res; i++) {
const idx = j * (res + 1) + i
const point = this.points[idx]
if (i < res) {
this.constrainPoints(point, this.points[idx + 1])
}
if (j < res) {
this.constrainPoints(point, this.points[idx + res + 1])
}
}
}
}
constrainPoints(p1, p2) {
const dx = p2.x - p1.x
const dy = p2.y - p1.y
const distance = Math.sqrt(dx * dx + dy * dy)
const diff = (this.spacing - distance) / distance
if (!p1.pinned && !p2.pinned) {
p1.x -= dx * diff * 0.5
p1.y -= dy * diff * 0.5
p2.x += dx * diff * 0.5
p2.y += dy * diff * 0.5
} else if (!p1.pinned) {
p1.x -= dx * diff
p1.y -= dy * diff
} else if (!p2.pinned) {
p2.x += dx * diff
p2.y += dy * diff
}
}
draw(ctx) {
const res = this.resolution
for (let j = 0; j < res; j++) {
for (let i = 0; i < res; i++) {
const idx = j * (res + 1) + i
const p1 = this.points[idx]
const p2 = this.points[idx + 1]
const p3 = this.points[idx + res + 1]
const p4 = this.points[idx + res + 2]
ctx.fillStyle = `hsl(${200 + i * 5}, 70%, 60%)`
ctx.beginPath()
ctx.moveTo(p1.x, p1.y)
ctx.lineTo(p2.x, p2.y)
ctx.lineTo(p4.x, p4.y)
ctx.lineTo(p3.x, p3.y)
ctx.closePath()
ctx.fill()
}
}
}
}
function eulerIntegrate(body, dt) {
body.vx += body.ax * dt
body.vy += body.ay * dt
body.x += body.vx * dt
body.y += body.vy * dt
}
function verletIntegrate(point, dt) {
const vx = (point.x - point.oldX) * 0.99
const vy = (point.y - point.oldY) * 0.99 + 0.5 * dt
point.oldX = point.x
point.oldY = point.y
point.x += vx
point.y += vy
}
function rk4Integrate(state, derivative, dt) {
const k1 = derivative(state, 0)
const k2 = derivative(state, dt * 0.5, k1)
const k3 = derivative(state, dt * 0.5, k2)
const k4 = derivative(state, dt, k3)
return {
x: state.x + (k1.x + 2 * k2.x + 2 * k3.x + k4.x) / 6 * dt,
y: state.y + (k1.y + 2 * k2.y + 2 * k3.y + k4.y) / 6 * dt
}
}