給 Unity 開發者

網頁遊戲對照手冊:你會的每件事,網頁上這樣寫

15 個主題,全部 Unity C# ↔ 網頁 TypeScript 並排。看完可以直接讀陀螺專案的程式碼、也能判斷 AI 寫得對不對。

1技術棧地圖:名詞對照

先把生態系名詞對上——這層是 Unity 人最陌生的。

Unity 生態網頁生態備註
Unity Editor + Player瀏覽器(Chrome 等)執行環境。使用者的瀏覽器就是你的 Player
C# / .NETTypeScript(TS)有型別的 JavaScript;語感跟 C# 很近(class/interface/泛型都有)
Package Manager / Asset Storenpm套件庫。package.json ≈ manifest.json
—(編譯器內建)Vite建置工具:TS 編譯+打包+熱重載開發伺服器
渲染管線(URP 等)Three.js(架在 WebGL 上)3D 函式庫,等於「輕量版 Unity 渲染層」,API 很像
Canvas 2D瀏覽器內建 2D 繪圖 API(Unity 沒有對應物;像 GL 立即模式畫 2D)
DOM / HTML / CSS網頁本體。遊戲外的 UI 用它(比 UGUI 強大)
PlayerPrefslocalStorage本地存檔
Debug.Logconsole.logF12 開 DevTools 看

2GameObject 與組件

關鍵差異:網頁沒有強制的 GameObject/Component 架構——你自己決定物件長什麼樣。陀螺專案用「純資料物件+系統函式」(類似你們知道的 ECS 思路但更樸素)。

UNITY C#
public class Zombie : MonoBehaviour {
    public float hp = 100;
    public float speed = 2;
    void Update() {
        transform.position += dir * speed * Time.deltaTime;
    }
}
// 掛在 GameObject 上、場景裡拖出來
網頁 TS(陀螺專案的做法)
// 純資料(沒有繼承任何引擎類)
interface Zombie {
    x: number; y: number;
    hp: number; speed: number;
}
// 集中放在陣列,系統函式統一處理
engine.zombies: Zombie[]
// update 時:
for (const z of engine.zombies) {
    z.x += dirX * z.speed * dt;
}

Unity 的「行為跟著物件走」(MonoBehaviour)在網頁上是選擇之一而非強制。資料與邏輯分離的寫法對 AI 更友善(AI 改邏輯不會弄壞資料結構)。

3生命週期與遊戲迴圈

關鍵差異:沒有引擎替你呼叫 Awake/Start/Update——主迴圈自己寫(就 5 行),初始化就是建構子/init 函式。

UNITY C#
void Awake()  { /* 建構期 */ }
void Start()  { /* 首幀前 */ }
void Update() {
    // 每幀,引擎自動呼叫
    Move(Time.deltaTime);
}
網頁 TS
const engine = new GameEngine(); // ≈ Awake
engine.init();                    // ≈ Start

let last = performance.now();
function loop(now: number) {
    const dt = (now - last) / 1000; // ≈ Time.deltaTime
    last = now;
    engine.update(dt);   // ≈ Update(自己呼叫)
    engine.render();
    requestAnimationFrame(loop); // 下一幀再來(≈60fps)
}
requestAnimationFrame(loop);

4Transform 與場景樹

好消息:Three.js 的 Object3D 幾乎就是 Transform——position/rotation/scale/parent 全都在,API 一看就懂。

UNITY C#
var go = new GameObject("boss");
go.transform.position = new Vector3(0, 2, 5);
go.transform.rotation = Quaternion.Euler(0, 90, 0);
go.transform.localScale = Vector3.one * 2;
go.transform.SetParent(parent);
THREE.JS TS
const boss = new THREE.Group();
boss.position.set(0, 2, 5);
boss.rotation.y = Math.PI / 2;  // 弧度制!90°=π/2
boss.scale.setScalar(2);
parent.add(boss);               // = SetParent
scene.add(parent);              // scene ≈ 場景根節點

⚠️ 兩個坑:①角度是弧度不是度 ②座標慣例要自訂——陀螺專案定「遊戲 XY 平面 = Three 的 XZ 平面、Y 是高度」,我們踩過的「粒子噴到天上」就是這裡沒對齊。

5Prefab 與生成

