跳转到内容

相机

每个场景都通过一个相机渲染:一个带 Camera 组件的实体。相机控制缩放、投影,以及世界里 哪一片会到达屏幕。本指南涵盖配置相机、跟随目标、在屏幕与世界空间之间转换、震屏与混合视图, 以及同时运行多个相机。

你可以有很多相机实体,但同一时刻只有一个是活动的。引擎按 isActive + priority 选出活动相机(优先级最高的活动相机胜出),并把世界渲染到它的 viewport(一个屏幕矩形)里。 2D 游戏用**正交(Orthographic)**投影,此时 orthoSize——可见半高(世界单位)——就是缩放: 越小越放大。

生成一个正交相机并设为活动:

import { defineSystem, Commands, Transform, Camera, ProjectionType } from 'esengine';
const spawnCamera = defineSystem([Commands()], (cmds) => {
cmds.spawn()
.insert(Transform, { position: { x: 0, y: 0, z: 0 } })
.insert(Camera, {
projectionType: ProjectionType.Orthographic,
orthoSize: 540, // half-height in world units → zoom
isActive: true,
});
});

SDK 默认值为 2D 调好(正交、活动、orthoSize 540)。移动相机就是移动它实体的 Transform

属性 类型 默认 说明
projectionType ProjectionType Orthographic Orthographic(2D)或 Perspective
orthoSize number 540 可见半高(世界单位,正交)——你的缩放。可动画。
fov number 60 视场角(度,仅透视;1–179)。
nearPlane number 0.1 近裁剪距离。
farPlane number 1000 远裁剪距离。
aspectRatio number 1.77 宽 / 高。0 = 从视口自动推算。
isActive boolean true 该相机是否有资格成为活动相机。
priority number 0 活动相机之间的优先级(高者胜)。
viewport Vec4 {0,0,1,1} 屏幕矩形 x, y, w, h(0–1,分屏/画中画)。
clearFlags ClearFlags ColorAndDepth 渲染前清什么:Nothing / Color / Depth / ColorAndDepth
pixelPerfect boolean false 吸附到世界像素网格,让像素画清晰(正交)。
showFrustum boolean false 仅编辑器:绘制相机的视锥 gizmo。

orthoSize 是缩放控制——减半即让屏上尺寸翻倍。因为它是 animatable 的,最平滑的缩放方式 是用一个 tween:

import { defineSystem, Res, Tween, TweenTarget } from 'esengine';
const zoomIn = defineSystem([Res(Tween)], (tween) => {
tween.to(cameraEntity, TweenTarget.CameraOrthoSize, 270, 0.5); // zoom to 2×
});

Perspective 只用于伪 3D 的分层场景;2D 游戏保持 Orthographic,尺寸才不随深度变化。

发布的游戏里,主相机通常不会按原始 orthoSize 渲染。引擎每帧解析一个适配源,当其 生效时,把项目的设计分辨率(默认 1920×1080)letterbox 进实际屏幕宽高比——可见半高由 designResolution.y / 2 加一个缩放模式算出,让世界单位在任何设备上都是设计像素。适配源 按此顺序:

  1. ScreenScaling 资源(项目设置 → 显示 → 相机适配),当其 scaleMode 是真实模式 (≥ 0)时;
  2. 否则,场景的 Canvas 组件(若存在);
  3. 否则无——相机按原始 orthoSize 渲染。

默认 orthoSize540,恰是 designResolution.y / 2,所以有无适配的视图在设计宽高比 下一致。缩放模式、letterbox 实例与 ScreenScaling 字段详见 屏幕与设计分辨率

给相机加一个 FollowTarget 并指向一个实体。跟随是帧率无关的阻尼移动,带一个死区,所以相机 平滑漂移并忽略细小移动:

相机把目标保持在离视图中心固定的 offset;目标在死区圆内自由漫游,一旦离开相机就阻尼跟上

import { defineSystem, Query, Commands, Camera, FollowTarget } from 'esengine';
const follow = defineSystem([Query(Camera), Commands()], (cameras, cmds) => {
for (const [cameraEntity] of cameras) {
cmds.entity(cameraEntity).insert(FollowTarget, {
target: playerEntity, // the Entity to follow
offsetY: 40,
deadzone: 24,
damping: 0.25,
});
}
});
属性 类型 默认 说明
target Entity -1 要跟随的实体(-1 = 无)。
offsetX number 0 相对目标的世界空间 X 偏移。
offsetY number 0 相对目标的世界空间 Y 偏移。
deadzone number 0 目标可漫游而相机不动的半径(世界单位)。
damping number 0.25 平滑时间常数(秒,越大越慢;0 = 立即吸附)。

数据形状导出为 FollowTargetData 类型。跟随系统直接写相机的 Transform(所以 director 混合的是已跟随的视图),且只在 play 模式运行——编辑器的导航视图永远不会被它拖走。

CameraView 资源在两个空间之间转换——用于把实体放到光标下,或检测什么可见。无活动相机 时每个方法返回 null:

import { defineSystem, Res, CameraView, Input } from 'esengine';
const pick = defineSystem([Res(CameraView), Res(Input)], (view, input) => {
const world = view.screenToWorld(input.mouseX, input.mouseY); // { x, y } | null
const screen = view.worldToScreen(x, y); // { x, y } | null
const mouse = view.getWorldMousePosition(); // { x, y } | null
const bounds = view.getWorldBounds(); // { left, right, bottom, top } | null
});
方法 返回 说明
screenToWorld(sx, sy) {x, y} | null 屏幕像素 → 世界位置。
worldToScreen(wx, wy) {x, y} | null 世界位置 → 屏幕像素。
getWorldMousePosition() {x, y} | null 世界空间里的光标。
getWorldBounds() {left, right, bottom, top} | null 可见的世界矩形。

