平台游戏的角色
CharacterBody2D 是代码控制移动的角色:游戏自己算速度,引擎负责让它贴着图块地图的墙和地面滑动,不会穿墙。
ts
import { CharacterBody2D, key, rectangle, Scene, TileMapLayer, tileset, v } from 'sapling2d'
declare module 'sapling2d' {
interface ActionRegistry {
left: true
right: true
jump: true
}
}
const TILES = tileset('tiles.png', { tileSize: 16, tiles: { 1: { collision: 'solid' }, 2: { collision: 'oneWay' } } })
const LEVEL = [
'....................',
'.........222........', // 单向平台:从下面能跳上去
'....................',
'....................',
'11111111111111111111',
]
const GRAVITY = 1200
const SPEED = 90
const JUMP = -380
export class Player extends CharacterBody2D {
/** 最近一次顶到的格子。 */
bumped = ''
constructor() {
super({ name: 'Player', shape: rectangle(12, 16), position: v(40, 40) }) // 碰撞盒以节点位置为中心
}
// 移动写在 physicsProcess 里(固定 60Hz);moveAndSlide 只能在这里调用
override physicsProcess(dt: number) {
const input = this.tree.input
const dir = (input.isActionPressed('right') ? 1 : 0) - (input.isActionPressed('left') ? 1 : 0)
// 重力每一步都加,站在地上也加:被地面挡住才算 isOnFloor
let vy = this.velocityY + GRAVITY * dt
// physicsProcess 里的 isActionJustPressed 按物理步算,高刷新率屏幕上也不会丢
if (this.isOnFloor && input.isActionJustPressed('jump')) vy = JUMP
this.setVelocity(dir * SPEED, vy) // 用 velocityX / velocityY / setVelocity,不分配 Vector2
this.moveAndSlide()
for (let i = 0; i < this.slideCollisionCount; i++) {
const hit = this.getSlideCollision(i)
if (hit.normal.y > 0) this.bumped = `${hit.cellX},${hit.cellY}` // 顶到了格子
}
}
}
export class Level extends Scene {
static override assets = { tiles: TILES }
player!: Player
override ready() {
const ids: Record<string, number> = { '.': 0, '1': 1, '2': 2 }
this.add(
new TileMapLayer({
tileSet: TILES,
width: LEVEL[0]!.length,
height: LEVEL.length,
cells: LEVEL.join('').split('').map((c) => ids[c]!),
position: v(0, 100),
}),
)
this.player = this.add(new Player())
}
}
export const options = { main: Level, actions: { left: [key('ArrowLeft')], right: [key('ArrowRight')], jump: [key('Space')] } }移动
shape是rectangle(w, h):以节点位置为中心、轴对齐,不随 rotation / scale 变化。- 每个物理步设置速度(
velocityX/velocityY/setVelocity(x, y),或velocity),然后调用moveAndSlide():先移动 x 再移动 y,被实心格挡住时贴着格子停下,那一轴的速度清零。 moveAndSlide()只能在physicsProcess里调用。- 重力、加速度、跳跃都由游戏自己写。重力每一步都要加,站在地上也加,否则
isOnFloor为 false。 - 跳跃用
isActionJustPressed判断:在physicsProcess里它按物理步算,一次按下只在一个物理步里为 true。
碰撞结果
isOnFloor/isOnWall/isOnCeilingslideCollisionCount/getSlideCollision(i):撞到的tileMap、cellX、cellY、normal(对象会复用,不要存起来)。
单向平台
图块属性 collision: 'oneWay' 的格子只在角色下落、且脚底原来在平台顶面以上时挡住它,可以从下面跳上去。速度再快也不会穿过去。
collisionMask 和图层的 collisionLayer 有交集才碰撞。
限制
- 只和
TileMapLayer的格子碰撞。它不是刚体:StaticBody2D挡不住它(重叠时会警告一次),RigidBody2D/Area2D也感知不到它。墙和地面都画进图块地图。 - 没有斜坡。
- 碰撞盒不随角色自己的 rotation / scale 变化:翻转贴图用子节点
Sprite2D的flipH。角色的祖先、图层和图层的祖先只能平移,旋转、缩放会报错。 - 角色之间不互相阻挡:主角和敌人、金币之间用
HitTester判断。 - 图块集里没有任何碰撞图块的图层(纯装饰)不参与碰撞。