UNITY C#
[SerializeField] GameObject zombiePrefab;
var z = Instantiate(zombiePrefab, pos, rot);
z.GetComponent<Zombie>().hp = 200;
網頁 TS(配置+工廠)
// 「Prefab」= 配置資料(可外部化成 JSON 給企劃改)
const ZOMBIE_CONFIGS = {
  mummy: { model: 'Mummy_Ani.fbx', hp: 200, speed: 1.2 },
};
// 「Instantiate」= 工廠函式
function spawnZombie(type: string, x: number, y: number) {
    const cfg = ZOMBIE_CONFIGS[type];
    engine.zombies.push({ x, y, hp: cfg.hp, ... });
}
// 3D 模型的複製:
const clone = template.clone(true);  // ≈ Instantiate

Prefab 的「視覺化編輯」部分沒有對應物——這就是總藍圖第 03 章「配置表外部化+編輯面板」要補的。

6Material 與 Shader

關鍵差異:沒有 Shader Graph——shader 直接寫 GLSL 文字(AI 很會寫)。標準材質跟 Unity 幾乎一一對應。

UNITY C#
var mat = new Material(Shader.Find("Standard"));
mat.mainTexture = tex;
mat.EnableKeyword("_EMISSION");
mat.SetColor("_EmissionColor", Color.white * 0.2f);
renderer.material = mat;
THREE.JS TS
const mat = new THREE.MeshStandardMaterial({
    map: tex,                  // = mainTexture
    emissive: 0xffffff,        // = _EmissionColor
    emissiveIntensity: 0.2,
});
mesh.material = mat;
// Lambert/Basic/Physical 等對應 Unity 各光照模型

自訂 shader(對照 Unity 的 HLSL 手寫 shader):

UNITY SHADERLAB/HLSL
fixed4 frag(v2f i) : SV_Target {
    float d = distance(i.uv, 0.5) * 2;
    float ring = smoothstep(r-w, r, d)
               * (1 - smoothstep(r, r+w, d));
    return fixed4(_Color.rgb, ring * _Alpha);
}
THREE.JS GLSL(我們的衝擊波,幾乎同一段)
void main() {
    float d = length(vUv - 0.5) * 2.0;
    float ring = smoothstep(r-w, r, d)
               * (1.0 - smoothstep(r, r+w, d));
    gl_FragColor = vec4(uColor, ring * uAlpha);
}
// HLSL 和 GLSL 是近親——你們的 shader 直覺全部可用

7Animator 與動畫

關鍵差異:AnimationMixer = 播放器;Animator Controller(狀態機)沒有對應物,要自己寫——陀螺專案的 stateMapper 就是手寫的狀態機(我們踩過「只搬 3 個狀態」的坑)。

UNITY C#
animator.SetBool("isDead", true);
// 狀態機在 Animator 視窗連線
// transition 條件圖形化設定
THREE.JS TS
const mixer = new THREE.AnimationMixer(model);
const walk = mixer.clipAction(walkClip);
const dead = mixer.clipAction(deadClip);
// 「狀態機」自己寫(我們的 resolveAnim):
function pickClip(z: Zombie) {
    if (z.hp <= 0) return { clip: 'dead', mode: 'clamp' };
    if (z.hurtTimer > 0) return { clip: 'hurt', mode: 'loop' };
    return { clip: 'walk', mode: 'loop' };
}
mixer.update(dt);  // 每幀推進

大量同種角色的動畫:Unity 你們會想到 GPU Instancing + Animation Instancing;網頁對應 = InstancedMesh + VAT(總藍圖 02 章)。概念完全相同。

8Coroutine 與非同步

關鍵差異:網頁是單執行緒。WaitForSeconds 類的邏輯建議用「計時器變數」(跟遊戲時間走),async/await 只用在載入。

UNITY C#
IEnumerator Explode() {
    warning.SetActive(true);
    yield return new WaitForSeconds(1.5f);
    DoExplosion();
}
網頁 TS(計時器變數,可暫停可回放)
// 遊戲內計時 → 用狀態+計時器(尊重 timeScale/暫停)
z.explodeTimer = 1.5;
// update 內:
if (z.explodeTimer !== undefined) {
    z.explodeTimer -= dt;
    if (z.explodeTimer <= 0) doExplosion(z);
}
// 資源載入才用 async/await(≈ C# 的 await):
const fbx = await loader.loadAsync('Mummy.fbx');

9Input 輸入

UNITY C#
if (Input.GetKey(KeyCode.W)) Move();
if (Input.GetKeyDown(KeyCode.Space)) Jump();
網頁 TS(事件 → 狀態變數)
// 瀏覽器只給事件,自己維護「按著」狀態:
const keys: Record<string, boolean> = {};
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup',   e => keys[e.code] = false);
// update 內就跟 Unity 一樣了:
if (keys['KeyW']) move();
// GetKeyDown = 記上一幀狀態比對(自己包 3 行)

