屏幕按钮和摇杆
手机上没有键盘,屏幕按钮和摇杆就是虚拟按键。它们在独立的包 @sapling2d/touch 里,作为插件安装:
sh
pnpm add @sapling2d/touchplugins: [touch()] 会预加载一套默认皮肤(灰白半透明的圆),不传贴图也能看到控件。
屏幕按钮
ts
import { CanvasLayer, key, Rect2, Scene, tex, v } from 'sapling2d'
import { touch, TouchScreenButton } from '@sapling2d/touch'
declare module 'sapling2d' {
interface ActionRegistry {
left: true
right: true
jump: true
}
}
// 键盘和屏幕按钮共用同一个动作名:浏览器里用方向键,小游戏里用按钮
export const actions = { left: [key('ArrowLeft')], right: [key('ArrowRight')], jump: [key('Space')] }
export const plugins = [touch()] // 屏幕控件是插件:预加载默认皮肤
const PAD = new Rect2(-90, -90, 180, 180) // 触摸区域比按钮图片大,手指不容易按偏
export class Controls extends Scene {
static override assets = { arrow: tex('arrow.png'), jump: tex('jump.png') }
jumps = 0
override ready() {
const hud = this.add(new CanvasLayer()) // 按钮固定在屏幕上,不跟随相机
const { arrow, jump } = Controls.assets
hud.add(new TouchScreenButton({ name: 'Left', action: 'left', texture: arrow, flipH: true, position: v(110, 1220), hitArea: PAD }))
hud.add(new TouchScreenButton({ name: 'Right', action: 'right', texture: arrow, position: v(300, 1220), hitArea: PAD }))
hud.add(new TouchScreenButton({ name: 'Jump', action: 'jump', texture: jump, position: v(640, 1220), hitArea: PAD }))
}
override physicsProcess() {
// 和键盘一样查询动作;physicsProcess 里的 isActionJustPressed 按物理步算
if (this.tree.input.isActionJustPressed('jump')) this.jumps++
}
}TouchScreenButton(继承 Sprite2D)按住时,它的 action 处于按下状态。动作名和键盘绑定共用,isActionPressed / isActionJustPressed 照常用,游戏代码不用区分输入来自哪里。
| 成员 | 说明 |
|---|---|
action | 按下时触发的动作,应该是已定义的动作(可以是没有绑定的 []) |
texture / texturePressed | 平时和按下时的贴图;不传用默认皮肤,null 只做输入不显示 |
hitArea | 触摸区域,可以比贴图大(推荐);不设时用贴图范围 |
isPressed、信号 pressed / released | 按下状态 |
- 每个手指独立。滑出按钮、抬起、触摸取消、切到后台都会松开。
- 滑出后再滑回来不算按下,从别处滑进来也不算。需要滑动换方向的方向键,用后面讲的
fixed摇杆。 - 和
pointerPress()的区别:pointerPress()是"屏幕上任意一处没被节点处理的按下";按在屏幕按钮上的手指不会触发它,也不会点中下面的节点。 - 放进
CanvasLayer,固定在屏幕上。隐藏或暂停时按不下,按着的会在下一帧松开。 - 拾取按绘制顺序:画在上面的可点击节点(比如弹出的对话框)先收到指针;一个手指只按最上层的那个按钮。
摇杆
ts
import { CanvasLayer, CharacterBody2D, key, rectangle, Scene, tex, v } from 'sapling2d'
import { touch, TouchJoystick } from '@sapling2d/touch'
declare module 'sapling2d' {
interface ActionRegistry {
left: true
right: true
up: true
down: true
}
}
// 键盘和摇杆共用四个方向的动作:浏览器里用 WASD,小游戏里用摇杆
export const actions = { left: [key('KeyA')], right: [key('KeyD')], up: [key('KeyW')], down: [key('KeyS')] }
export const plugins = [touch()]
class Hero extends CharacterBody2D {
constructor() {
super({ shape: rectangle(40, 40), position: v(375, 667) })
}
override physicsProcess() {
// 长度 0–1(键盘斜着按也不超过 1);推得越远走得越快
const dir = this.tree.input.getVector('left', 'right', 'up', 'down')
this.setVelocity(dir.x * 300, dir.y * 300)
this.moveAndSlide()
}
}
export class Field extends Scene {
static override assets = { base: tex('arrow.png'), knob: tex('jump.png') }
hero!: Hero
override ready() {
this.hero = this.add(new Hero())
const hud = this.add(new CanvasLayer()) // 摇杆固定在屏幕上,不跟随相机
// dynamic(默认):在屏幕左半边按下的地方出现,松手后隐藏
hud.add(new TouchJoystick({ actions: { left: 'left', right: 'right', up: 'up', down: 'down' }, radius: 100, texture: Field.assets.base, textureKnob: Field.assets.knob }))
}
}ts
const input = g.tree.input
g.pointerDown(200, 1000) // 左半边:摇杆出现在这里
g.pointerMove(250, 1000) // 向右推了半径的一半:扣掉 20% 死区,力度 (0.5 - 0.2) / 0.8 = 0.375
g.step()
expect(input.getActionStrength('right')).toBeCloseTo(0.375)
expect(input.isActionPressed('right')).toBe(false) // 力度 ≥ 0.5 才算按下
const x0 = g.scene.hero.x
g.pointerMove(400, 1000) // 推到底:每秒 300 像素
g.stepSeconds(1)
expect(g.scene.hero.x - x0).toBeCloseTo(300, 0)动作的力度
动作有力度(0–1)。按键、pointerPress()、屏幕按钮按下时是 1;摇杆按推动的程度给出 0–1(已经扣掉死区)。几个来源同时作用时取最大值。力度 ≥ 0.5 时动作算按下。
| 读取 | 返回 |
|---|---|
getActionStrength(action) | 0–1 |
getAxis(neg, pos) | -1 到 1,不分配内存 |
getVector(negX, posX, negY, posY) | 长度不超过 1 的 Vector2;每次分配一个,热循环里用 getAxis |
TouchJoystick
TouchJoystick(继承 Node2D)的常用属性:
| 属性 | 说明 |
|---|---|
actions | { left, right, up, down } 四个方向对应的动作 |
radius | 推到这么远力度为 1,默认 100 |
deadzone | 死区,默认 0.2 |
texture / textureKnob | 底座和摇杆头;不设用默认皮肤,null 不显示 |
vectorX / vectorY / vector | 当前方向(前两个不分配内存) |
isPressed、信号 pressed / released | 按下状态 |
两种模式:
mode: 'dynamic'(默认):在region里按下的地方出现,松手后隐藏。region默认是可见区域的左半边(设计坐标)。mode: 'fixed':固定在position,在hitArea里按下才算(默认是半径 1.5 ×radius的圆)。
方向键
只配 left / right、radius 很小、deadzone: 0 的 fixed 摇杆就是一个方向键:按在哪边就是哪边,手指滑过去换方向不用抬起。
- 只认一个手指:按着摇杆的手指不触发
pointerPress()、不点中下面的节点、不会滑进屏幕按钮。另一个手指可以同时按屏幕按钮。 - 抬起、触摸取消、切到后台、隐藏、暂停、移出树时松开,力度归零。
- 放进
CanvasLayer。dynamic的区域里,画在它上面的按钮先收到指针,所以先加摇杆、后加按钮。摇杆本身不要旋转或缩放。
自己写控件
屏幕按钮和摇杆只用了引擎的公开能力:节点拾取(inputPickable、覆写 hitTest、pointerDown / pointerMove / pointerUp)加一个 InputSource。自己的控件可以照着写,见 插件。