跳到主要内容

从 Unity 迁移

如果你曾在 Unity 中开发项目,PlayCanvas 的许多内容都会让你感到熟悉。PlayCanvas 编辑器同样具有层级面板、检查器、资源面板和 3D 视口。场景是实体构成的树,实体由组件组成,行为通过包含 initialize、update 等生命周期方法的脚本编写。

本指南将你熟悉的 Unity 知识对应到 PlayCanvas:编辑器面板和快捷键、常用概念的名称、C# API 对应的 JavaScript 写法,以及二者工作方式不同之处。各节均提供对应主题的完整文档链接。

主要差异​

  • **一切都运行在浏览器中。**编辑器是 Web 应用,项目存储在线上,发布的也是 Web 应用。无需等待 WebGL 构建:应用与编辑器视口使用同一个引擎。
  • **脚本使用 JavaScript。**你编写的是 JavaScript 模块,而非 C# 类,无需编译,保存脚本后即可运行。
  • **多个脚本共用一个组件。**Unity 的每个 MonoBehaviour 都是独立组件;PlayCanvas 的实体只有一个 Script 组件,用于容纳该实体的所有脚本。
  • **每个实体同类型组件最多一个。**实体最多具有一个相机、一个灯光、一个碰撞组件等,同一个脚本也只能添加一次。如果需要两个同类对象,例如两个碰撞体,请使用子实体。
  • **使用右手坐标系。**前方是 -Z,而非 +Z。参见坐标系与单位。
  • **没有 Play Mode。**Launch 会在独立浏览器标签页中运行场景,并保持与编辑器的连接。
  • **内置协作和版本控制。**多人可同时编辑同一个场景,检查点、分支和合并都是编辑器内置功能。

编辑器​

编辑器布局与 Unity 默认布局非常相似:

PlayCanvas 编辑器,工具栏、层级面板、视口、检查器和资源面板依次标记为 1 到 5

PlayCanvasUnity
1工具栏主菜单、变换工具和工具栏
2层级面板Hierarchy 窗口
3视口Scene 视图
4检查器Inspector 窗口
5资源面板Project 窗口

其他 Unity 窗口和菜单的对应项如下:

UnityPlayCanvas
Play 按钮和 Game 视图视口右上角的 Launch 按钮
Console 窗口启动标签页中的浏览器开发者工具控制台。编辑器左下角自己的 CONSOLE 面板显示脚本解析错误等编辑器消息。
Project Settings 和 Lighting 窗口工具栏齿轮图标打开的设置
Build Settings工具栏中的 Publish / Download
Animator 窗口选择动画状态图资源时打开的动画状态图编辑器
Asset Store资源面板右上角的 ASSET STORE,提供 PlayCanvas Store 和 Sketchfab 资源
Visual Studio 或 Rider内置代码编辑器,或安装了 VS Code 扩展的 VS Code
Unity Version Control 或 Git内置版本控制

使用 Launch 代替 Play Mode​

Unity 在编辑器内运行游戏。PlayCanvas 则在新的浏览器标签页,即运行页面中打开游戏,场景的运行方式与发布后相同。点击 Launch,或按 Ctrl+Enter 即可。

运行页面始终与编辑器连接。修改实体、组件、脚本属性、资源或场景设置时,运行中的应用会实时更新。这些修改与 Play Mode 中的修改不同,停止运行后不会丢失:编辑器会即时保存每项修改。可以将两个标签页并排放置。修改代码后通常需要重新加载,除非脚本支持热重载。

脚本不会在编辑器视口中运行,因此没有 Unity 的 [ExecuteInEditMode] 对应功能;脚本创建或修改的内容只会出现在已启动的应用中。

Launch 按钮的菜单提供测试选项:选择图形 API,打开 Profiler、MiniStats 或 Inspector,以及勾选 Debug,将引擎警告和错误输出到控制台。

操作UnityPlayCanvas
环绕Alt + 鼠标左键拖动鼠标左键拖动
环视鼠标右键拖动鼠标右键拖动
平移鼠标中键拖动鼠标中键拖动,或 Shift + 鼠标左键拖动
缩放鼠标滚轮鼠标滚轮
飞行按住鼠标右键,再按 W、A、S、D、Q、E直接按 W、A、S、D、Q、E,无需按住鼠标按键;按住 Shift 可加速
聚焦所选对象FF
框选在空白处按住鼠标左键拖动Ctrl + 鼠标左键拖动
增加或移除所选对象Shift+单击或 Ctrl+单击Ctrl+单击

键盘快捷键​

