pin_drop当前位置:知识文库 ❯ 图文
游戏开发:游戏物理基础 - 从入门到实践详解
教程简介
物理模拟是让游戏世界感觉真实和有趣的关键因素。从简单的重力下落到复杂的弹跳和摩擦,物理系统赋予游戏对象符合直觉的运动行为。本教程将全面讲解游戏物理的基础知识,包括运动学(速度/加速度)、重力模拟、弹跳、摩擦力、简单物理引擎的实现,以及物理引擎的集成(Matter.js/Cannon.js),帮助你构建具有真实物理行为的游戏。
核心概念
运动学基础
运动学描述物体的运动状态,不涉及力的原因:
代码示例
class Kinematics {
constructor() {
this.position = { x: 0, y: 0 };
this.velocity = { x: 0, y: 0 };
this.acceleration = { x: 0, y: 0 };
}
update(dt) {
// v = v0 + a * t
this.velocity.x += this.acceleration.x * dt;
this.velocity.y += this.acceleration.y * dt;
// s = s0 + v * t
this.position.x += this.velocity.x * dt;
this.position.y += this.velocity.y * dt;
}
}重力模拟
重力是最基本的物理效果,让物体向下加速:
代码示例
const GRAVITY = 980; // 像素/秒^2
class PhysicsBody {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.vx = 0;
this.vy = 0;
this.gravity = GRAVITY;
this.isOnGround = false;
this.restitution = 0.5; // 弹性系数
this.friction = 0.8; // 摩擦系数
}
update(dt) {
// 应用重力
this.vy += this.gravity * dt;
// 更新位置
this.x += this.vx * dt;
this.y += this.vy * dt;
// 地面碰撞
if (this.y + this.height > groundY) {
this.y = groundY - this.height;
this.vy = -this.vy * this.restitution; // 弹跳
this.vx *= this.friction; // 摩擦
this.isOnGround = true;
// 速度过小时停止弹跳
if (Math.abs(this.vy) < 10) {
this.vy = 0;
}
}
}
}弹跳
弹跳通过弹性系数(restitution)控制反弹高度:
代码示例
// 弹性系数范围:0(完全非弹性)到1(完全弹性)
// 弹跳高度 = 下落高度 * restitution^2
function applyBounce(body, surfaceNormal, restitution) {
// 计算速度在法线方向的分量
const dot = body.vx * surfaceNormal.x + body.vy * surfaceNormal.y;
// 反射速度
body.vx -= (1 + restitution) * dot * surfaceNormal.x;
body.vy -= (1 + restitution) * dot * surfaceNormal.y;
}摩擦力
摩擦力减缓物体在接触面上的运动:
代码示例
function applyFriction(body, friction) {
if (body.isOnGround) {
// 动摩擦
if (Math.abs(body.vx) > 1) {
body.vx *= friction;
} else {
body.vx = 0; // 静摩擦
}
}
}简单物理引擎实现
代码示例
class SimplePhysicsEngine {
constructor(width, height) {
this.width = width;
this.height = height;
this.bodies = [];
this.gravity = 980;
}
addBody(body) {
this.bodies.push(body);
return body;
}
removeBody(body) {
const idx = this.bodies.indexOf(body);
if (idx >= 0) this.bodies.splice(idx, 1);
}
update(dt) {
for (const body of this.bodies) {
if (body.isStatic) continue;
// 应用重力
body.vy += this.gravity * dt;
// 应用阻力
body.vx *= (1 - body.drag * dt);
// 更新位置
body.x += body.vx * dt;
body.y += body.vy * dt;
// 边界碰撞
this.resolveBoundaries(body);
// 更新旋转
body.rotation += body.angularVelocity * dt;
}
// 碰撞检测与响应
this.resolveCollisions();
}
resolveBoundaries(body) {
if (body.x < 0) {
body.x = 0;
body.vx = -body.vx * body.restitution;
}
if (body.x + body.width > this.width) {
body.x = this.width - body.width;
body.vx = -body.vx * body.restitution;
}
if (body.y < 0) {
body.y = 0;
body.vy = -body.vy * body.restitution;
}
if (body.y + body.height > this.height) {
body.y = this.height - body.height;
body.vy = -body.vy * body.restitution;
body.vx *= body.friction;
body.isOnGround = Math.abs(body.vy) < 5;
if (body.isOnGround) body.vy = 0;
}
}
resolveCollisions() {
for (let i = 0; i < this.bodies.length; i++) {
for (let j = i + 1; j < this.bodies.length; j++) {
const a = this.bodies[i];
const b = this.bodies[j];
if (a.isStatic && b.isStatic) continue;
this.resolveBodyCollision(a, b);
}
}
}
resolveBodyCollision(a, b) {
if (a.x < b.x + b.width && a.x + a.width > b.x &&
a.y < b.y + b.height && a.y + a.height > b.y) {
const overlapX = Math.min(a.x + a.width - b.x, b.x + b.width - a.x);
const overlapY = Math.min(a.y + a.height - b.y, b.y + b.height - a.y);
if (overlapX < overlapY) {
const sign = (a.x + a.width / 2) < (b.x + b.width / 2) ? -1 : 1;
if (!a.isStatic) a.x += sign * overlapX / 2;
if (!b.isStatic) b.x -= sign * overlapX / 2;
const restitution = Math.min(a.restitution, b.restitution);
if (!a.isStatic && !b.isStatic) {
const tempVx = a.vx;
a.vx = b.vx * restitution;
b.vx = tempVx * restitution;
} else if (a.isStatic) {
b.vx = -b.vx * restitution;
} else {
a.vx = -a.vx * restitution;
}
} else {
const sign = (a.y + a.height / 2) < (b.y + b.height / 2) ? -1 : 1;
if (!a.isStatic) a.y += sign * overlapY / 2;
if (!b.isStatic) b.y -= sign * overlapY / 2;
const restitution = Math.min(a.restitution, b.restitution);
if (!a.isStatic && !b.isStatic) {
const tempVy = a.vy;
a.vy = b.vy * restitution;
b.vy = tempVy * restitution;
} else if (a.isStatic) {
b.vy = -b.vy * restitution;
} else {
a.vy = -a.vy * restitution;
}
}
}
}
}Matter.js集成
Matter.js是一个功能强大的2D物理引擎:
代码示例
// 引入Matter.js
// <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
const { Engine, Render, Runner, Bodies, Composite, Events } = Matter;
// 创建引擎
const engine = Engine.create();
const render = Render.create({
element: document.body,
engine: engine,
canvas: document.getElementById('gameCanvas'),
options: { width: 800, height: 600, wireframes: false }
});
// 创建地面和墙壁
const ground = Bodies.rectangle(400, 580, 800, 40, { isStatic: true });
const wallLeft = Bodies.rectangle(10, 300, 20, 600, { isStatic: true });
const wallRight = Bodies.rectangle(790, 300, 20, 600, { isStatic: true });
Composite.add(engine.world, [ground, wallLeft, wallRight]);
// 创建动态物体
for (let i = 0; i < 20; i++) {
const body = Bodies.rectangle(
200 + Math.random() * 400,
50 + Math.random() * 200,
30 + Math.random() * 40,
30 + Math.random() * 40,
{ restitution: 0.6, friction: 0.3 }
);
Composite.add(engine.world, body);
}
// 运行
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);语法与用法
完整的物理引擎演示
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏物理基础</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: 'Segoe UI', sans-serif; color: #fff; }
canvas { border: 2px solid #2a2a4a; border-radius: 8px; }
.controls { margin-top: 12px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
button { padding: 6px 16px; border: 1px solid #4a90d9; background: transparent; color: #4a90d9; border-radius: 6px; cursor: pointer; font-size: 13px; }
button:hover { background: #4a90d922; }
label { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #aaa; }
input[type="range"] { width: 80px; }
</style>
</head>
<body>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="controls">
<button id="addBox">添加方块</button>
<button id="addBall">添加球</button>
<button id="addPlatform">添加平台</button>
<button id="reset">重置</button>
<label>重力: <input type="range" id="gravitySlider" min="0" max="2000" value="980"> <span id="gravityVal">980</span></label>
<label>弹性: <input type="range" id="bounceSlider" min="0" max="100" value="60"> <span id="bounceVal">0.6</span></label>
</div>
<script>
class PhysicsBody {
constructor(x, y, width, height, options = {}) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.vx = options.vx || 0;
this.vy = options.vy || 0;
this.restitution = options.restitution !== undefined ? options.restitution : 0.6;
this.friction = options.friction !== undefined ? options.friction : 0.95;
this.isStatic = options.isStatic || false;
this.isCircle = options.isCircle || false;
this.radius = options.radius || width / 2;
this.mass = options.mass || (width * height);
this.color = options.color || this.randomColor();
this.rotation = 0;
this.angularVelocity = options.angularVelocity || 0;
this.isOnGround = false;
this.id = PhysicsBody.nextId++;
}
randomColor() {
const colors = ['#4a90d9', '#e94560', '#16c79a', '#f0a500', '#533483', '#ff6b6b', '#4ecdc4'];
return colors[Math.floor(Math.random() * colors.length)];
}
}
PhysicsBody.nextId = 0;
class PhysicsEngine {
constructor(width, height) {
this.width = width;
this.height = height;
this.bodies = [];
this.gravity = 980;
this.platforms = [];
}
addBody(body) {
this.bodies.push(body);
return body;
}
addPlatform(x, y, width, height) {
const platform = new PhysicsBody(x, y, width, height, {
isStatic: true,
color: '#2a4a6a'
});
this.platforms.push(platform);
return platform;
}
clear() {
this.bodies = [];
this.platforms = [];
}
update(dt) {
const fixedDt = 1 / 60;
this.step(fixedDt);
}
step(dt) {
for (const body of this.bodies) {
if (body.isStatic) continue;
// 重力
body.vy += this.gravity * dt;
// 空气阻力
body.vx *= 0.999;
// 更新位置
body.x += body.vx * dt;
body.y += body.vy * dt;
// 旋转
body.rotation += body.angularVelocity * dt;
body.angularVelocity *= 0.99;
body.isOnGround = false;
// 边界碰撞
this.resolveBoundary(body);
// 平台碰撞
for (const platform of this.platforms) {
this.resolvePlatformCollision(body, platform);
}
}
// 动态体之间碰撞
this.resolveBodyCollisions();
}
resolveBoundary(body) {
const r = body.isCircle ? body.radius : 0;
if (body.x - r < 0) {
body.x = r;
body.vx = Math.abs(body.vx) * body.restitution;
body.angularVelocity += body.vy * 0.01;
}
if (body.x + body.width + r > this.width) {
body.x = this.width - body.width - r;
body.vx = -Math.abs(body.vx) * body.restitution;
body.angularVelocity -= body.vy * 0.01;
}
if (body.y - r < 0) {
body.y = r;
body.vy = Math.abs(body.vy) * body.restitution;
}
if (body.y + body.height > this.height) {
body.y = this.height - body.height;
body.vy = -Math.abs(body.vy) * body.restitution;
body.vx *= body.friction;
body.isOnGround = Math.abs(body.vy) < 5;
if (body.isOnGround) {
body.vy = 0;
body.angularVelocity *= 0.9;
}
}
}
resolvePlatformCollision(body, platform) {
if (body.isCircle) {
this.resolveCircleRect(body, platform);
} else {
this.resolveRectRect(body, platform);
}
}
resolveRectRect(body, platform) {
if (body.x < platform.x + platform.width &&
body.x + body.width > platform.x &&
body.y < platform.y + platform.height &&
body.y + body.height > platform.y) {
const overlapLeft = body.x + body.width - platform.x;
const overlapRight = platform.x + platform.width - body.x;
const overlapTop = body.y + body.height - platform.y;
const overlapBottom = platform.y + platform.height - body.y;
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapTop && body.vy > 0) {
body.y = platform.y - body.height;
body.vy = -body.vy * body.restitution;
body.vx *= body.friction;
body.isOnGround = Math.abs(body.vy) < 5;
if (body.isOnGround) body.vy = 0;
} else if (minOverlap === overlapBottom && body.vy < 0) {
body.y = platform.y + platform.height;
body.vy = -body.vy * body.restitution;
} else if (minOverlap === overlapLeft) {
body.x = platform.x - body.width;
body.vx = -Math.abs(body.vx) * body.restitution;
} else if (minOverlap === overlapRight) {
body.x = platform.x + platform.width;
body.vx = Math.abs(body.vx) * body.restitution;
}
}
}
resolveCircleRect(circle, rect) {
const cx = circle.x + circle.radius;
const cy = circle.y + circle.radius;
const closestX = Math.max(rect.x, Math.min(cx, rect.x + rect.width));
const closestY = Math.max(rect.y, Math.min(cy, rect.y + rect.height));
const dx = cx - closestX;
const dy = cy - closestY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < circle.radius && dist > 0) {
const nx = dx / dist;
const ny = dy / dist;
const overlap = circle.radius - dist;
circle.x += nx * overlap;
circle.y += ny * overlap;
const dot = circle.vx * nx + circle.vy * ny;
circle.vx -= (1 + circle.restitution) * dot * nx;
circle.vy -= (1 + circle.restitution) * dot * ny;
if (ny < -0.5) {
circle.isOnGround = true;
circle.vx *= circle.friction;
}
circle.angularVelocity += (circle.vx * ny - circle.vy * nx) * 0.01;
}
}
resolveBodyCollisions() {
for (let i = 0; i < this.bodies.length; i++) {
for (let j = i + 1; j < this.bodies.length; j++) {
const a = this.bodies[i];
const b = this.bodies[j];
if (a.isStatic && b.isStatic) continue;
if (a.isCircle && b.isCircle) {
this.resolveCircleCircle(a, b);
} else if (a.isCircle || b.isCircle) {
const circle = a.isCircle ? a : b;
const rect = a.isCircle ? b : a;
this.resolveCircleRectDynamic(circle, rect);
} else {
this.resolveRectRectDynamic(a, b);
}
}
}
}
resolveCircleCircle(a, b) {
const dx = (b.x + b.radius) - (a.x + a.radius);
const dy = (b.y + b.radius) - (a.y + a.radius);
const dist = Math.sqrt(dx * dx + dy * dy);
const radiusSum = a.radius + b.radius;
if (dist < radiusSum && dist > 0) {
const nx = dx / dist;
const ny = dy / dist;
const overlap = radiusSum - dist;
const totalMass = a.mass + b.mass;
if (!a.isStatic) { a.x -= nx * overlap * (b.mass / totalMass); a.y -= ny * overlap * (b.mass / totalMass); }
if (!b.isStatic) { b.x += nx * overlap * (a.mass / totalMass); b.y += ny * overlap * (a.mass / totalMass); }
const dvx = a.vx - b.vx;
const dvy = a.vy - b.vy;
const dvn = dvx * nx + dvy * ny;
if (dvn > 0) {
const restitution = Math.min(a.restitution, b.restitution);
const j = -(1 + restitution) * dvn / (1 / a.mass + 1 / b.mass);
if (!a.isStatic) { a.vx += j * nx / a.mass; a.vy += j * ny / a.mass; }
if (!b.isStatic) { b.vx -= j * nx / b.mass; b.vy -= j * ny / b.mass; }
}
}
}
resolveCircleRectDynamic(circle, rect) {
const cx = circle.x + circle.radius;
const cy = circle.y + circle.radius;
const closestX = Math.max(rect.x, Math.min(cx, rect.x + rect.width));
const closestY = Math.max(rect.y, Math.min(cy, rect.y + rect.height));
const dx = cx - closestX;
const dy = cy - closestY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < circle.radius && dist > 0) {
const nx = dx / dist;
const ny = dy / dist;
const overlap = circle.radius - dist;
if (!circle.isStatic) { circle.x += nx * overlap * 0.5; circle.y += ny * overlap * 0.5; }
if (!rect.isStatic) { rect.x -= nx * overlap * 0.5; rect.y -= ny * overlap * 0.5; }
const restitution = Math.min(circle.restitution, rect.restitution);
const dvx = circle.vx - rect.vx;
const dvy = circle.vy - rect.vy;
const dvn = dvx * nx + dvy * ny;
if (dvn > 0) {
const j = -(1 + restitution) * dvn / 2;
if (!circle.isStatic) { circle.vx += j * nx; circle.vy += j * ny; }
if (!rect.isStatic) { rect.vx -= j * nx; rect.vy -= j * ny; }
}
}
}
resolveRectRectDynamic(a, b) {
if (a.x < b.x + b.width && a.x + a.width > b.x &&
a.y < b.y + b.height && a.y + a.height > b.y) {
const overlapX = Math.min(a.x + a.width - b.x, b.x + b.width - a.x);
const overlapY = Math.min(a.y + a.height - b.y, b.y + b.height - a.y);
if (overlapX < overlapY) {
const sign = (a.x + a.width / 2) < (b.x + b.width / 2) ? -1 : 1;
if (!a.isStatic) a.x += sign * overlapX * 0.5;
if (!b.isStatic) b.x -= sign * overlapX * 0.5;
const restitution = Math.min(a.restitution, b.restitution);
const tempVx = a.vx;
if (!a.isStatic) a.vx = b.vx * restitution;
if (!b.isStatic) b.vx = tempVx * restitution;
} else {
const sign = (a.y + a.height / 2) < (b.y + b.height / 2) ? -1 : 1;
if (!a.isStatic) a.y += sign * overlapY * 0.5;
if (!b.isStatic) b.y -= sign * overlapY * 0.5;
const restitution = Math.min(a.restitution, b.restitution);
const tempVy = a.vy;
if (!a.isStatic) a.vy = b.vy * restitution;
if (!b.isStatic) b.vy = tempVy * restitution;
}
}
}
}
// ========== 主程序 ==========
class PhysicsDemo {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.width = 800;
this.height = 600;
this.engine = new PhysicsEngine(this.width, this.height);
this.setupDefaultScene();
this.setupControls();
this.lastTime = 0;
this.mouseX = 0;
this.mouseY = 0;
this.dragBody = null;
this.dragOffsetX = 0;
this.dragOffsetY = 0;
}
setupDefaultScene() {
// 默认平台
this.engine.addPlatform(200, 450, 200, 15);
this.engine.addPlatform(450, 350, 200, 15);
this.engine.addPlatform(100, 250, 150, 15);
this.engine.addPlatform(550, 200, 180, 15);
// 默认物体
for (let i = 0; i < 5; i++) {
this.engine.addBody(new PhysicsBody(
200 + Math.random() * 400, 50 + Math.random() * 100,
25 + Math.random() * 25, 25 + Math.random() * 25,
{ restitution: 0.6, angularVelocity: (Math.random() - 0.5) * 2 }
));
}
for (let i = 0; i < 3; i++) {
const r = 12 + Math.random() * 15;
this.engine.addBody(new PhysicsBody(
250 + Math.random() * 300, 30 + Math.random() * 80,
r * 2, r * 2,
{ isCircle: true, radius: r, restitution: 0.7, angularVelocity: (Math.random() - 0.5) * 3 }
));
}
}
setupControls() {
document.getElementById('addBox').addEventListener('click', () => {
this.engine.addBody(new PhysicsBody(
300 + Math.random() * 200, 30,
25 + Math.random() * 30, 25 + Math.random() * 30,
{ restitution: parseFloat(document.getElementById('bounceSlider').value) / 100,
angularVelocity: (Math.random() - 0.5) * 3 }
));
});
document.getElementById('addBall').addEventListener('click', () => {
const r = 12 + Math.random() * 18;
this.engine.addBody(new PhysicsBody(
300 + Math.random() * 200, 30,
r * 2, r * 2,
{ isCircle: true, radius: r, restitution: parseFloat(document.getElementById('bounceSlider').value) / 100,
angularVelocity: (Math.random() - 0.5) * 3 }
));
});
document.getElementById('addPlatform').addEventListener('click', () => {
this.engine.addPlatform(100 + Math.random() * 500, 200 + Math.random() * 300, 100 + Math.random() * 150, 15);
});
document.getElementById('reset').addEventListener('click', () => {
this.engine.clear();
this.setupDefaultScene();
});
document.getElementById('gravitySlider').addEventListener('input', (e) => {
this.engine.gravity = parseInt(e.target.value);
document.getElementById('gravityVal').textContent = e.target.value;
});
document.getElementById('bounceSlider').addEventListener('input', (e) => {
document.getElementById('bounceVal').textContent = (parseInt(e.target.value) / 100).toFixed(2);
});
// 拖拽
this.canvas.addEventListener('mousedown', (e) => {
const rect = this.canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (this.width / rect.width);
const my = (e.clientY - rect.top) * (this.height / rect.height);
for (const body of this.engine.bodies) {
if (body.isStatic) continue;
if (body.isCircle) {
const dx = mx - (body.x + body.radius);
const dy = my - (body.y + body.radius);
if (dx * dx + dy * dy < body.radius * body.radius) {
this.dragBody = body;
this.dragOffsetX = mx - body.x;
this.dragOffsetY = my - body.y;
break;
}
} else {
if (mx > body.x && mx < body.x + body.width && my > body.y && my < body.y + body.height) {
this.dragBody = body;
this.dragOffsetX = mx - body.x;
this.dragOffsetY = my - body.y;
break;
}
}
}
});
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
this.mouseX = (e.clientX - rect.left) * (this.width / rect.width);
this.mouseY = (e.clientY - rect.top) * (this.height / rect.height);
if (this.dragBody) {
const targetX = this.mouseX - this.dragOffsetX;
const targetY = this.mouseY - this.dragOffsetY;
this.dragBody.vx = (targetX - this.dragBody.x) * 10;
this.dragBody.vy = (targetY - this.dragBody.y) * 10;
this.dragBody.x = targetX;
this.dragBody.y = targetY;
}
});
this.canvas.addEventListener('mouseup', () => {
this.dragBody = null;
});
}
start() {
this.lastTime = performance.now();
requestAnimationFrame((t) => this.loop(t));
}
loop(currentTime) {
const dt = Math.min((currentTime - this.lastTime) / 1000, 0.1);
this.lastTime = currentTime;
this.engine.update(dt);
this.render();
requestAnimationFrame((t) => this.loop(t));
}
render() {
const ctx = this.ctx;
// 背景
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, this.width, this.height);
// 网格
ctx.strokeStyle = '#0f0f2a';
ctx.lineWidth = 0.5;
for (let x = 0; x < this.width; x += 50) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, this.height); ctx.stroke();
}
for (let y = 0; y < this.height; y += 50) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(this.width, y); ctx.stroke();
}
// 平台
for (const p of this.engine.platforms) {
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, p.width, p.height);
ctx.fillStyle = '#4a90d9';
ctx.fillRect(p.x, p.y, p.width, 3);
}
// 动态体
for (const body of this.engine.bodies) {
if (body.isCircle) {
ctx.save();
ctx.translate(body.x + body.radius, body.y + body.radius);
ctx.rotate(body.rotation);
ctx.fillStyle = body.color;
ctx.beginPath();
ctx.arc(0, 0, body.radius, 0, Math.PI * 2);
ctx.fill();
// 旋转指示线
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(body.radius * 0.7, 0);
ctx.stroke();
ctx.restore();
} else {
ctx.save();
ctx.translate(body.x + body.width / 2, body.y + body.height / 2);
ctx.rotate(body.rotation);
ctx.fillStyle = body.color;
ctx.fillRect(-body.width / 2, -body.height / 2, body.width, body.height);
ctx.restore();
}
}
// 拖拽线
if (this.dragBody) {
ctx.strokeStyle = '#fff4';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(this.mouseX, this.mouseY);
ctx.lineTo(this.dragBody.x + this.dragBody.width / 2, this.dragBody.y + this.dragBody.height / 2);
ctx.stroke();
ctx.setLineDash([]);
}
// HUD
ctx.fillStyle = '#fff';
ctx.font = '13px monospace';
ctx.textAlign = 'left';
ctx.fillText(`物体: ${this.engine.bodies.length} | 平台: ${this.engine.platforms.length}`, 10, 20);
ctx.fillText(`重力: ${this.engine.gravity}`, 10, 40);
ctx.fillText('拖拽物体移动', 10, 580);
}
}
const game = new PhysicsDemo();
game.start();
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
使用固定时间步长:物理模拟必须使用固定时间步长(通常1/60秒),确保不同帧率下物理行为一致。
限制最大速度:防止物体速度过大导致穿透碰撞体或数值溢出。
弹性系数范围:restitution应在0到1之间,超过1会导致能量增加(超弹性),物理行为不稳定。
多次迭代求解:碰撞响应可能一次无法完全分离,需要多次迭代。
物理与渲染分离:物理更新使用固定步长,渲染使用实际帧时间插值。
避免每帧创建对象:物理计算中避免创建临时对象,减少GC压力。
使用Matter.js等成熟引擎:对于复杂物理需求,使用Matter.js比自己实现更可靠高效。
调试可视化:绘制速度向量、碰撞法线、包围盒等辅助调试。
代码规范示例
代码示例
/**
* 物理体组件
* 描述游戏对象的物理属性
*/
class PhysicsComponent {
/**
* @param {Object} config - 物理配置
* @param {boolean} [config.isStatic=false] - 是否为静态体
* @param {number} [config.mass=1] - 质量
* @param {number} [config.restitution=0.5] - 弹性系数 (0-1)
* @param {number} [config.friction=0.3] - 摩擦系数
* @param {number} [config.drag=0.01] - 空气阻力
*/
constructor(config = {}) {
this.isStatic = config.isStatic || false;
this.mass = config.isStatic ? Infinity : (config.mass || 1);
this.invMass = this.isStatic ? 0 : (1 / this.mass);
this.restitution = Math.max(0, Math.min(1, config.restitution || 0.5));
this.friction = Math.max(0, Math.min(1, config.friction || 0.3));
this.drag = config.drag || 0.01;
this.velocity = { x: 0, y: 0 };
this.acceleration = { x: 0, y: 0 };
this.angularVelocity = 0;
this.force = { x: 0, y: 0 };
}
/**
* 施加力
* @param {number} fx - X方向力
* @param {number} fy - Y方向力
*/
applyForce(fx, fy) {
this.force.x += fx;
this.force.y += fy;
}
/**
* 施加冲量
* @param {number} ix - X方向冲量
* @param {number} iy - Y方向冲量
*/
applyImpulse(ix, iy) {
this.velocity.x += ix * this.invMass;
this.velocity.y += iy * this.invMass;
}
/**
* 物理更新
* @param {number} dt - 固定时间步长
*/
integrate(dt) {
if (this.isStatic) return;
// a = F/m
this.acceleration.x = this.force.x * this.invMass;
this.acceleration.y = this.force.y * this.invMass;
// v += a * dt
this.velocity.x += this.acceleration.x * dt;
this.velocity.y += this.acceleration.y * dt;
// 阻力
this.velocity.x *= (1 - this.drag);
this.velocity.y *= (1 - this.drag);
// 清除力
this.force.x = 0;
this.force.y = 0;
}
}常见问题与解决方案
问题1:物体穿透地面
原因:速度过快或时间步长过大,一帧内物体跨越了碰撞体。
解决方案:使用固定时间步长,限制最大速度,或实现连续碰撞检测。
问题2:物体堆叠时抖动
原因:碰撞分离不彻底,或重力持续作用导致反复碰撞。
解决方案:增加碰撞迭代次数,设置速度阈值(低于阈值时归零),使用睡眠机制。
问题3:物理模拟不稳定
原因:时间步长过大、弹性系数过高、力计算错误。
解决方案:使用固定时间步长,限制弹性系数不超过1,检查力的方向和大小。
问题4:圆形物体滚动不自然
原因:没有正确实现角速度和摩擦力矩。
解决方案:在碰撞时根据切线速度计算角速度变化,添加滚动摩擦。
问题5:Matter.js性能问题
原因:物体过多、碰撞分类不当、渲染开销大。
解决方案:使用碰撞过滤减少检测对数,启用睡眠机制,使用wireframe模式调试。
总结
本教程全面讲解了游戏物理的基础知识。我们学习了运动学的基本公式(速度、加速度、位移的关系),实现了重力模拟和弹跳效果,掌握了摩擦力的应用,构建了一个完整的简单物理引擎(支持矩形和圆形碰撞、弹性碰撞响应、平台碰撞),并了解了如何集成Matter.js等专业物理引擎。
关键要点:
物理模拟必须使用固定时间步长确保行为一致
重力是持续向下的加速度
弹性系数控制反弹高度,范围0-1
摩擦力减缓接触面上的运动
碰撞响应需要同时处理速度变化和位置分离
复杂物理需求建议使用Matter.js等成熟引擎
调试时可视化速度向量和碰撞法线
限制最大速度防止穿透和数值不稳定
掌握物理基础后,你将能够为游戏添加真实的物理交互,让游戏世界更加生动有趣。
常见问题
什么是游戏物理基础?
游戏物理基础是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习游戏物理基础的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
游戏物理基础有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
游戏物理基础适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
游戏物理基础的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别