shakeCamera 只给渲染的视图加一个衰减扰动——绝不碰 Transform,所以相机总会恢复,场景 保持干净:

import { shakeCamera } from 'esengine';
shakeCamera(app, { amplitude: 12, rotation: 0, frequency: 22, duration: 0.4 });

shakeCamerasetViewTarget 的第一个参数既可以是 App(宿主代码),也可以是 CameraDirector 资源状态——在系统里直接传 Res(CameraDirector) 给你的值:

import { defineSystem, Res, Input, CameraDirector, shakeCamera } from 'esengine';
const shakeOnHit = defineSystem([Res(Input), Res(CameraDirector)], (input, director) => {
if (input.isKeyPressed('Space')) shakeCamera(director, { amplitude: 16 });
});
选项 默认 说明
amplitude 12 峰值位置偏移(世界单位)。
rotation 0 峰值旋转震动(弧度)。
frequency 22 每秒振荡次数。
duration 0.4 衰减到零的秒数。

setViewTarget 把活动视图切到另一个相机实体,可选地在 time 秒里混合而非硬切——用于 电影化的镜头交接:

import { setViewTarget, BlendCurve } from 'esengine';
// Cut instantly:
setViewTarget(app, cutsceneCamera);
// Or ease over 1.5s:
setViewTarget(app, cutsceneCamera, { time: 1.5, curve: BlendCurve.EaseInOut });

BlendCurve 取值为 LinearEaseInEaseOutEaseInOuttime <= 0(或没有先前视图) 即硬切。位置、缩放、旋转会插值;离散字段(投影、视口)在中点翻转。

setViewTargetshakeCamera相机 director 的 API——一个 per-app 资源 (CameraDirector),每帧从全屏相机候选中解析出一个主视点:已提交的视图目标(或活动/ 最高优先级相机),过渡进行中则向新目标混合,震屏只叠加在渲染视图之上。其状态 (CameraDirectorState)可读;把它当只读,用 setViewTarget / shakeCamera 去改:

字段 类型 默认 说明
target number -1 已提交的视图目标实体;-1 = 回退到 isActive / 最高 priority 的选择。
blending boolean false 视图目标混合正在进行。
duration / curve number 0 / EaseInOut 当前混合的时长(秒)与缓动。
currentMain CameraPOV | null null 最后解析出的主 POV,未加震屏——也是屏幕↔世界转换所用的视图。
shakes ActiveShake[] [] 活动的瞬时震屏(由 shakeCamera 压入,衰减完即丢弃)。

其余字段(hasPendingpendingTargetpendingTimependingCurvefromstartTimeshakeSeq)是 setViewTarget 写入的 director 内部请求/混合簿记。

import { defineSystem, Res, CameraDirector } from 'esengine';
const watchDirector = defineSystem([Res(CameraDirector)], (director) => {
if (director.blending) { /* e.g. suppress player input during the hand-off */ }
const pov = director.currentMain; // CameraPOV | null — the view being rendered
if (pov) { /* pov.x, pov.y, pov.orthoSize, ... */ }
});

CameraPOV 是相机被作者的视图参数的一个纯快照——director 混合的单位,也是 currentMain 存的形状:

字段 类型 说明
entity number 来源相机实体,-1 表示合成 POV(编辑器视图)。
isActive boolean 权威的“这就是主相机”标志。
x / y / z number 相机位置(世界单位)。
rotation number Z 旋转,弧度。
projection number 一个 ProjectionType 值。
orthoSize number 作者写的正交半高(世界单位)。
fov number 视场角,度(透视)。
near / far number 裁剪距离。
viewport {x, y, z, w} 屏幕矩形,0–1(z/w 是宽/高)。
clearFlags number 一个 ClearFlags 值。
priority number 活动相机间的优先级。
pixelPerfect boolean 像素网格吸附标志。

混合期间,连续字段(位置、按最短角路径的 rotationorthoSizefovnear/far) 插值;离散字段(projectionpixelPerfect)在中点翻转,viewport / clearFlags / priority 取目标值。

updateCameraAspectRatio(world, aspect)aspectRatio 写到世界里每个 Camera 组件上。各发布运行时启动时用真实画布的 width / height 调用它,让作者字段与实际表面保持 同步(渲染本身每帧从活动视口推导宽高比):

import { updateCameraAspectRatio } from 'esengine';
updateCameraAspectRatio(app.world, canvas.width / canvas.height);

有多个 Camera 实体时,isActive + priority 决定谁是活动的。用 viewport 给每个相机一个 子矩形来共享屏幕——例如双人分屏,左右各半:

.insert(Camera, { viewport: { x: 0, y: 0, w: 0.5, h: 1 }, isActive: true }); // left
.insert(Camera, { viewport: { x: 0.5, y: 0, w: 0.5, h: 1 }, isActive: true }); // right
  • orthoSize 驱动缩放,别用 Transform 缩放——缩放相机实体会扭曲渲染。
  • 跟随带死区,相机才不会因细小移动抖动;调大 damping 得到更慵懒、电影化的感觉。
  • 震屏而非移动:冲击用 shakeCamera,视图总能恢复。
  • 像素画开 pixelPerfect 消除亚像素抖动。
  • 场景与过场切换优先用混合(带 timesetViewTarget),而不是瞬移相机。
  • 屏幕与设计分辨率 —— 设计分辨率适配、缩放模式、 ScreenScalingpixelPerfect 的深入讲解。
  • 动画 —— 用 tween 缩放 orthoSize
  • 输入 —— 用鼠标配 screenToWorld 做拾取。
  • UI —— UI 在屏幕空间渲染,与相机无关。