操作UnityPlayCanvas
移动工具W1
旋转工具E2
缩放工具R3
矩形工具(UI 元素)T4
切换操作器的局部和世界空间XL
聚焦所选对象FF
运行Ctrl+PCtrl+Enter(Launch)
创建空子对象Alt+Shift+NCtrl+E
复制对象Ctrl+DCtrl+D
重命名F2F2
删除DeleteDelete
撤销 / 重做Ctrl+Z / Ctrl+YCtrl+Z / Ctrl+Y
保存Ctrl+S无需手动保存,每次修改都会保存。Ctrl+S 会打开创建版本控制检查点的表单。
最大化视图Shift+SpaceSpace(隐藏层级、检查器和资源面板)
前视图、右视图和顶视图点击 Scene Gizmo 的坐标轴数字键盘 1、3、7(加 Ctrl 查看相反方向),或点击视图立方体的坐标轴
切换正交和透视点击 Scene Gizmo 中心数字键盘 5

在 macOS 上,用 Cmd 代替 Ctrl。Unity 的 View 工具(Q)没有对应项,因为视口导航不需要专门的工具。完整快捷键列表见操作与键盘快捷键。

概念与术语​

UnityPlayCanvas说明
GameObject实体
Component组件组件作为实体属性访问,例如 entity.camera、entity.light。
Transform每个实体自身的一部分实体本身具有位置、旋转和缩放。参见层级与变换。
MonoBehaviour脚本通过实体的 Script 组件附加。
Prefab模板参见预制体与模板。
Scene场景
Tag标签实体可以有任意数量的标签。
Layer图层仅用于渲染;物理使用碰撞组与掩码。
Mesh Filter 和 Mesh RendererRender 组件蒙皮网格同样使用 Render 组件。
Material材质
CameraCamera 组件
LightLight 组件点光源称为 omni light。
RigidbodyRigid Body 组件
ColliderCollision 组件没有刚体时,碰撞组件充当触发器。参见物理。
Audio SourceSound 组件一个组件可以播放任意数量的声音,各声音通过槽位组织。
Audio ListenerAudio Listener 组件
Particle SystemParticle System 组件
Animator 和 Animator ControllerAnim 组件与动画状态图资源
CanvasScreen 组件
RectTransform、Image 和 TextElement 组件
Sprite RendererSprite 组件
ScriptableObjectJSON 资源也可以使用导出数据的 JavaScript 模块。
Resources 和 Addressables资源注册表参见资源。

坐标系与单位​

两个引擎都以 Y 轴为上方,距离使用米,角度使用度。区别在于坐标系的手性:Unity 使用左手坐标系,+Z 为前方;PlayCanvas 使用右手坐标系,-Z 为前方。相机朝向自身的负 Z 轴,entity.forward 返回负 Z 轴方向,lookAt 则将负 Z 轴指向目标。

Unity 使用 +Z 向前的左手坐标系,PlayCanvas 使用 -Z 向前的右手坐标系

迁移代码和内容时,需要注意以下影响:

  • 向前移动就是沿 -Z 移动。transform.Translate(0, 0, speed * Time.deltaTime) 对应 this.entity.translateLocal(0, 0, -this.speed * dt)。
  • **正角度旋转的方向相反。**绕 Y 轴正向旋转会使实体向左转(从上方看为逆时针),绕 X 轴正向旋转则使实体向上抬头。Unity 中分别为右转和低头,因此在 PlayCanvas 中俯视场景的相机,其 X 轴旋转角度为负。
  • **欧拉角的应用顺序不同。**PlayCanvas 先绕 X,再绕 Y,最后绕 Z;Unity 则先绕 Z,再绕 X,最后绕 Y。加上坐标轴方向差异,Unity Inspector 中读取的旋转值不能直接照搬。应通过导出文件迁移布局,让格式转换处理差异,参见迁移项目内容。
  • **灯光沿 -Y 照射。**方向光和聚光灯朝向局部负 Y 轴,因此未旋转的灯光垂直向下照射;Unity 中则沿 +Z 照射。
  • **模型通常朝向 +Z。**glTF 将 +Z 定义为模型前方,从 Unity 导出的模型仍朝向 +Z,在 PlayCanvas 中这相当于朝后。如果脚本沿 entity.forward 移动角色,可将模型放到父实体下,绕 Y 轴旋转 180°,让模型面向父实体的前方。
  • **默认视野角度更窄。**两者都使用垂直视野角度,但 PlayCanvas 新相机默认为 45°,而不是 60°。

脚本编程​

PlayCanvas 脚本是继承 Script 的 JavaScript 类。每个脚本保存在资源面板中的 .mjs 文件内,为实体添加 Script 组件并选择脚本,即可将其附加到实体。完整指南见脚本编程。

