Skip to content

平台游戏的角色 ​

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 / isOnCeiling
  • slideCollisionCount / getSlideCollision(i):撞到的 tileMap、cellX、cellY、normal(对象会复用,不要存起来)。

单向平台 ​

图块属性 collision: 'oneWay' 的格子只在角色下落、且脚底原来在平台顶面以上时挡住它,可以从下面跳上去。速度再快也不会穿过去。

collisionMask 和图层的 collisionLayer 有交集才碰撞。

限制 ​

  • 只和 TileMapLayer 的格子碰撞。它不是刚体:StaticBody2D 挡不住它(重叠时会警告一次),RigidBody2D / Area2D 也感知不到它。墙和地面都画进图块地图。
  • 没有斜坡。
  • 碰撞盒不随角色自己的 rotation / scale 变化:翻转贴图用子节点 Sprite2D 的 flipH。角色的祖先、图层和图层的祖先只能平移,旋转、缩放会报错。
  • 角色之间不互相阻挡:主角和敌人、金币之间用 HitTester 判断。
  • 图块集里没有任何碰撞图块的图层(纯装饰)不参与碰撞。

MIT 许可证