10UGUI 與網頁 UI

這是網頁的主場:HTML/CSS 比 UGUI 強大且成熟,佈局/字型/動效/響應式全部內建,美術可以直接調。

UNITY(UGUI)
// Canvas → Panel → Button + Text
// 錨點/Pivot/Layout Group 拖半天
button.onClick.AddListener(OnStart);
text.text = $"分數 {score}";
網頁(HTML+CSS+TS)
<button id="start" class="btn">開始</button>
<div id="score"></div>

/* CSS:樣式/齊法/動效全在這 */
.btn { font-size: 20px; border-radius: 12px; }

// TS:
startBtn.onclick = onStart;
scoreDiv.textContent = `分數 ${score}`;

世界內 UI(怪頭上血條)例外——要跟著鏡頭走,畫在遊戲渲染層內(我們用 canvas 疊層畫)。

11資源載入

UNITY C#
var tex = Resources.Load<Texture>("zombie_d");
// 或 Addressables 非同步
網頁 TS
// 資產放 public/ 目錄,用 loader 非同步載:
const tex = await new THREE.TextureLoader()
    .loadAsync('/Textures/zombie_d.png');
const fbx = await new FBXLoader()
    .loadAsync('/FBX-1/Mummy_Ani.fbx');
tex.colorSpace = THREE.SRGBColorSpace; // ≈ sRGB 勾選

匯入設定(壓縮/mipmap/sRGB)沒有 Inspector——是程式碼參數(我們踩過漏設 anisotropy 導致貼圖又糊又暗)。資產規格要立文件(總藍圖 03 章)。

12物理步長:沒有 FixedUpdate 怎麼辦

瀏覽器只給你 rAF(幀率不定:60/120/掉幀)。兩種做法:

UNITY
void FixedUpdate() {
    // 固定 0.02s 一次,物理穩定
}
網頁 TS
// 法1(陀螺專案現況):可變步長 + dt 上限
const dt = Math.min(rawDt, 1/30); // 防切分頁回來爆衝
update(dt);

// 法2(要更穩時):固定步長累加器 ≈ FixedUpdate
acc += rawDt;
while (acc >= STEP) { fixedUpdate(STEP); acc -= STEP; }

13Build 與部署

Unity網頁
建置Build(分鐘~小時)npm run build(秒~1分)→ 產出一包靜態檔
發佈裝機/上架流程丟到任何靜態主機(我們:一支 bat,3 分鐘上線網址)
更新重新裝包重新部署,玩家重新整理即最新版
開發預覽Editor Playnpm run dev 熱重載(改碼即時反映,比 Editor 快)

14Debug 與 Profiler

Unity網頁備註
Console 視窗DevTools Console(F12)console.log/錯誤堆疊都在這
斷點除錯DevTools Sources/VS Code 附加TS 原始碼直接下斷點
ProfilerDevTools Performance 分頁火焰圖看每幀 CPU;Memory 分頁抓洩漏
Frame DebuggerSpector.js(擴充)逐 draw call 檢視 WebGL
Inspector 即時調值自建除錯面板我們的 PERF 面板:FPS/切渲染模式/凍結畫面——這類面板 AI 半天做一個

15專案結構對照(直接讀懂陀螺專案)

Unity 專案陀螺專案內容
Assets/Scripts/(玩法)src/game/GameEngine(主迴圈+玩法)、physics、CollisionSystem、systems/(AI/生成)
渲染管線+Renderersrc/render-pipeline/3D 場景/相機/怪物/道具/特效各 adapter(Three.js 這層)
舊版 2D 畫面碼src/game/renderers/Canvas 2D 各 renderer(legacy 模式+compat 疊層)
Animator/狀態橋接src/game/stateMapper.ts遊戲狀態 → 渲染狀態的翻譯官(動畫狀態機在這)
Assets/(美術資源)public/FBX-1/、Textures/FBX 模型、貼圖,loader 從這載
ProjectSettingspackage.json、vite.config、tsconfig依賴/建置/型別設定
—(引擎內建)src/game/perfConfig.ts+PERF 面板渲染模式切換(legacy/pipeline/compat)+除錯開關

上手路徑建議(Unity 人版):①開 GameEngine.ts 找 update/_renderFrame——對應你們的 Update,10 分鐘抓到骨架 → ②看 stateMapper.ts——理解 2D/3D 橋接 → ③挑一個 render-pipeline 的 adapter 讀(建議 ShockwaveFxAdapter,短、含 shader)→ ④開 PERF 面板切三種渲染模式對照畫面。半天可以建立全貌。