脚本对照示例​

下面的 Unity 脚本让所属 GameObject 旋转,并通过空格键切换灯光:

using UnityEngine;

public class Spinner : MonoBehaviour
{
// Degrees per second
[SerializeField] float speed = 90f;
[SerializeField] Light lamp;

void Start()
{
lamp.enabled = false;
}

void Update()
{
transform.Rotate(0f, speed * Time.deltaTime, 0f);

if (Input.GetKeyDown(KeyCode.Space))
{
lamp.enabled = !lamp.enabled;
Debug.Log($"Lamp on: {lamp.enabled}");
}
}
}

下面是对应的 PlayCanvas 脚本,文件名为 spinner.mjs:

import { Entity, KEY_SPACE, Script } from 'playcanvas';

export class Spinner extends Script {
static scriptName = 'spinner';

/**
* Degrees per second.
*
* @attribute
*/
speed = 90;

/**
* @attribute
* @type {Entity}
*/
lamp;

initialize() {
this.lamp.light.enabled = false;
}

update(dt) {
this.entity.rotateLocal(0, this.speed * dt, 0);

if (this.app.keyboard.wasPressed(KEY_SPACE)) {
this.lamp.light.enabled = !this.lamp.light.enabled;
console.log(`Lamp on: ${this.lamp.light.enabled}`);
}
}
}

需要注意的区别:

  • **scriptName 指定脚本名称。**编辑器通过该名称列出脚本,其他脚本也通过它访问脚本实例,例如 entity.script.spinner。
  • **@attribute 代替 [SerializeField]。**在 JSDoc 注释中标记了 @attribute 的类字段会出现在 Inspector 中,注释的第一句话作为字段提示。参见检查器字段。
  • 实体属性引用实体,而非组件。Entity 类型的属性保存实体引用,需要通过实体访问组件,例如 this.lamp.light。
  • this.entity 同时对应 GameObject 及其变换。this.app 代表应用,提供输入设备、场景层级(this.app.root)、资源注册表等功能。
  • **dt 对应 Time.deltaTime。**引擎会将以秒为单位的帧间隔传给 update。

生命周期方法​

UnityPlayCanvas说明
Awake、Startinitialize()脚本与其实体首次同时启用时调用一次。不要使用类构造函数进行初始化配置。
依赖其他脚本 Awake 的 StartpostInitialize()所有脚本的 initialize 执行后调用,此时其他脚本已经就绪。
Updateupdate(dt)
LateUpdatepostUpdate(dt)所有脚本的 update 执行后调用。
FixedUpdate无物理引擎自行按固定频率步进。参见物理。
OnEnable、OnDisableenable 和 disable 事件使用 this.on('enable', ...) 订阅。
OnDestroydestroy 事件使用 this.once('destroy', ...) 订阅,并在处理函数中取消其他事件订阅。
OnValidate属性 setter参见响应属性变化。
OnCollisionEnter、OnTriggerEnter 等碰撞组件事件参见物理。

详细说明见脚本生命周期。

检查器字段​

脚本属性相当于 PlayCanvas 中的序列化字段。编辑器从代码中读取它们,因此添加或修改属性后,需要在脚本资源或 Script 组件上点击 Parse,更新 Inspector。

UnityPlayCanvas
[SerializeField] float speed = 5f;/** @attribute */ speed = 5;
[Tooltip("...")]属性 JSDoc 注释的第一句话
[Range(0, 10)]@range [0, 10]
[Min(0)]@range [0]
[HideInInspector],或没有 [SerializeField]不带 @attribute 的字段
GameObject 或 Transform 字段@type {Entity}
Texture2D、Material、AudioClip 字段@type {Asset},并添加 @resource texture、material 或 audio
Prefab 字段@type {Asset},并添加 @resource template
Color、Vector3 字段color = new Color()、offset = new Vec3()
AnimationCurve 字段@type {Curve}
数组或 List<T> 字段数组类型,例如 @type {Entity[]}
enum 字段@enum 对象,或字面量联合类型,例如 @type {'low' | 'high'}
[Serializable] 类字段@interface 类或 @typedef

编辑器中设置的属性值会在 initialize 运行前应用。各个标签的说明见属性参考。

常用 API 对照​

在这些表格中,entity 表示任意实体,例如脚本中的 this.entity;Vec3、KEY_W 等标识符从 playcanvas 导入。

实体与组件​

