Unityからの移行
Unityでプロジェクトを作ったことがあれば、PlayCanvasの多くはなじみのあるものに感じられるはずです。PlayCanvas Editorには、ヒエラルキー、インスペクター、アセットパネル、3Dビューポートがあります。シーンはエンティティのツリーで、エンティティはコンポーネントで構成され、振る舞いはinitializeやupdateなどのライフサイクルメソッドを持つスクリプトで記述します。
このガイドでは、Unityで身につけた知識をPlayCanvasに対応付けます。エディターのパネルとショートカット、共通する概念の名前、C#のAPIとそれに相当するJavaScript、そしてPlayCanvasの動作が異なる点を取り上げます。各セクションから、そのトピックの詳しいドキュメントにリンクしています。
主な違い
- すべてがブラウザーで動作します。 エディターはWebアプリケーションで、プロジェクトはオンラインに保存され、公開するものもWebアプリです。WebGLのビルドを待つ必要はありません。アプリは、エディターのビューポートと同じエンジンで動作します。
- スクリプトはJavaScriptで書きます。 スクリプトはC#のクラスではなくJavaScriptのモジュールとして書きます。コンパイルは不要で、スクリプトを保存したらそのまま起動できます。
- スクリプトは1つのコンポーネントを共有します。 Unityでは、MonoBehaviourのそれぞれが独立したコンポーネントです。PlayCanvasでは、エンティティに1つのScriptコンポーネントがあり、そのエンティティのすべてのスクリプトを保持します。
- 各タイプのコンポーネントはエンティティごとに1つです。 エンティティが持てるカメラ、ライト、Collisionコンポーネントなどはそれぞれ1つで、各スクリプトも1回しか追加できません。コライダーを2つ使いたい場合など、同じものが2つ必要なときは子エンティティを使います。
- 座標系は右手系です。 前方は+Zではなく-Zです。座標系と単位を参照してください。
- Playモードはありません。 シーンを起動すると、エディターに接続されたままの別のブラウザータブで実行されます。
- コラボレーションとバージョン管理が組み込まれています。 複数の人が同じシーンを同時に編集でき、チェックポイント、ブランチ、マージはエディターの機能です。
エディター
エディターのレイアウトは、Unityのデフォルトのレイアウトとよく似ています。

