給 Unity 開發者
15 個主題,全部 Unity C# ↔ 網頁 TypeScript 並排。看完可以直接讀陀螺專案的程式碼、也能判斷 AI 寫得對不對。
先把生態系名詞對上——這層是 Unity 人最陌生的。
| Unity 生態 | 網頁生態 | 備註 |
|---|---|---|
| Unity Editor + Player | 瀏覽器(Chrome 等) | 執行環境。使用者的瀏覽器就是你的 Player |
| C# / .NET | TypeScript(TS) | 有型別的 JavaScript;語感跟 C# 很近(class/interface/泛型都有) |
| Package Manager / Asset Store | npm | 套件庫。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 強大) |
| PlayerPrefs | localStorage | 本地存檔 |
| Debug.Log | console.log | F12 開 DevTools 看 |
關鍵差異:網頁沒有強制的 GameObject/Component 架構——你自己決定物件長什麼樣。陀螺專案用「純資料物件+系統函式」(類似你們知道的 ECS 思路但更樸素)。
public class Zombie : MonoBehaviour {
public float hp = 100;
public float speed = 2;
void Update() {
transform.position += dir * speed * Time.deltaTime;
}
}
// 掛在 GameObject 上、場景裡拖出來// 純資料(沒有繼承任何引擎類)
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 改邏輯不會弄壞資料結構)。
關鍵差異:沒有引擎替你呼叫 Awake/Start/Update——主迴圈自己寫(就 5 行),初始化就是建構子/init 函式。
void Awake() { /* 建構期 */ }
void Start() { /* 首幀前 */ }
void Update() {
// 每幀,引擎自動呼叫
Move(Time.deltaTime);
}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);好消息:Three.js 的 Object3D 幾乎就是 Transform——position/rotation/scale/parent 全都在,API 一看就懂。
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);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 是高度」,我們踩過的「粒子噴到天上」就是這裡沒對齊。
[SerializeField] GameObject zombiePrefab;
var z = Instantiate(zombiePrefab, pos, rot);
z.GetComponent<Zombie>().hp = 200;// 「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); // ≈ InstantiatePrefab 的「視覺化編輯」部分沒有對應物——這就是總藍圖第 03 章「配置表外部化+編輯面板」要補的。
關鍵差異:沒有 Shader Graph——shader 直接寫 GLSL 文字(AI 很會寫)。標準材質跟 Unity 幾乎一一對應。
var mat = new Material(Shader.Find("Standard"));
mat.mainTexture = tex;
mat.EnableKeyword("_EMISSION");
mat.SetColor("_EmissionColor", Color.white * 0.2f);
renderer.material = mat;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):
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);
}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 直覺全部可用關鍵差異:AnimationMixer = 播放器;Animator Controller(狀態機)沒有對應物,要自己寫——陀螺專案的 stateMapper 就是手寫的狀態機(我們踩過「只搬 3 個狀態」的坑)。
animator.SetBool("isDead", true);
// 狀態機在 Animator 視窗連線
// transition 條件圖形化設定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 章)。概念完全相同。
關鍵差異:網頁是單執行緒。WaitForSeconds 類的邏輯建議用「計時器變數」(跟遊戲時間走),async/await 只用在載入。
IEnumerator Explode() {
warning.SetActive(true);
yield return new WaitForSeconds(1.5f);
DoExplosion();
}// 遊戲內計時 → 用狀態+計時器(尊重 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');if (Input.GetKey(KeyCode.W)) Move();
if (Input.GetKeyDown(KeyCode.Space)) Jump();// 瀏覽器只給事件,自己維護「按著」狀態:
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 行)這是網頁的主場:HTML/CSS 比 UGUI 強大且成熟,佈局/字型/動效/響應式全部內建,美術可以直接調。
// Canvas → Panel → Button + Text
// 錨點/Pivot/Layout Group 拖半天
button.onClick.AddListener(OnStart);
text.text = $"分數 {score}";<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 疊層畫)。
var tex = Resources.Load<Texture>("zombie_d");
// 或 Addressables 非同步// 資產放 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 章)。
瀏覽器只給你 rAF(幀率不定:60/120/掉幀)。兩種做法:
void FixedUpdate() {
// 固定 0.02s 一次,物理穩定
}// 法1(陀螺專案現況):可變步長 + dt 上限
const dt = Math.min(rawDt, 1/30); // 防切分頁回來爆衝
update(dt);
// 法2(要更穩時):固定步長累加器 ≈ FixedUpdate
acc += rawDt;
while (acc >= STEP) { fixedUpdate(STEP); acc -= STEP; }| Unity | 網頁 | |
|---|---|---|
| 建置 | Build(分鐘~小時) | npm run build(秒~1分)→ 產出一包靜態檔 |
| 發佈 | 裝機/上架流程 | 丟到任何靜態主機(我們:一支 bat,3 分鐘上線網址) |
| 更新 | 重新裝包 | 重新部署,玩家重新整理即最新版 |
| 開發預覽 | Editor Play | npm run dev 熱重載(改碼即時反映,比 Editor 快) |
| Unity | 網頁 | 備註 |
|---|---|---|
| Console 視窗 | DevTools Console(F12) | console.log/錯誤堆疊都在這 |
| 斷點除錯 | DevTools Sources/VS Code 附加 | TS 原始碼直接下斷點 |
| Profiler | DevTools Performance 分頁 | 火焰圖看每幀 CPU;Memory 分頁抓洩漏 |
| Frame Debugger | Spector.js(擴充) | 逐 draw call 檢視 WebGL |
| Inspector 即時調值 | 自建除錯面板 | 我們的 PERF 面板:FPS/切渲染模式/凍結畫面——這類面板 AI 半天做一個 |
| Unity 專案 | 陀螺專案 | 內容 |
|---|---|---|
| Assets/Scripts/(玩法) | src/game/ | GameEngine(主迴圈+玩法)、physics、CollisionSystem、systems/(AI/生成) |
| 渲染管線+Renderer | src/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 從這載 |
| ProjectSettings | package.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 面板切三種渲染模式對照畫面。半天可以建立全貌。