UnityPlayCanvas
new GameObject("Box")const box = new Entity('Box'); this.app.root.addChild(box);
Instantiate(original)const copy = original.clone(); this.app.root.addChild(copy);
Instantiate(prefab)const instance = templateAsset.resource.instantiate(); this.app.root.addChild(instance);
Destroy(gameObject)entity.destroy()
gameObject.SetActive(false)entity.enabled = false
gameObject.activeInHierarchyentity.enabled,当祖先实体被禁用时读取为 false
GetComponent<Light>()entity.light
GetComponent<Spinner>()entity.script.spinner
GetComponentInChildren<Light>()entity.findComponent('light')
GetComponentsInChildren<Light>()entity.findComponents('light')
AddComponent<Light>()entity.addComponent('light', { type: 'omni' })
Destroy(GetComponent<Light>())entity.removeComponent('light')
GameObject.Find("Player")this.app.root.findByName('Player')
GameObject.FindGameObjectsWithTag("Enemy")this.app.root.findByTag('enemy')
transform.Find("Arm/Hand")entity.findByPath('Arm/Hand')
CompareTag("Enemy")entity.tags.has('enemy')

clone() 会复制实体的组件和子实体,但脚本会从创建时的属性值重新开始。应用运行期间由脚本修改的值不会被复制,而 Unity 的 Instantiate 会复制序列化字段的当前值。

变换​

UnityPlayCanvas
transform.positionentity.getPosition(), entity.setPosition(x, y, z)
transform.localPositionentity.getLocalPosition(), entity.setLocalPosition(x, y, z)
transform.rotationentity.getRotation(), entity.setRotation(quat)
transform.eulerAnglesentity.getEulerAngles(), entity.setEulerAngles(x, y, z)
transform.localScaleentity.getLocalScale(), entity.setLocalScale(x, y, z)
transform.Translate(x, y, z)entity.translateLocal(x, y, z)(世界空间使用 translate)
transform.Rotate(x, y, z)entity.rotateLocal(x, y, z)(世界空间使用 rotate)
transform.LookAt(target)entity.lookAt(target.getPosition())
transform.forward、right、upentity.forward、entity.right、entity.up(forward 为 -Z)
transform.parententity.parent
transform.SetParent(parent)entity.reparent(parent),保留局部变换,因此实体会相对于新父实体改变位置
transform.GetChild(i), childCountentity.children[i], entity.children.length

时间、数学与调试​

UnityPlayCanvas
Time.deltaTimeupdate 的 dt 参数
Time.timeScalethis.app.timeScale
Time.time无对应项;在 update 中累加 dt。
Vector3, Quaternion, ColorVec3, Quat, Color
Vector3.up、Vector3.forwardVec3.UP、Vec3.FORWARD(值为 (0, 0, -1))
Quaternion.Euler(x, y, z)new Quat().setFromEulerAngles(x, y, z)
rotation * vectorrotation.transformVector(vector, result)
Vector3.Distance(a, b)a.distance(b)
Vector3.Lerp(a, b, t)result.lerp(a, b, math.clamp(t, 0, 1)),因为 lerp 不会限制 t 的范围
Mathf.Clamp, Mathf.Lerpmath.clamp, math.lerp
Mathf.Deg2Radmath.DEG_TO_RAD
浮点数版 Random.Range(min, max)math.random(min, max)
整数版 Random.Range(min, max)Math.floor(math.random(min, max)),因为 math.random 始终返回浮点数
Debug.Log, LogWarning, LogErrorconsole.log, console.warn, console.error
Debug.DrawLine(start, end, color)this.app.drawLine(start, end, color)

输入​

UnityPlayCanvas
Input.GetKey(KeyCode.W)this.app.keyboard.isPressed(KEY_W)
Input.GetKeyDown(KeyCode.W)this.app.keyboard.wasPressed(KEY_W)
Input.GetKeyUp(KeyCode.W)this.app.keyboard.wasReleased(KEY_W)
Input.GetMouseButton(0)this.app.mouse.isPressed(MOUSEBUTTON_LEFT)
Input.mousePosition鼠标事件的 x 和 y,从画布左上角而非左下角测量
Input.GetAxis("Mouse X")mousemove 事件的 dx
Cursor.lockState = CursorLockMode.Lockedthis.app.mouse.enablePointerLock()
Input.touchesthis.app.touch 上的触摸事件
Physics.Raycastthis.app.systems.rigidbody.raycastFirst(start, end)

没有内置输入轴或动作映射:直接读取设备,或在自定义脚本中将其映射为动作。浏览器只允许在响应点击或按键时启用指针锁定。

Web 平台​

UnityPlayCanvas
PlayerPrefs浏览器的 localStorage
UnityWebRequestfetch
JsonUtilityJSON.parse 和 JSON.stringify
Application.OpenURLwindow.open

向量是对象​

