关于 physics-matter
面向 Phaser 4 开发者的 Matter.js 物理技能,说明如何创建和配置刚体、约束及复合体,并处理碰撞和瓦片地图。还介绍传感器、碰撞筛选、指针拖拽与调试配置。
主要功能
- 01创建精灵、图像及自定义刚体
- 02配置重力、摩擦、速度和力
- 03创建约束、复合体与复合刚体
- 04碰撞事件、传感器和碰撞筛选
- 05瓦片地图物理与指针拖拽
适用场景
- 01为角色和环境设置刚体碰撞
- 02用约束构建弹簧、关节或链条
- 03实现拾取物和触发区域检测
- 04将瓦片地图碰撞转换为 Matter 刚体
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ phaserjs/ phaser --skill physics-matter安装后在 Claude Code 中输入/physics-matter确认已加载。
详细介绍
Matter.js 物理
在 Phaser 4 中设置并使用 Matter.js 物理 —— 支持刚体、复合刚体、约束、组合体、传感器、碰撞过滤、指针拖拽、Tilemap 集成和调试渲染的完整刚体物理。
关键源码路径: src/physics/matter-js/MatterPhysics.js, src/physics/matter-js/World.js, src/physics/matter-js/Factory.js, src/physics/matter-js/MatterSprite.js, src/physics/matter-js/MatterImage.js, src/physics/matter-js/MatterGameObject.js, src/physics/matter-js/PointerConstraint.js, src/physics/matter-js/MatterTileBody.js, src/physics/matter-js/components/, src/physics/matter-js/events/, src/physics/matter-js/typedefs/
相关技能: ../game-setup-and-config/SKILL.md, ../sprites-and-images/SKILL.md, ../physics-arcade/SKILL.md, ../tilemaps/SKILL.md
快速开始
class GameScene extends Phaser.Scene {
create() {
// Matter sprite (dynamic, has animation support)
this.player = this.matter.add.sprite(400, 200, 'player');
this.player.setBounce(0.5);
this.player.setFriction(0.05);
// Matter image (dynamic, no animation)
const box = this.matter.add.image(300, 100, 'crate');
// Static body from raw shape
this.matter.add.rectangle(400, 580, 800, 40, { isStatic: true });
// Enable pointer dragging on all bodies
this.matter.add.mouseSpring();
this.cursors = this.input.keyboard.createCursorKeys();
}
update() {
if (this.cursors.left.isDown) {
this.player.setVelocityX(-5);
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(5);
}
if (this.cursors.up.isDown && this.player.body.velocity.y > -0.1) {
this.player.setVelocityY(-10);
}
}
}
// Enable Matter physics in game config
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: {
default: 'matter',
matter: {
gravity: { y: 1 },
enableSleeping: true,
debug: true,
setBounds: true // walls around canvas edges
}
},
scene: GameScene
};
const game = new Phaser.Game(config);
核心概念
场景插件(this.matter)
MatterPhysics 类是场景级插件。关键属性:
this.matter.add——Factory,用于创建刚体、约束、游戏对象(自动添加到世界)。this.matter.world——World实例,管理引擎、边界、调试渲染。继承EventEmitter。this.matter.body/bodies/composite/composites/constraint—— 直接引用 Matter.js 模块以进行底层使用。this.matter.bodyBounds—— 用于按可视边界对齐刚体的辅助工具。
世界(this.matter.world)
engine——MatterJS.Engine实例。localWorld—— 包含所有刚体和约束的MatterJS.World组合体。enabled—— 布尔值;false暂停模拟。autoUpdate——true= 引擎在每个游戏步更新。walls——{ left, right, top, bottom }边界墙刚体(或 null)。
世界配置(MatterWorldConfig)
在游戏或场景配置中通过 physics.matter 传入:
| 属性 | 默认值 | 用途 |
|---|---|---|
gravity | { x: 0, y: 1 } | 重力向量。设为 false 禁用 |
setBounds | false | true 或 { x, y, width, height, thickness, left, right, top, bottom } |
enableSleeping | false | 允许刚体静止时休眠 |
positionIterations | 6 | 位置求解精度 |
velocityIterations | 4 | 速度求解精度 |
constraintIterations | 2 | 约束稳定性 |
timing.timeScale | 1 | 0 冻结,0.5 慢动作 |
autoUpdate | true | 每个游戏帧自动步进 |
debug | false | true 或 MatterDebugConfig 对象 |
runner | {} | 使用 runner.fps 设置固定时间步 |
Matter 游戏对象
Phaser 提供了两个物理感知的游戏对象类,以及一个可为任意游戏对象添加物理的函数:
Phaser.Physics.Matter.Sprite—— 继承Sprite,包含所有 Matter 组件。通过this.matter.add.sprite(x, y, key, frame, options)创建。支持动画。Phaser.Physics.Matter.Image—— 继承Image,包含所有 Matter 组件。通过this.matter.add.image(x, y, key, frame, options)创建。不支持动画,更轻量。MatterGameObject(world, gameObject, options)—— 将所有 Matter 组件注入任意现有游戏对象。通过this.matter.add.gameObject(mySprite, options)创建。
MatterSprite 和 MatterImage 默认都使用与纹理尺寸匹配的矩形刚体。传入 options.shape 可覆盖。
Matter 组件(Mixin)
所有 Matter 游戏对象都混入了这些组件方法:
| 组件 | 关键方法 |
|---|---|
| Velocity | setVelocity(x, y)、setVelocityX(x)、setVelocityY(y)、getVelocity()、setAngularVelocity(v)、getAngularVelocity()、setAngularSpeed(s)、getAngularSpeed() |
| Force | applyForce(vec2)、applyForceFrom(position, force)、thrust(speed)、thrustLeft(speed)、thrustRight(speed)、thrustBack(speed) |
| Bounce | setBounce(value) -- 恢复系数,0 到 1 |
| Friction | setFriction(value, air?, fstatic?)、setFrictionAir(value)、setFrictionStatic(value) |
| Mass | setMass(value)、setDensity(value)、centerOfMass(getter) |
| Gravity | setIgnoreGravity(bool) |
| Sensor | setSensor(bool)、isSensor() |
| Static | setStatic(bool)、isStatic() |
| Sleep | setToSleep()、setAwake()、setSleepThreshold(n)、setSleepEvents(start, end) |
| Collision | setCollisionCategory(cat)、setCollisionGroup(group)、setCollidesWith(cats)、setOnCollide(cb)、setOnCollideEnd(cb)、setOnCollideActive(cb)、setOnCollideWith(body, cb) |
| SetBody | setRectangle(w, h, opts)、setCircle(r, opts)、setPolygon(r, sides, opts)、setTrapezoid(w, h, slope, opts)、setBody(config, opts)、setExistingBody(body) |
| Transform | Matter body 与游戏对象之间的位置同步 |
常见模式
设置与世界配置
// Runtime gravity and bounds
this.matter.world.setGravity(0, 2); // x, y, scale (default 0.001)
this.matter.world.disableGravity();
this.matter.world.setBounds(0, 0, 1600, 1200, 64, true, true, true, true);
this.matter.set60Hz(); // fixed timestep
this.matter.world.autoUpdate = false; // then manual: this.matter.step(16.666);
this.matter.pause(); // pause/resume
this.matter.resume();
创建 Matter Sprite 和 Image
const player = this.matter.add.sprite(200, 300, 'hero'); // default rect body matching texture
// Custom body shapes via options.shape
const ball = this.matter.add.image(400, 100, 'ball', null, { shape: { type: 'circle', radius: 24 } });
const hex = this.matter.add.sprite(300, 100, 'hex', null, { shape: { type: 'polygon', sides: 6, radius: 32 } });
const ship = this.matter.add.sprite(400, 200, 'ship', null, {
shape: { type: 'fromVerts', verts: '0 0 40 0 40 40 20 60 0 40' }
});
// PhysicsEditor shape data
const shapes = this.cache.json.get('shapes');
const enemy = this.matter.add.sprite(500, 200, 'enemy', null, { shape: shapes.enemy });
// Add Matter physics to an existing Game Object
const existingSprite = this.add.sprite(100, 100, 'box');
this.matter.add.gameObject(existingSprite, { restitution: 0.8 });
// existingSprite now has setVelocity, setBounce, etc.
Body 配置选项(MatterBodyConfig)
作为 options 参数传递给任意工厂方法,或作为 Matter 游戏对象的选项:
label、isStatic、isSensor、angle(弧度)、timeScale、ignoreGravity、ignorePointerdensity(默认 0.001,自动计算质量)、mass、restitution(弹力 0-1)friction(0-1)、frictionAir(空气阻力)、frictionStatic(静止时的粘附力)slop(重叠容差)、chamfer({ radius: 5 }用于圆角)collisionFilter: { category: 0x0001, mask: 0xFFFFFFFF, group: 0 }onCollideCallback、onCollideEndCallback、onCollideActiveCallbackshape(用于游戏对象):{ type: 'circle', radius: 24 }或 PhysicsEditor 数据
速度、力与推力
sprite.setVelocity(3, -5); // units per step, not pixels/sec
sprite.setVelocityX(-3);
sprite.setAngularVelocity(0.05);
const vel = sprite.getVelocity(); // { x, y }
// Forces use very small values (0.01 - 0.1)
sprite.applyForce({ x: 0.05, y: 0 });
sprite.applyForceFrom(position, { x: 0.02, y: -0.02 });
// Directional thrust relative to body angle
sprite.thrust(0.05); // forward
sprite.thrustBack(0.05); // backward
sprite.thrustLeft(0.03); // strafe left
sprite.thrustRight(0.03); // strafe right
// Batch operations via this.matter
this.matter.setVelocity(arrayOfBodies, 2, -3);
this.matter.applyForce(arrayOfBodies, { x: 0.01, y: 0 });
约束(关节与弹簧)
// constraint(bodyA, bodyB, length?, stiffness?, options?) -- aliases: joint, spring
const rigid = this.matter.add.constraint(bodyA, bodyB, 100, 1); // rigid joint
const spring = this.matter.add.constraint(bodyA, bodyB, 200, 0.02, { damping: 0.05 });
const pin = this.matter.add.constraint(bodyA, bodyB, 0, 0.9); // pin joint
// World constraint (one body pinned to world point)
this.matter.add.worldConstraint(body, 50, 0.5, { pointA: { x: 400, y: 100 } });
// Offset attachment points
this.matter.add.constraint(bodyA, bodyB, 80, 1, {
pointA: { x: 20, y: 0 }, // offset from bodyA center
pointB: { x: -20, y: 0 } // offset from bodyB center
});
this.matter.getConstraintLength(constraint); // distance between anchor points
this.matter.world.removeConstraint(constraint); // remove from world
复合体(堆叠、链条、软体)
// Stack of bodies in a grid
const stack = this.matter.add.stack(100, 100, 5, 4, 10, 10, (x, y) => {
return this.matter.bodies.rectangle(x, y, 40, 40);
});
// Image stack (grid of Matter Images)
const imageStack = this.matter.add.imageStack('crate', null, 100, 100, 5, 4, 5, 5);
// Chain bodies in a composite together
this.matter.add.chain(stack, 0.5, 0, -0.5, 0, { stiffness: 0.7 });
// Mesh (grid with constraints, optional cross braces)
this.matter.add.mesh(stack, 5, 4, true, { stiffness: 0.5 });
// Soft body (cols, rows, gaps, crossBrace, particleRadius, bodyOpts, constraintOpts)
this.matter.add.softBody(200, 100, 5, 5, 0, 0, true, 10, { friction: 0.1 }, { stiffness: 0.5 });
// Pre-built composites: newtonsCradle, car, pyramid
this.matter.add.newtonsCradle(300, 50, 5, 20, 200);
this.matter.add.car(400, 300, 120, 30, 25);
复合 Body
将多个形状组合成单个 body。第一部分是父级。
const partA = this.matter.bodies.rectangle(0, 0, 60, 20);
const partB = this.matter.bodies.circle(0, -30, 15);
const compoundBody = this.matter.body.create({ parts: [partA, partB] });
// Attach to a Game Object
const player = this.matter.add.sprite(400, 200, 'hero');
player.setExistingBody(compoundBody);
各部分共享位置、角度和速度。约束必须指向父 body,而不是各部分。
睡眠系统
处于静止状态的 body 可以进入睡眠以跳过模拟。需要在配置中设置 enableSleeping: true。
if (body.isSleeping) { /* body is at rest */ }
sprite.setSleepThreshold(30); // lower = falls asleep faster (default 60)
sprite.setToSleep(); // force sleep
sprite.setAwake(); // force wake
sprite.setSleepEvents(true, true); // enable sleepstart/sleepend events
this.matter.world.on('sleepstart', (event, body) => { /* body slept */ });
this.matter.world.on('sleepend', (event, body) => { /* body woke */ });
传感器
传感器会检测碰撞,但不会产生物理反应。适用于触发区域、拾取物、检测区域。
const trigger = this.matter.add.rectangle(400, 300, 100, 100, { isSensor: true, isStatic: true });
sprite.setSensor(true); // toggle on Game Object
sprite.isSensor(); // check state
// Sensors fire normal collision events -- use collisionstart/end to detect entry/exit
碰撞类别与过滤
Matter 使用位掩码:category(该 body 属于哪个组,2 的幂)、mask(它与哪些类别碰撞)以及 group(快捷方式:相同正值 = 始终碰撞,相同负值 = 永不碰撞,0 = 使用 category/mask)。
const PLAYER = this.matter.world.nextCategory(); // 0x0002 (32 max)
const ENEMY = this.matter.world.nextCategory(); // 0x0004
const GROUND = this.matter.world.nextCategory(); // 0x0008
player.setCollisionCategory(PLAYER);
player.setCollidesWith([ENEMY, GROUND]);
bullet.setCollisionCategory(0x0010);
bullet.setCollidesWith([ENEMY, GROUND]); // bullets skip player
// Collision groups: same negative = never collide with each other
const noCollide = this.matter.world.nextGroup(true);
spriteA.setCollisionGroup(noCollide);
spriteB.setCollisionGroup(noCollide);
// Via body config: collisionFilter: { category, mask, group }
// Batch: this.matter.setCollisionCategory([bodyA, bodyB], ENEMY);
碰撞回调
// Per-body callbacks (on Matter Game Objects)
player.setOnCollide((pair) => { /* pair.bodyA, pair.bodyB */ });
player.setOnCollideEnd((pair) => { /* collision ended */ });
player.setOnCollideActive((pair) => { /* still colliding */ });
player.setOnCollideWith(enemy, (body, pair) => { /* hit specific body */ });
// Game Object-level events (emitted on the Game Object itself)
player.on('collide', (bodyA, bodyB, pair) => {});
player.on('collideEnd', (bodyA, bodyB, pair) => {});
瓦片地图集成
const map = this.make.tilemap({ key: 'level' });
const tileset = map.addTilesetImage('tiles', 'tiles-img');
const layer = map.createLayer('Ground', tileset, 0, 0);
layer.setCollisionByProperty({ collides: true }); // MUST set collision first
this.matter.world.convertTilemapLayer(layer); // creates MatterTileBody per colliding tile
// Uses Tiled collision shapes (rect, circle, polygon) if defined, otherwise tile bounds.
// Access: tile.physics.matterBody
// Individual tile: this.matter.add.tileBody(tile, { isStatic: true, friction: 0.5 });
// After map changes: this.matter.world.convertTiles([tile1, tile2]);
摩擦类型
Matter.js 有三个独立的摩擦值:
sprite.setFriction(0.1); // dynamic: resistance during motion (0-1)
sprite.setFrictionStatic(0.5); // static: resistance before motion starts
sprite.setFrictionAir(0.05); // air: environmental drag (default 0.01)
sprite.setFriction(0.1, 0.02, 0.3); // set all three: dynamic, air, static
由顶点构成的复杂形状
// Create body from vertex string (concave shapes auto-decomposed)
const body = this.matter.add.fromVertices(400, 300, '0 0 40 0 40 40 20 60 0 40');
// Multiple vertex sets for complex shapes
const vertexSets = [
[{ x: 0, y: 0 }, { x: 40, y: 0 }, { x: 40, y: 40 }],
[{ x: 40, y: 40 }, { x: 20, y: 60 }, { x: 0, y: 40 }]
];
this.matter.add.fromVertices(300, 200, vertexSets);
指针约束(鼠标/触摸拖拽)
// Enable click-and-drag on all Matter bodies
const pc = this.matter.add.mouseSpring({ stiffness: 0.2, damping: 0.1 });
pc.active = false; // disable temporarily
body.ignorePointer = true; // prevent specific body from being dragged
pc.stopDrag(); // release current drag programmatically
pc.destroy(); // remove entirely
// Drag events on the world
this.matter.world.on('dragstart', (body, part, constraint) => {});
this.matter.world.on('drag', (body, constraint) => {});
this.matter.world.on('dragend', (body, constraint) => {});
查询(射线检测与命中测试)
const hits = this.matter.intersectPoint(pointer.x, pointer.y); // bodies at point
const contains = this.matter.containsPoint(body, x, y); // point-in-body test
const inRegion = this.matter.intersectRect(100, 100, 200, 200); // bodies in rect
const rayHits = this.matter.intersectRay(0, 300, 800, 300, 1); // raycast
const colliding = this.matter.intersectBody(playerBody); // body overlap
// Overlap with callbacks
this.matter.overlap(playerBody, enemyBodies, (bodyA, bodyB, info) => {
console.log('Overlapping', bodyA, bodyB);
});
调试渲染
// Enable debug with specific options (pass as debug property in matter config)
// Boolean flags: showBody, showStaticBody, showVelocity, showCollisions, showSensors,
// showJoint, showPositions, showBounds, showAxes, showAngleIndicator, showSleeping,
// showConvexHulls, showInternalEdges, renderFill, renderLine
// Color/style: lineColor, lineThickness, fillColor, staticLineColor, staticFillColor,
// sensorLineColor, jointColor, pinColor, springColor, anchorColor, positionColor
// Toggle debug at runtime
this.matter.world.drawDebug = false;
this.matter.world.debugGraphic.visible = false;
// Set render style on individual body or constraint
this.matter.world.setBodyRenderStyle(body, 0xff0000, 1, 2, 0x00ff00, 0.5);
this.matter.world.setConstraintRenderStyle(constraint, 0xffff00, 1, 2);
事件
所有事件都在 this.matter.world(它继承自 EventEmitter)上触发:
| 事件 | 回调签名 | 触发时机 |
|---|---|---|
'collisionstart' | (event, bodyA, bodyB) | 两个刚体首次开始碰撞 |
'collisionactive' | (event, bodyA, bodyB) | 两个刚体仍在碰撞 |
'collisionend' | (event, bodyA, bodyB) | 两个刚体停止碰撞 |
'beforeupdate' | (event) | 引擎更新步骤之前 |
'afterupdate' | (event) | 引擎更新步骤之后 |
'beforeadd' | (event) | 添加刚体/约束之前 |
'afteradd' | (event) | 添加刚体/约束之后 |
'beforeremove' | (event) | 移除刚体/约束之前 |
'afterremove' | (event) | 移除刚体/约束之后 |
'dragstart' | (body, part, constraint) | 指针开始拖动刚体 |
'drag' | (body, constraint) | 指针正在拖动刚体 |
'dragend' | (body, constraint) | 指针停止拖动刚体 |
'sleepstart' | (event, body) | 刚体进入休眠(需要 setSleepEvents) |
'sleepend' | (event, body) | 刚体被唤醒(需要 setSleepEvents) |
'pause' | 无 | 世界已暂停 |
'resume' | 无 | 世界已恢复 |
碰撞事件包含 event.pairs —— 一个碰撞对对象数组,带有 bodyA、bodyB、碰撞深度和法线。
API 快速参考
this.matter.add(工厂)
游戏对象: sprite(x, y, key, frame?, opts?)、image(x, y, key, frame?, opts?)、gameObject(go, opts?)、tileBody(tile, opts?)
刚体形状: rectangle(x, y, w, h, opts?)、circle(x, y, r, opts?)、polygon(x, y, sides, r, opts?)、trapezoid(x, y, w, h, slope, opts?)、fromVertices(x, y, verts, opts?)、fromPhysicsEditor(x, y, config, opts?)、fromSVG(x, y, xml, scale?, opts?)、fromJSON(x, y, config, opts?)
约束: constraint(a, b, len?, stiff?, opts?)(别名:joint、spring)、worldConstraint(body, len?, stiff?, opts?)、mouseSpring(opts?)(别名:pointerConstraint)
复合体: stack(x, y, cols, rows, colGap, rowGap, cb)、imageStack(key, frame, x, y, cols, rows)、pyramid(...)、chain(composite, xA, yA, xB, yB, opts?)、mesh(composite, cols, rows, cross, opts?)、softBody(...)、car(x, y, w, h, wheelSize)、newtonsCradle(x, y, num, size, len)
this.matter(MatterPhysics)—— 批处理和实用方法
pause()、resume()、set60Hz()、set30Hz()、step(delta?)、setVelocity(bodies, x, y)、setAngularVelocity(bodies, v)、applyForce(bodies, force)、applyForceFromAngle(bodies, speed, angle?)、containsPoint(body, x, y)、intersectPoint(x, y)、intersectRect(x, y, w, h, outside?)、intersectRay(x1, y1, x2, y2, width?)、intersectBody(body)、overlap(target, bodies?, cb?)、setCollisionCategory(bodies, value)、setCollisionGroup(bodies, value)、setCollidesWith(bodies, cats)、alignBody(body, x, y, align)
this.matter.world(World)
setBounds(x?, y?, w?, h?, thickness?, l?, r?, t?, b?)、setGravity(x?, y?, scale?)、disableGravity()、add(object)、remove(object, deep?)、removeConstraint(constraint)、convertTilemapLayer(layer, opts?)、convertTiles(tiles, opts?)、nextCategory()、nextGroup(isNonColliding?)、getAllBodies()、has(body)、pause()、resume()
this.matter 上直接引用的 Matter.js 模块
body (Matter.Body)、bodies (Matter.Bodies)、composite (Matter.Composite)、composites (Matter.Composites)、constraint (Matter.Constraint)、detector、query、pair、pairs、resolver、axes、bounds、svg、vector、vertices
注意事项
- 力的值非常小。 力使用
0.01-0.1,速度使用1-15。不基于像素。 setBody/setRectangle/等会重置所有属性 —— 质量、摩擦力、碰撞过滤器、回调都会被清除。更改形状后需重新应用。- 约束必须针对父刚体,而不是复合刚体的
parts。 - 最多 32 个碰撞类别。 每次
nextCategory()使用一个位。 collisionFilter.group会覆盖类别/掩码。 相同的正值 = 始终碰撞;相同的负值 = 永不碰撞;零/不同 = 使用类别/掩码。- 传感器仍需匹配的碰撞过滤器才能触发事件。
- Matter 位置是质心,不是左上角(与 Arcade Physics 不同)。
- 睡眠事件需要主动启用:
sprite.setSleepEvents(true, true)。 - Tilemap 转换需要先设置碰撞,通过
setCollisionByProperty等。 - 弹性使用
Math.max(bodyA.restitution, bodyB.restitution)—— 弹性更大的值胜出。 body.ignorePointer = true会阻止指针约束拖拽该刚体。
源文件映射
| 路径 | 用途 |
|---|---|
src/physics/matter-js/MatterPhysics.js | 场景插件(this.matter),暴露所有 Matter 模块 |
src/physics/matter-js/World.js | 世界管理、引擎、边界、调试、事件代理 |
src/physics/matter-js/Factory.js | this.matter.add —— 所有创建方法 |
src/physics/matter-js/MatterSprite.js | 物理精灵(Sprite + Matter 组件) |
src/physics/matter-js/MatterImage.js | 物理图像(Image + Matter 组件) |
src/physics/matter-js/MatterGameObject.js | 将 Matter 组件注入任何游戏对象 |
src/physics/matter-js/MatterTileBody.js | 用 Matter 刚体包装 Tile |
src/physics/matter-js/PointerConstraint.js | 点击并拖拽刚体约束 |
src/physics/matter-js/BodyBounds.js | 按视觉边界对齐刚体 |
src/physics/matter-js/PhysicsEditorParser.js | 将 PhysicsEditor JSON 解析为刚体 |
src/physics/matter-js/components/ | 混入:Velocity、Force、Collision、SetBody、Sensor、Bounce、Friction、Mass、Gravity、Static、Sleep、Transform |
src/physics/matter-js/events/ | 事件常量(COLLISION_START、DRAG_START、SLEEP_START 等) |
src/physics/matter-js/typedefs/ | 类型定义:MatterWorldConfig、MatterBodyConfig、MatterCollisionFilter、MatterConstraintConfig 等 |
src/physics/matter-js/lib/ | 捆绑的 Matter.js 库模块 |