| PlayCanvas | Unity | |
|---|---|---|
| 1 | ツールバー | メインメニュー、トランスフォームツール、ツールバー |
| 2 | ヒエラルキー | Hierarchyウィンドウ |
| 3 | ビューポート | Sceneビュー |
| 4 | インスペクター | Inspectorウィンドウ |
| 5 | アセットパネル | Projectウィンドウ |
Unityのその他のウィンドウやメニューに相当するものは、次のとおりです。
| Unity | PlayCanvas |
|---|---|
| PlayボタンとGameビュー | ビューポートの右上にあるLaunchボタン |
| Consoleウィンドウ | 起動したタブで開く、ブラウザーの開発者ツールのコンソール。エディター左下にあるエディター自体の CONSOLE パネルには、スクリプトの解析エラーなど、エディターのメッセージが表示されます。 |
| Project SettingsとLightingウィンドウ | ツールバーの歯車アイコンから開く設定 |
| Build Settings | ツールバーの Publish / Download |
| Animatorウィンドウ | anim state graphアセットを選択すると開くanim state graphエディター |
| Asset Store | PlayCanvasストアとSketchfabのアセットがある、アセットパネル右上の ASSET STORE |
| Visual StudioまたはRider | 組み込みのコードエディター、またはVS Code拡張機能を入れたVS Code |
| Unity Version ControlまたはGit | 組み込みのバージョン管理 |
Playモードの代わりに起動する
Unityはゲームをエディターの中で再生します。PlayCanvasは新しいブラウザータブで起動ページを開き、公開したときと同じようにシーンを実行します。Launch をクリックするか、Ctrl+Enterを押してください。
起動ページはエディターに接続されたままです。エンティティ、コンポーネント、スクリプト属性、アセット、シーン設定への変更は、加えたそばから実行中のアプリに反映されます。Playモード中の編集とは違い、停止しても変更は失われません。エディターはすべての変更をその都度保存するからです。この使い方をするには、2つのタブを横に並べて配置してください。コードの変更は、スクリプトがホットリロードに対応していない限り、再読み込みが必要です。
スクリプトがエディターのビューポートで実行されることはないため、Unityの[ExecuteInEditMode]に相当するものはありません。スクリプトが作成したり変更したりするものは、起動したアプリにしか現れません。
Launchボタンのメニューには、テスト用のオプションがあります。グラフィックスAPIを選んだり、プロファイラー、MiniStats、Inspectorを開いたりできます。Debug にチェックを入れると、エンジンの警告とエラーがコンソールに記録されます。
ビューポートの操作
| 操作 | Unity | PlayCanvas |
|---|---|---|
| 周回(オービット) | Alt + 左ドラッグ | 左ドラッグ |
| 見回す | 右ドラッグ | 右ドラッグ |
| パン | 中ドラッグ | 中ドラッグ、またはShift + 左ドラッグ |
| ズーム | マウスホイール | マウスホイール |
| 飛行 | 右ボタンを押しながらW、A、S、D、Q、Eを押す | マウスボタンを押さずにW、A、S、D、Q、Eを押す(Shiftを押すと速く移動) |
| 選択対象にフォーカス | F | F |
| 矩形選択 | 何もない場所を左ドラッグ | Ctrl + 左ドラッグ |
| 選択への追加と選択からの除外 | Shift+クリックまたはCtrl+クリック | Ctrl+クリック |
キーボードショートカット
| 操作 | Unity | PlayCanvas |
|---|---|---|
| 移動ツール | W | 1 |
| 回転ツール | E | 2 |
| スケールツール | R | 3 |
| 矩形ツール(UI要素) | T | 4 |
| ギズモのローカル空間とワールド空間の切り替え | X | L |
| 選択対象にフォーカス | F | F |
| 再生 | Ctrl+P | Ctrl+Enter(Launch) |
| 空の子オブジェクトを作成 | Alt+Shift+N | Ctrl+E |
| 複製 | Ctrl+D | Ctrl+D |
| 名前の変更 | F2 | F2 |
| 削除 | Delete | Delete |
| 元に戻す / やり直し | Ctrl+Z / Ctrl+Y | Ctrl+Z / Ctrl+Y |
| 保存 | Ctrl+S | すべての変更が保存されるので不要です。Ctrl+Sを押すと、バージョン管理のチェックポイントを作成するフォームが開きます。 |
| ビューの最大化 | Shift+Space | Space(ヒエラルキー、インスペクター、アセットパネルを隠す) |
| 正面、右側面、上面のビュー | シーンギズモの軸をクリック | テンキーの1、3、7(Ctrlを加えると反対側)、またはビューキューブの軸をクリック |
| 正投影と透視投影の切り替え | シーンギズモの中央をクリック | テンキーの5 |
macOSでは、Ctrlの代わりにCmdを使います。UnityのViewツール(Q)に相当するものはありません。ビューポートの操作にツールは必要ないためです。すべてのショートカットは操作とキーボードショートカットのページにあります。
概念と用語
| Unity | PlayCanvas | 備考 |
|---|---|---|
| GameObject | エンティティ | |
| Component | コンポーネント | コンポーネントはエンティティのプロパティです(entity.camera、entity.light)。 |
| Transform | すべてのエンティティの一部 | エンティティはそれ自体が位置、回転、スケールを持ちます。階層とトランスフォームを参照してください。 |
| MonoBehaviour | スクリプト | エンティティのScriptコンポーネントを通じてアタッチします。 |
| Prefab | テンプレート | Prefabとテンプレートを参照してください。 |
| Scene | シーン | |
| Tag | タグ | 1つのエンティティにいくつでもタグを付けられます。 |
| Layer | レイヤー | レンダリング専用です。物理演算ではコリジョングループとマスクを使います。 |
| Mesh FilterとMesh Renderer | Renderコンポーネント | スキンメッシュにもRenderコンポーネントを使います。 |
| Material | マテリアル | |
| Camera | Cameraコンポーネント | |
| Light | Lightコンポーネント | ポイントライトはオムニライトと呼ばれます。 |
| Rigidbody | Rigid Bodyコンポーネント | |
| Collider | Collisionコンポーネント | Rigid Bodyがない場合、Collisionコンポーネントはトリガーになります。物理演算を参照してください。 |
| Audio Source | Soundコンポーネント | 1つのコンポーネントで、スロットと呼ばれる任意の数のサウンドを再生できます。 |
| Audio Listener | Audio Listenerコンポーネント | |
| Particle System | Particle Systemコンポーネント | |
| AnimatorとAnimator Controller | Animコンポーネントとanim state graphアセット | |
| Canvas | Screenコンポーネント | |
| RectTransform、Image、Text | Elementコンポーネント | |
| Sprite Renderer | Spriteコンポーネント | |
| ScriptableObject | JSONアセット | または、データをエクスポートするJavaScriptモジュール。 |
ResourcesとAddressables | アセットレジストリ | アセットを参照してください。 |
座標系と単位
どちらのエンジンもYを上方向とし、距離をメートル、角度を度で扱います。違いは座標系の向きです。Unityの座標系は+Zが前方の左手系ですが、PlayCanvasは -Zが前方 の右手系です。カメラは自身の負のZ軸の方向を向き、entity.forwardは負のZ軸を返し、lookAtは負のZ軸をターゲットに向けます。
このため、コードやコンテンツを移植するときには、次の点に注意が必要です。
- 前進は-Z方向への移動です。
transform.Translate(0, 0, speed * Time.deltaTime)はthis.entity.translateLocal(0, 0, -this.speed * dt)になります。 - 正の回転の向きが逆になります。 Y軸まわりの正の回転はエンティティを左に向け(上から見て反時計回り)、X軸まわりの正の回転は上に傾けます。Unityではそれぞれ右と下に回るため、シーンを見下ろすカメラのX回転は、PlayCanvasでは負の値になります。
- オイラー角を適用する順序が異なります。 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の新しいカメラは60°ではなく45°から始まります。
スクリプト
PlayCanvasのスクリプトは、Scriptを継承するJavaScriptのクラスです。各スクリプトはアセットパネル内の.mjsファイルにあり、エンティティにScriptコンポーネントを追加してスクリプトを選ぶとアタッチできます。詳しくはスクリプティングを参照してください。
スクリプトの比較
次のUnityスクリプトは、GameObjectを回転させ、スペースキーでライトのオンとオフを切り替えます。
using UnityEngine;
public class Spinner : MonoBehaviour
{
// 1秒あたりの回転角度(度)
[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';
/**
* 1秒あたりの回転角度(度)。
*
* @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)。[SerializeField]の代わりに@attributeを使います。 JSDocコメントで@attributeタグを付けたクラスフィールドがインスペクターに表示され、コメントの最初の文がフィールドのツールチップになります。インスペクターのフィールドを参照してください。- Entity属性は、コンポーネントではなくエンティティを参照します。
Entity型の属性はエンティティを保持するので、this.lamp.lightのように、そのエンティティを通じてコンポーネントにアクセスします。 this.entityはGameObjectとそのTransformに相当します。this.appはアプリケーションで、入力デバイス、シーンの階層(this.app.root)、アセットレジストリなどにアクセスできます。dtはTime.deltaTimeに相当します。 エンジンは、フレーム時間を秒単位でupdateに渡します。
ライフサイクルメソッド
| Unity | PlayCanvas | 備考 |
|---|---|---|
Awake、Start | initialize() | スクリプトとそのエンティティが初めて両方とも有効になったときに1回呼ばれます。初期化にクラスのコンストラクターは使わないでください。 |
別のスクリプトのAwakeに依存するStart | postInitialize() | すべてのスクリプトのinitializeが実行された後に呼ばれるので、他のスクリプトの準備はできています。 |
Update | update(dt) | |
LateUpdate | postUpdate(dt) | すべてのスクリプトのupdateが実行された後に呼ばれます。 |
FixedUpdate | なし | 物理エンジンは独自に固定レートでステップします。物理演算を参照してください。 |
OnEnable、OnDisable | enableとdisableイベント | this.on('enable', ...)で購読します。 |
OnDestroy | destroyイベント | this.once('destroy', ...)で購読し、そこで他のイベントの購読を解除します。 |
OnValidate | 属性のセッター | 属性の変更に応答するを参照してください。 |
OnCollisionEnter、OnTriggerEnterなど | Collisionコンポーネントのイベント | 物理演算を参照してください。 |
詳しくはスクリプトのライフサイクルを参照してください。
インスペクターのフィールド
スクリプト属性は、PlayCanvasにおけるシリアライズされたフィールドです。エディターはコードから属性を読み取るため、属性を追加または変更したら、スクリプトアセットまたはScriptコンポーネントの Parse をクリックしてインスペクターを更新します。
| Unity | PlayCanvas |
|---|---|
[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からインポートします。
エンティティとコンポーネント
| Unity | PlayCanvas |
|---|---|
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.activeInHierarchy | entity.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は、シリアライズされたフィールドの現在の値をコピーします。
トランスフォーム
| Unity | PlayCanvas |
|---|---|
transform.position | entity.getPosition()、entity.setPosition(x, y, z) |
transform.localPosition | entity.getLocalPosition()、entity.setLocalPosition(x, y, z) |
transform.rotation | entity.getRotation()、entity.setRotation(quat) |
transform.eulerAngles | entity.getEulerAngles()、entity.setEulerAngles(x, y, z) |
transform.localScale | entity.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、up | entity.forward、entity.right、entity.up(forwardは-Z) |
transform.parent | entity.parent |
transform.SetParent(parent) | entity.reparent(parent)。ローカルのトランスフォームが保たれるため、エンティティは新しい親に合わせて移動します |
transform.GetChild(i)、childCount | entity.children[i]、entity.children.length |
時間、数学、デバッグ
| Unity | PlayCanvas |
|---|---|
Time.deltaTime | updateの引数dt |
Time.timeScale | this.app.timeScale |
Time.time | なし。updateでdtを足し合わせます。 |
Vector3、Quaternion、Color | Vec3、Quat、Color |
Vector3.up、Vector3.forward | Vec3.UP、Vec3.FORWARD((0, 0, -1)) |
Quaternion.Euler(x, y, z) | new Quat().setFromEulerAngles(x, y, z) |
rotation * vector | rotation.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.Lerp | math.clamp、math.lerp |
Mathf.Deg2Rad | math.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、LogError | console.log、console.warn、console.error |
Debug.DrawLine(start, end, color) | this.app.drawLine(start, end, color) |
入力
| Unity | PlayCanvas |
|---|---|
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.Locked | this.app.mouse.enablePointerLock() |
Input.touches | this.app.touchのタッチイベント |
Physics.Raycast | this.app.systems.rigidbody.raycastFirst(start, end) |
入力軸やアクションマップはありません。デバイスを直接読み取るか、独自のスクリプトでアクションに対応付けてください。ブラウザーは、クリックやキー入力に応じた場合にのみポインターロックを許可します。
Webプラットフォーム
| Unity | PlayCanvas |
|---|---|
PlayerPrefs | ブラウザーのlocalStorage |
UnityWebRequest | fetch |
JsonUtility | JSON.parseとJSON.stringify |
Application.OpenURL | window.open |
ベクトルはオブジェクトです
UnityのVector3とQuaternionは構造体で、代入するたびにコピーされます。PlayCanvasのVec3とQuatはクラスなので、代入すると1つのオブジェクトを共有します。また、JavaScriptには演算子のオーバーロードがありません。このため、次の3つを習慣にしてください。
- 返されたベクトルは読み取り専用として扱います。
entity.getPosition()などのゲッターは、エンティティが所有して再利用するベクトルを返します。その値はエンティティが動くと変わり、書き込んでもエンティティは正しく更新されません。値を保持するにはclone()を呼び、値を変えるにはsetPosition()などのセッターを使います。 - 演算子の代わりにメソッドを使います。
add、sub、mulScalarなどのメソッドは、呼び出したベクトル自体を変更して、そのベクトルを返します。add2(a, b)などのメソッドは、呼び出したベクトルに結果を書き込みます。 update内でのアロケーションを避けます。 毎フレームベクトルを作成すると、ガベージコレクターの負担が増え、カクつきの原因になります。一度だけ作成して再利用してください。
import { Script, Vec3 } from 'playcanvas';
export class Mover extends Script {
static scriptName = 'mover';
/** @attribute */
speed = 5;
// 一度だけ作成し、毎フレーム再利用する
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のイベントに対応します。アプリケーション、エンティティ、スクリプトは、イベントを発火したりリッスンしたりできます。
// どこからでも: アプリケーションでイベントを発火する
this.app.fire('player:died', this.entity);
// 別のスクリプトで: イベントをリッスンし、スクリプトが破棄されたらリッスンをやめる
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からパッケージをインストールします。
Prefabとテンプレート
テンプレートは、PlayCanvasにおけるPrefabです。ほぼ同じように機能します。
| Unity | PlayCanvas |
|---|---|
| GameObjectをProjectウィンドウにドラッグ | ヒエラルキーでエンティティを右クリックし、Template > New Template を選択 |
| Prefabインスタンス | ヒエラルキーのアイコンで区別されるテンプレートインスタンス |
| オーバーライド | インスペクターに表示されるオーバーライド(View Diff ボタンで一覧を確認できます) |
| Overrides > Apply All / Revert All | インスタンスのルートのインスペクターにある Apply All / Revert All |
| ネストされたPrefab | ネストされたテンプレート |
| Prefabモード | シーン内の任意のインスタンスを編集し、そのオーバーライドを適用 |
| Unpack Prefab | Template > Unlink From Template |
Instantiate(prefab) | templateAsset.resource.instantiate() |
Prefabバリアントはありません。オーバーライドを持つテンプレートインスタンスが、最も近いものです。モデルをインポートすると、Unityがモデルのプレハブを作るのと同じように、その階層のテンプレートが作成されます。
テンプレートアセットへのオーバーライドの適用は、元に戻せません。実行時にテンプレートをインスタンス化するには、そのアセットが読み込まれている必要があります。@resource templateを指定したスクリプト属性から参照し、プリロードするように設定するか、先に読み込んでください。
シーン
| Unity | PlayCanvas |
|---|---|
SceneManager.LoadScene("Level 2") | this.app.scenes.changeScene('Level 2') |
LoadSceneMode.Additive | this.app.scenes.loadSceneHierarchy('Level 2', callback) |
| 加算的に読み込んだシーンのアンロード | loadSceneHierarchyがコールバックに渡したルートエンティティを破棄 |
DontDestroyOnLoad | なし。以下を参照してください。 |
| シーンごとに保存されるライティング設定 | シーン設定。changeSceneまたはloadSceneSettingsで適用されます |
changeSceneは、新しいシーンを追加する前に、アプリケーションのルートの下にあるすべてのエンティティを破棄します。スクリプトが作成したエンティティも例外ではありません。レベルをまたいでエンティティを残すには、シーンを切り替えずに、各レベルを加算的に読み込み、前のレベルのルートエンティティを破棄します。スクリプトだけが使うデータはモジュールに置けます。モジュールはシーンの切り替えの影響を受けません。シーンの読み込みを参照してください。
エディターで開けるシーンは一度に1つなので、Unityのマルチシーン編集に相当するものはありません。シーンを加算的に読み込むのは、実行時にアプリが行うことです。
アセット
アセットパネルはProjectウィンドウと同じようにファイルを保存しますが、プロジェクトはディスク上のフォルダーではなくPlayCanvasのサーバー上にあるため、.metaファイルはありません。各アセットにはIDがあり、インポート設定を編集する専用のインスペクターがあります。
| Unity | PlayCanvas |
|---|---|
Resources.Load<Texture2D>("logo") | this.app.assets.find('logo', 'texture')で見つけてから読み込む |
| AddressablesとAssetBundles | プリロードしないアセットを必要なときに読み込み、名前やタグで見つける |
| ScriptableObject | resourceが解析済みのオブジェクトになるJSONアセット |
| テクスチャのインポート設定 | テクスチャ圧縮を含むテクスチャのインスペクター |
| モデルのインポート設定 | アセットインポート設定 |
Preload が設定されたアセットは、アプリケーションの開始前にダウンロードされます。アセットインポート設定の Assets default to preload をオフにしない限り、新しいアセットはプリロードされます。一部のシーンでしか使わない大きなアセットでは、このオプションを外してください。
レンダリング
PlayCanvasのレンダラーは1つなので、レンダーパイプラインを選ぶ必要はありません。WebGPUまたはWebGL 2でレンダリングし、クラスタードライティングによって、オムニライトやスポットライトが多いシーンでも高速に描画できます。
マテリアルとシェーダー
PlayCanvasのマテリアルは、UnityのStandardシェーダーやURPのLitシェーダーと同様の物理ベースのマテリアルで、メタルネスとスペキュラーの両方のワークフローに対応しています。
| Unity | PlayCanvas |
|---|---|
| AlbedoまたはBase Map | Diffuse |
| Metallic | Metalness |
| Smoothness | Glossiness(どちらも値が大きいほど滑らか) |
| Normal Map | Normal |
| Occlusion | Ambient Occlusion |
| Emission | Emissive |
| Rendering Mode: Cutout | Alpha Test |
| Rendering Mode: TransparentまたはFade | Blend TypeとOpacity |
| TilingとOffset | TilingとOffset(すべてのマップ共通、またはマップごと) |
| Shader Graphとカスタムシェーダー | GLSLとWGSLで書くシェーダー |
UnityのMetallicマップは、赤チャンネルにメタルネス、アルファチャンネルにスムースネスを格納します。PlayCanvasでは、同じテクスチャを Metalness マップ(チャンネルR)と Glossiness マップ(チャンネルA)に割り当てます。Glossinessの Invert オプションはマップをラフネスマップとして扱うので、オフになっていることを確認してください。
ライティング
| Unity | PlayCanvas |
|---|---|
| Directional Light | ディレクショナルライト |
| Point Light | オムニライト |
| Spot Light | スポットライト。Inner Cone AngleとOuter Cone Angleはライトの軸から測るため、UnityのSpot Angleの半分になります。 |
| Area Light | レンダリング設定で Area Lights Enabled をオンにしてから、Shape をRectangle、Disk、Sphereのいずれかにしたライト |
| Lightingウィンドウの環境設定とフォグ | レンダリング設定(スカイボックス、アンビエントカラー、露出、フォグ) |
| Reflection Probe | すべてのマテリアルを照らすスカイボックスと、ボックス投影を使うマテリアルごとのキューブマップ |
| Light Probe | なし。ライトマップと環境ライティングを使います。 |
| Progressive Lightmapper | ツールバーの Bake から、または実行中のアプリで実行するライトマッパー |
ライティングを参照してください。
カメラとレイヤー
| Unity | PlayCanvas |
|---|---|
| Culling Mask | Layers |
| Depth、またはURPのカメラスタッキング | Priority(値が小さいほど先にレンダリング) |
| Clear Flags | Clear Color BufferとClear Depth Buffer |
| Viewport Rect | Viewport |
| Target Texture | スクリプトから設定するrenderTarget |
| Orthographic Size | Ortho Height |
UnityのGameObjectは1つのレイヤーにだけ属し、レイヤーはレンダリング、物理演算、レイキャストのすべてに使われます。PlayCanvasのレイヤーはレンダリング専用で、どのカメラがメッシュを描画するか、どの順序で描画するかを決めます。各カメラは、リストにあるレイヤーを Settings > Layers で設定した順序でレンダリングします。メッシュを描画するコンポーネントは、複数のレイヤーに属することができます。物理演算では、代わりにコリジョングループとマスクでフィルタリングします。複数のカメラを参照してください。
ポストプロセス
Unityのポストプロセスボリュームに相当するのはCameraFrameで、ブルーム、アンビエントオクルージョン、被写界深度、アンチエイリアス、カラーグレーディング、ビネットなどをカメラに追加します。エディターでは、カメラのエンティティにCameraFrameスクリプトを追加します。他のスクリプトと同じく起動したアプリでしか実行されないため、エフェクトはビューポートには表示されません。CameraFrameを使わない場合、トーンマッピングとガンマはCameraコンポーネントのプロパティです。
パフォーマンス機能
| Unity | PlayCanvas |
|---|---|
| 静的バッチングと動的バッチング | バッチグループ |
| GPUインスタンシング | ハードウェアインスタンシング |
| オクルージョンカリング | なし。カメラは視野外のメッシュをカリングします。 |
| LOD Group | メッシュにはなし |
| Profiler | デバッグとプロファイリングを参照 |
物理演算
PlayCanvasの物理演算は、物理エンジンBulletのWebAssembly版であるammo.jsで動作します。オプトイン方式なので、プロジェクトに追加するには、Settings を開いて PHYSICS を展開し、IMPORT AMMO をクリックします。重力は同じセクションで、シーンごとに設定します。物理演算を参照してください。
| Unity | PlayCanvas |
|---|---|
| Rigidbody | DynamicタイプのRigid Bodyコンポーネント |
| Is Kinematic を有効にしたRigidbody | KinematicタイプのRigid Body |
| Rigidbodyのないコライダー | Collisionコンポーネント と StaticタイプのRigid Body |
| Is Trigger を有効にしたコライダー | Rigid BodyのないCollisionコンポーネント |
AddForce(force) | entity.rigidbody.applyForce(force) |
AddForce(force, ForceMode.Impulse) | entity.rigidbody.applyImpulse(force) |
AddTorque | applyTorque、applyTorqueImpulse |
velocity(Unity 6ではlinearVelocity) | linearVelocity |
| Dynamicなボディの位置の設定 | entity.rigidbody.teleport(position) |
KinematicなボディのMovePosition | エンティティの位置を設定(Kinematicなボディはエンティティに追従します) |
| Constraints(位置や回転の固定) | Linear FactorとAngular Factor |
| Physic Material | Rigid BodyのFrictionとRestitution |
OnCollisionEnter、OnCollisionStay、OnCollisionExit | Collisionコンポーネントのcollisionstart、contact、collisionendイベント |
OnTriggerEnter、OnTriggerExit | トリガーのCollisionコンポーネントと、トリガーに入ったRigid Bodyで発生するtriggerenterとtriggerleaveイベント |
| レイヤーの衝突マトリックス | スクリプトから設定するコリジョングループとマスク |
Physics.Raycast | raycastFirstとraycastAll |
| Character Controller | なし。スクリプトでDynamicなRigid Bodyを動かします。 |
Unityの開発者の多くが、次の違いでつまずきます。
- 静的なコライダーにもRigid Bodyが必要です。 Collisionコンポーネントだけではトリガーボリュームになり、ボディを検出しますが、止めることはしません。床や壁には、StaticタイプのRigid Bodyコンポーネントを追加してください。
- プリミティブのコリジョン形状はスケールを無視します。 ボックス、球、カプセルの形状は、エンティティをどうスケールしてもHalf ExtentsやRadiusのままです。例外はメッシュ形状です。スケールを参照してください。
FixedUpdateはありません。 シミュレーションは1秒に60回の固定レートでステップし、力はupdateから加えます。衝突イベントは、物理ステップごとに1回発生します。- Dynamicなボディは物理エンジンが所有します。 各ステップの後、エンジンはすべてのDynamicなボディの位置と回転をそのエンティティに書き込みます。そのため、Dynamicなボディは、エンティティのトランスフォームを設定するのではなく、力、速度、
teleportで動かします。
ユーザーインターフェース
PlayCanvasのキャンバス内UIは、UnityのUI(uGUI)とほぼ同じように機能します。ユーザーインターフェースを参照してください。
| Unity | PlayCanvas |
|---|---|
| Screen Space - OverlayのCanvas | Screen Space を有効にしたScreenコンポーネント |
| World SpaceのCanvas | Screen Space を無効にしたScreenコンポーネント |
| Canvas Scaler: Scale With Screen Size | Scale Mode をBlendにし、Ref Resolution と Scale Blend を設定 |
| RectTransform | アンカー、ピボット、マージンを持つElementコンポーネント |
| Image、Raw Image | テクスチャまたはスプライトを表示する、ImageタイプのElement |
| Text、TextMeshPro | TTFまたはOTFファイルから作成したフォントアセットを使う、TextタイプのElement |
| Button | Buttonコンポーネント |
| Horizontal、Vertical、Grid Layout Group | Layout Groupコンポーネント |
| Layout Element | Layout Childコンポーネント |
| Scroll Rect、Scrollbar | Scroll ViewとScrollbarコンポーネント |
| Mask、Rect Mask 2D | Mask を有効にしたElement。マスクを参照してください。 |
| EventSystemとGraphic Raycaster | 入力を受け取るElementで Use Input を有効にする |
| Toggle、Slider、Dropdown | Elementで組み立てます。よく使うウィジェットを参照してください。 |
| UI Toolkit | HTMLとCSS |
アプリはWebページの中で動作するので、インターフェースのどの部分でも、HTMLとCSSで作ってキャンバスの上に重ねることができます。テキストの入力欄やアクセシブルなメニューは、この方法の方が簡単に作れます。
アニメーション
| Unity | PlayCanvas |
|---|---|
| Animatorコンポーネント | Animコンポーネント |
| Animator Controller | ステート、トランジション、パラメーター、レイヤーを持つanim state graphアセット |
| Animation Clip | モデルのインポート時に作成されるアニメーションアセット |
animator.SetFloat("Speed", 2f) | entity.anim.setFloat('Speed', 2) |
SetBool、SetInteger、SetTrigger | setBoolean、setInteger、setTrigger |
| Avatar Mask | レイヤーマスク |
| Blend Tree | コードで作成したステートグラフでのブレンドツリー(1D、2D、Direct)。エディターのステートグラフエディターにはありません。 |
| Root Motion | なし。スクリプトでエンティティを動かします。 |
| Animation Event | アニメーションアセットのAnimイベント |
| Animator Override Controller | 1つのanim state graphを使い、エンティティごとに異なるアニメーションアセットをステートに割り当てる |
PlayCanvasには、ヒューマノイドのリターゲットはありません。アニメーションはボーンを名前で動かすため、同じ名前のボーンを持つモデルであれば再生できますが、プロポーションの異なるスケルトンに合わせて調整されることはありません。アニメーションは、再生するスケルトン向けに作成するか、そのスケルトンにベイクしてください。
オーディオ
| Unity | PlayCanvas |
|---|---|
| Audio Source | Soundコンポーネントのスロット |
audioSource.Play() | entity.sound.play('slot name') |
| Spatial Blendを3Dに設定 | Soundコンポーネントの Positional |
| Min DistanceとMax Distance | Ref DistanceとMax Distance |
| Volume Rolloff | Distance Model(デフォルトはLinearで、UnityのデフォルトはLogarithmic) |
| Audio Listener | Audio Listenerコンポーネント |
| Audio Mixer | なし。Web Audio APIを使います。 |
ブラウザーは、ユーザーがクリックやキー入力などでページを操作するまで音を再生しません。音楽などのサウンドは、最初のクリックの後に開始してください。
コンテンツの移行
Unityのプロジェクトを自動で変換するツールはありません。アセットは標準的なファイル形式で移行し、コードは書き直します。
- モデルとアニメーション。 UnityGLTFやUnityのglTFastパッケージなどのglTFエクスポーターでGLBファイルとして、またはUnityのFBX ExporterパッケージでFBXとしてエクスポートします。エディターがインポートするのはglTFのバイナリ形式である
.glbで、.gltfではありません。ファイルをアセットパネルにドラッグすると、FBXファイルはGLBに変換され、各モデルからRender、マテリアル、テクスチャ、アニメーションのアセットと、その階層のテンプレートが作成されます。3Dモデルのインポートを参照してください。 - マテリアル。 Unityのシェーダーは移行できません。glTFエクスポーターはStandardやURP LitのマテリアルをglTFのPBRマテリアルに変換し、それがPlayCanvasのマテリアルとしてインポートされます。FBXファイルが持つマテリアルの情報は少ないため、FBXのマテリアルは設定し直すことになります。
- テクスチャ、オーディオ、フォント。 元のファイルをアップロードします。サポートされているフォーマットを参照してください。
- シーンとPrefab。 シーンの静的なジオメトリを1つのGLBとしてエクスポートしてレイアウトを保ち、ライト、カメラ、物理演算、UI、スクリプトはエディターで追加します。
- Terrain。 エクスポートする前に、Terrainをメッシュに変換します。
- スクリプトとシェーダー。 JavaScriptと、GLSLまたはWGSLで書き直します。AIコーディングアシスタントは、MonoBehaviourの変換を得意としています。エディターのプロジェクトのスクリプトをアシスタントに扱わせるには、VS Code拡張機能でスクリプトを同期します。シーンも編集させる場合は、MCPサーバーでエディターに接続します。
- ライトマップ。 PlayCanvasのライトマッパーでベイクし直すか、3Dモデリングツールでベイクしてモデルと一緒にインポートします。
公開
Unityはプラットフォームごとにプレイヤーをビルドします。PlayCanvasが公開するのは、HTML、JavaScript、アセットで構成されるWebアプリです。
- Publish はビルドをPlayCanvasでホストし、共有用のURLを発行します。PlayCanvasホスティングを参照してください。
- Download は、セルフホスティング用のZIPファイルを作成します。
- アプリストア向けには、Webアプリをモバイルやデスクトップ向けにラップするか、プレイアブル広告として公開します。
デバッグとプロファイリング
アプリはブラウザーで動作するので、起動したタブで、ブラウザーの開発者ツール(F12、macOSではCmd+Option+I)を使ってデバッグします。
| Unity | PlayCanvas |
|---|---|
| Consoleウィンドウ | 開発者ツールの Console タブ。コンソールロギングを参照してください。 |
| Visual StudioやRiderのアタッチ | 開発者ツールの Sources タブで設定するブレークポイント。ブラウザ開発ツールを参照してください。 |
Debug.Break() | 開発者ツールを開いているときにアプリを一時停止するdebugger;文 |
| Profiler | Launchボタンのメニューから使うプロファイラーとMiniStats、および開発者ツールの Performance タブ |
| Frame Debugger | 同じくLaunchボタンのメニューから使え、各フレームのレンダーパスとドローコールを表示するInspectorと、ネイティブのGPUプロファイラー |
Debug.DrawLineとGizmos | this.app.drawLine()と、球やボックスなどの形状を描くWireRenderer |
バージョン管理とコラボレーション
Unityのプロジェクトは、自分でバージョン管理の下に置くフォルダーです。PlayCanvasのプロジェクトはオンラインで共有されます。チームメンバーが同じシーンを開くと、リアルタイムで一緒に編集でき、互いの変更をその場で確認できます。履歴はエディターのバージョン管理で管理します。コミットのようにチェックポイントを作成し、ブランチで作業し、競合を解決しながらマージします。
組み込みの機能がないもの
| Unity | PlayCanvasでの代替手段 |
|---|---|
| Terrain | 地形をメッシュとしてインポート |
| NavMeshとAI Navigation | PlayCanvasに対応したrecast-navigation-jsなどのライブラリ |
| Character Controller | スクリプトで動かすDynamicなRigid Body、またはエンジンの一人称・三人称コントローラースクリプト |
| 2D物理 | Linear FactorとAngular Factorで3つ目の軸を固定した3D物理 |
| TimelineとCinemachine | アニメーションさせる値にCurve属性を使うスクリプト |
| VFX Graph | Particle Systemコンポーネント |
| Light Probeとリアルタイムのグローバルイルミネーション | ライトマップと環境ライティング |
| DOTweenなどのトゥイーンパッケージ | CDNからインポートするJavaScriptのトゥイーンライブラリ |