Unity 的 Vector3 和 Quaternion 是结构体,赋值时会复制;PlayCanvas 的 Vec3 和 Quat 是类,赋值会共享同一个对象,JavaScript 也没有运算符重载。因此需要养成以下三个习惯:

  • 将返回的向量视为只读。entity.getPosition() 等 getter 返回实体自身持有并复用的向量,其值会在实体移动时变化;直接修改它不能正确更新实体。需要保留数值时调用 clone(),修改时使用 setPosition() 等 setter。
  • 使用方法代替运算符。add、sub、mulScalar 等方法会修改调用方法的向量,并返回该向量。add2(a, b) 等方法将结果写入调用方法的向量中。
  • **避免在 update 中分配对象。**每帧创建向量会增加垃圾回收负担,可能引起卡顿。应创建一次并重复使用。
import { Script, Vec3 } from 'playcanvas';

export class Mover extends Script {
static scriptName = 'mover';

/** @attribute */
speed = 5;

// Created once, and reused every frame
step = new Vec3();

update(dt) {
// Unity: transform.position += transform.forward * speed * Time.deltaTime;
this.step.copy(this.entity.forward).mulScalar(this.speed * dt);
this.entity.translate(this.step);
}
}

事件与消息​

C# 事件、UnityEvent 和 SendMessage 都可对应到 PlayCanvas 事件。应用、实体和脚本均可触发和监听事件:

// Anywhere: fire an event on the application
this.app.fire('player:died', this.entity);

// In another script: listen for it, and stop listening when the script is destroyed
initialize() {
this.app.on('player:died', this.onPlayerDied, this);
this.once('destroy', () => {
this.app.off('player:died', this.onPlayerDied, this);
});
}

脚本销毁时,务必移除添加到其他对象上的监听器,否则它们会继续执行,并让脚本留在内存中。

协程与定时器​

PlayCanvas 没有协程或 Invoke。游戏逻辑中的计时可以在 update 中倒计时:这样会遵循 app.timeScale,脚本禁用时暂停,实体销毁时停止。

// Unity: Invoke(nameof(Spawn), 2f);
initialize() {
this.spawnTimer = 2;
}

update(dt) {
if (this.spawnTimer > 0) {
this.spawnTimer -= dt;
if (this.spawnTimer <= 0) {
this.spawn();
}
}
}

浏览器的 setTimeout、setInterval 和带有 await 的 async 函数适合加载等不依赖游戏时间的工作。但它们按真实时间运行,脚本销毁后仍会继续,因此应在 destroy 处理函数中取消。

执行顺序​

同一实体上的脚本按 Script 组件中列出的顺序执行,可通过拖动调整。不要依赖实体之间的执行顺序(Unity 的 Script Execution Order 设置允许这样做),而应使用 postInitialize、postUpdate 或事件。

共享代码与使用包​

脚本是 ES 模块,通过 import 和 export 共享代码。模块的顶层变量由所有导入它的脚本共享,因此适合承载 C# 中通常写成静态类和单例的内容。这些变量也会在场景切换后保留。

在编辑器中使用 npm 包时,通过导入映射将包名映射到 CDN URL。不使用编辑器构建的项目,则像其他 Web 项目一样通过 npm 安装包。

预制体与模板​

模板相当于 PlayCanvas 的预制体,两者工作方式相近:

UnityPlayCanvas
将 GameObject 拖入 Project 窗口在层级面板中右键点击实体,选择 Template > New Template
预制体实例模板实例,在层级面板中用图标标记
覆盖项覆盖项,在 Inspector 中显示,并提供 View Diff 按钮
Overrides > Apply All / Revert All实例根实体 Inspector 中的 Apply All / Revert All
嵌套预制体嵌套模板
Prefab Mode在场景中编辑任意实例,然后应用其覆盖项
Unpack PrefabTemplate > Unlink From Template
Instantiate(prefab)templateAsset.resource.instantiate()

没有预制体变体;带覆盖项的模板实例是最接近的对应形式。导入模型会为其层级创建模板,类似于 Unity 创建模型预制体。

将覆盖项应用到模板资源后无法撤销。运行时实例化模板前,必须先加载模板资源:使用带有 @resource template 的脚本属性引用它,并将其标记为预加载,或先加载它。

场景​

UnityPlayCanvas
SceneManager.LoadScene("Level 2")this.app.scenes.changeScene('Level 2')
LoadSceneMode.Additivethis.app.scenes.loadSceneHierarchy('Level 2', callback)
卸载叠加加载的场景销毁 loadSceneHierarchy 传给回调的根实体
DontDestroyOnLoad无对应项,见下文。
随各场景保存的光照设置场景设置,由 changeScene 或 loadSceneSettings 应用

changeScene 会先销毁应用根节点下的所有实体,包括脚本创建的实体,再添加新场景。若要让实体跨关卡保留,不要切换场景,而应叠加加载各个关卡,并销毁上一关的根实体。仅供脚本使用的数据可保存在模块中,不受场景切换影响。参见加载场景。

编辑器一次只打开一个场景,因此没有 Unity 多场景编辑的对应功能。叠加加载场景由应用在运行时完成。

资源​

资源面板像 Project 窗口一样保存文件,但项目位于 PlayCanvas 服务器,而非本地磁盘目录,因此没有 .meta 文件。每个资源都有 ID,以及用于设置导入参数的独立 Inspector。

UnityPlayCanvas
Resources.Load<Texture2D>("logo")先调用 this.app.assets.find('logo', 'texture'),再加载资源
Addressables 和 AssetBundles不预加载、按需加载的资源,可通过名称或标签查找
ScriptableObjectJSON 资源,其 resource 为解析后的对象
纹理导入设置纹理检查器,包括纹理压缩
模型导入设置Asset Import 设置

标记为 Preload 的资源会在应用启动前下载。新资源默认预加载,除非在 Asset Import 设置中关闭 Assets default to preload。仅部分场景需要的大型资源应取消预加载。

渲染​

PlayCanvas 只有一种渲染器,无需选择渲染管线。它使用 WebGPU 或 WebGL 2 渲染,并通过集群光照让包含大量点光源和聚光灯的场景仍保持高效。

材质与着色器​

PlayCanvas 材质是基于物理的材质,类似 Unity 的 Standard 着色器和 URP 的 Lit 着色器,支持金属度和高光工作流:

UnityPlayCanvas
Albedo 或 Base MapDiffuse(漫反射)
MetallicMetalness(金属度)
SmoothnessGlossiness(两者都是值越大表面越光滑)
Normal MapNormal(法线)
OcclusionAmbient Occlusion(环境光遮蔽)
EmissionEmissive(自发光)
Rendering Mode: CutoutAlpha Test(透明度测试)
Rendering Mode: Transparent 或 FadeBlend Type(混合类型),结合 Opacity(不透明度)
Tiling 和 OffsetTiling 和 Offset,可统一设置所有贴图,也可分别设置
Shader Graph 和自定义着色器使用 GLSL 和 WGSL 编写的着色器

Unity 的 Metallic 贴图将金属度存储在红色通道,将光滑度存储在 alpha 通道。PlayCanvas 中,可将同一纹理指定给 Metalness 贴图并选择 R 通道,同时指定给 Glossiness 贴图并选择 A 通道。请关闭光泽度的 Invert 选项,否则贴图会按粗糙度解释。

光照​

UnityPlayCanvas
方向光Directional light
点光源Omni light
聚光灯Spot light。Inner 和 Outer Cone Angle 从光源轴线开始测量,因此是 Unity Spot Angle 的一半。
面光源在 Rendering Settings 中启用 Area Lights Enabled 后,为灯光选择 Rectangle、Disk 或 Sphere Shape
Lighting 窗口的环境设置和雾效渲染设置中的天空盒、环境颜色、曝光和雾效
反射探针为所有材质提供照明的天空盒,以及各材质独立设置的带盒投影的立方体贴图
光照探针无对应项,使用光照贴图和环境照明。
Progressive Lightmapper光照贴图烘焙器,可通过工具栏的 Bake 或在运行中的应用中执行

参见光照。

相机与图层​

UnityPlayCanvas
Culling MaskLayers(图层)
Depth 或 URP 相机堆叠Priority(值越小越先渲染)
Clear FlagsClear Color Buffer 和 Clear Depth Buffer
Viewport RectViewport(视口)
Target Texture通过脚本设置的 renderTarget
Orthographic SizeOrtho Height

Unity 的 GameObject 只能属于一个图层,图层同时用于渲染、物理和射线检测。PlayCanvas 图层仅用于渲染,决定哪些相机绘制网格以及绘制顺序:每台相机按 Settings > Layers 中的顺序渲染自己列表中的图层,绘制网格的组件可以属于多个图层。物理筛选改用碰撞组与掩码。参见多相机。

后期处理​

Unity 的后期处理 Volume 对应 CameraFrame,它为相机添加泛光、环境光遮蔽、景深、抗锯齿、颜色分级、暗角等效果。在编辑器中,为相机实体添加 CameraFrame 脚本。与其他脚本一样,它仅在已启动的应用中运行,因此效果不会出现在编辑器视口中。不使用它时,色调映射和伽马由 Camera 组件属性控制。

性能功能​

UnityPlayCanvas
静态和动态批处理批处理组
GPU 实例化硬件实例化
遮挡剔除无对应项。相机会剔除视野外的网格。
LOD Group网格没有对应项
Profiler参见调试与性能分析

物理​

PlayCanvas 物理基于 ammo.js,即 Bullet 物理引擎的 WebAssembly 构建。物理需要主动启用:打开 Settings,展开 PHYSICS 并点击 IMPORT AMMO,将其加入项目。各场景的重力也在此设置。参见物理。

UnityPlayCanvas
Rigidbody类型为 Dynamic 的 Rigid Body 组件
勾选 Is Kinematic 的 Rigidbody类型为 Kinematic 的 Rigid Body
不带 Rigidbody 的 ColliderCollision 组件加上类型为 Static 的 Rigid Body
勾选 Is Trigger 的 Collider不带 Rigid Body 的 Collision 组件
AddForce(force)entity.rigidbody.applyForce(force)
AddForce(force, ForceMode.Impulse)entity.rigidbody.applyImpulse(force)
AddTorqueapplyTorque, applyTorqueImpulse
velocity(Unity 6 中为 linearVelocity)linearVelocity
设置动态刚体的位置entity.rigidbody.teleport(position)
运动学刚体的 MovePosition设置实体位置:运动学刚体会跟随所属实体
Constraints(冻结位置或旋转)Linear Factor 和 Angular Factor
Physic MaterialRigid Body 上的 Friction 和 Restitution
OnCollisionEnter、OnCollisionStay、OnCollisionExit碰撞组件上的 collisionstart、contact 和 collisionend 事件
OnTriggerEnter、OnTriggerExit触发器碰撞组件和进入它的刚体上的 triggerenter、triggerleave 事件
图层碰撞矩阵通过脚本设置的碰撞组与掩码
Physics.RaycastraycastFirst 和 raycastAll
Character Controller无对应项,通过脚本驱动动态刚体。

以下差异最容易让 Unity 开发者遇到问题:

  • **静态碰撞体也需要刚体。**单独的碰撞组件是触发体积,只检测刚体,不阻挡运动。地板和墙壁需要类型为 Static 的 Rigid Body 组件。
  • **基本几何碰撞形状忽略缩放。**无论怎样缩放实体,盒体、球体或胶囊体仍使用设置的 Half Extents 或 Radius。网格形状例外。参见缩放。
  • **没有 FixedUpdate。**模拟按每秒 60 次的固定频率步进,在 update 中施加力。碰撞事件每个物理步触发一次。
  • **动态刚体由物理系统控制。**每步之后,引擎都会将动态刚体的位置和旋转写回实体。因此应通过力、速度或 teleport 移动动态刚体,而非直接设置实体变换。

用户界面​

PlayCanvas 的画布内 UI 与 Unity UI(uGUI)的工作方式相似。参见用户界面。

UnityPlayCanvas
Screen Space - Overlay 模式的 Canvas启用 Screen Space 的 Screen 组件
World Space 模式的 Canvas禁用 Screen Space 的 Screen 组件
Canvas Scaler: Scale With Screen SizeScale Mode 为 Blend,并配置 Ref Resolution 和 Scale Blend
RectTransformElement 组件的锚点、轴心和边距
Image、Raw Image类型为 Image 的 Element,显示纹理或精灵
Text、TextMeshPro类型为 Text 的 Element,使用从 TTF 或 OTF 文件创建的字体资源
ButtonButton 组件
Horizontal、Vertical 和 Grid Layout GroupLayout Group 组件
Layout ElementLayout Child 组件
Scroll Rect、ScrollbarScroll View 和 Scrollbar 组件
Mask、Rect Mask 2D启用 Mask 的元素。参见遮罩。
EventSystem 和 Graphic Raycaster在需要接收输入的元素上启用 Use Input
Toggle、Slider、Dropdown通过元素组合实现,参见常用控件。
UI ToolkitHTML 和 CSS

应用运行在网页中,因此也可以用 HTML 和 CSS 构建任意界面部分,叠加在画布上。文本输入框和无障碍菜单通常更适合用这种方式实现。

动画​

UnityPlayCanvas
Animator 组件Anim 组件
Animator Controller动画状态图资源,包含状态、过渡、参数和图层
Animation Clip导入模型时生成的动画资源
animator.SetFloat("Speed", 2f)entity.anim.setFloat('Speed', 2)
SetBool, SetInteger, SetTriggersetBoolean, setInteger, setTrigger
Avatar Mask图层遮罩
Blend Tree通过代码创建的状态图支持混合树(1D、2D 和直接混合);编辑器的状态图编辑器不提供此功能。
Root Motion无对应项,通过脚本移动实体。
Animation Events动画资源上的动画事件
Animator Override Controller使用同一个动画状态图,在不同实体上为状态指定不同动画资源

PlayCanvas 没有人形动画重定向。动画按名称驱动骨骼,因此可以应用到骨骼名称相同的模型上,但不会自动适配骨架比例。应针对实际播放动画的骨架制作或烘焙动画。

音频​

UnityPlayCanvas
Audio SourceSound 组件中的槽位
audioSource.Play()entity.sound.play('slot name')
Spatial Blend 设为 3DSound 组件上的 Positional
Min Distance 和 Max DistanceRef Distance 和 Max Distance
Volume RolloffDistance Model,默认 Linear,而 Unity 默认为 Logarithmic
Audio ListenerAudio Listener 组件
Audio Mixer无对应项,可使用 Web Audio API。

浏览器通常要求用户先与页面交互,例如点击或按键,才允许播放声音,因此应在用户首次点击后启动音乐和其他声音。

迁移项目内容​

没有自动转换 Unity 项目的工具。资源通过标准文件格式迁移,代码则需要重写。

  • **模型和动画。**使用 UnityGLTF 或 Unity glTFast 等 glTF 导出器导出 GLB,或使用 Unity FBX Exporter 导出 FBX。编辑器导入的是 glTF 的二进制 .glb 格式,而非 .gltf。将文件拖入资源面板:FBX 会转换为 GLB,每个模型会生成渲染、材质、纹理和动画资源,以及表示模型层级的模板。参见导入 3D 模型。
  • **材质。**Unity 着色器无法直接迁移。glTF 导出器会将 Standard 和 URP Lit 材质转换为 glTF PBR 材质,导入后成为 PlayCanvas 材质。FBX 保留的信息较少,通常需要重新配置材质。
  • **纹理、音频和字体。**上传源文件即可。参见支持的格式。
  • **场景和预制体。**将场景的静态几何体导出为单个 GLB,保留布局,然后在编辑器中添加灯光、相机、物理、UI 和脚本。
  • **地形。**导出前将地形转换为网格。
  • **脚本和着色器。**分别用 JavaScript,以及 GLSL 或 WGSL 重写。AI 编程助手比较擅长转换 MonoBehaviour。若希望它处理编辑器项目的脚本,可通过 VS Code 扩展同步脚本,再通过 MCP 服务器连接编辑器,使其也能修改场景。
  • **光照贴图。**通过 PlayCanvas 光照贴图烘焙器重新烘焙,或在 3D 建模工具中烘焙后随模型导入。

发布​

Unity 为各个平台构建播放器;PlayCanvas 发布由 HTML、JavaScript 和资源组成的 Web 应用:

调试与性能分析​

应用运行在浏览器中,因此请在已启动的标签页中使用浏览器开发者工具调试,快捷键为 F12,macOS 上为 Cmd+Option+I。

UnityPlayCanvas
Console 窗口开发者工具的 Console 标签页。参见控制台日志。
附加 Visual Studio 或 Rider开发者工具 Sources 标签页中的断点。参见浏览器开发者工具。
Debug.Break()debugger; 语句,在开发者工具打开时暂停应用
ProfilerLaunch 菜单中的 Profiler 和 MiniStats,以及开发者工具的 Performance 标签页
Frame DebuggerLaunch 菜单中的 Inspector,用于查看每帧的渲染通道和绘制调用;也可使用原生 GPU 性能分析器
Debug.DrawLine 和 Gizmosthis.app.drawLine(),以及用于球体、盒体等形状的 WireRenderer

版本控制与协作​

Unity 项目是放在版本控制下的本地目录,PlayCanvas 项目则在线共享:团队成员打开场景后,可以实时共同编辑,即时看到彼此的修改。历史记录通过编辑器版本控制保存:像提交代码一样创建检查点,在分支上工作,并通过带冲突解决的合并整合修改。

没有内置对应项的功能​

UnityPlayCanvas 中的替代方式
Terrain将地形作为网格导入
NavMesh 和 AI Navigation使用支持 PlayCanvas 的库,例如 recast-navigation-js
Character Controller通过脚本驱动动态刚体,或使用引擎的第一人称和第三人称控制器脚本
2D 物理使用 3D 物理,通过 Linear Factor 和 Angular Factor 锁定第三个轴
Timeline 和 Cinemachine使用脚本,通过曲线属性控制动画数值
VFX GraphParticle System 组件
光照探针和实时全局光照光照贴图与环境照明
DOTween 等补间动画包从 CDN 导入 JavaScript 补间动画库

下一步​