一場關於Web 3D遊戲開發的完整旅程,從構思到實現,探索空間計算時代的遊戲開發新範式

引言:為什麼選擇Web 3D開發?

在XR(擴展現實)技術蓬勃發展的今天,空間計算正在改變我們與數字世界的交互方式。作為一名開發者,我一直在思考:如何以最低的門檻進入這個激動人心的領域?答案是——從Web 3D開發開始。

Web技術棧具有天然的優勢:無需下載安裝跨平台運行、快速迭代、易於分享。更重要的是,掌握了Web 3D開發的核心技能後,遷移到專業XR平台(Rokid空間計算平台)只需要瞭解平台特定的API即可。這就像學會了做菜的基本功,換個廚房依然能烹飪美味。

本文將帶你完整經歷《光之島》這款3D收集遊戲的開發過程,從技術選型到架構設計,從基礎搭建到核心玩法實現,每一步都會詳細剖析背後的原理和決策依據。

下面為演示視頻:

https://ai.feishu.cn/wiki/FrxUwh8Uoijt7IkyJyLchibAnjf?fromScene=spaceOverview#share-IS1HdSij3oDdDExSuvtcanWynHg

一、項目構思:從想法到設計

1.1 遊戲創意的誕生

《光之島》的靈感來源於經典的3D平台跳躍遊戲。我希望創造一個簡單卻有趣的遊戲體驗:玩家控制角色在浮空島嶼上自由移動,收集散落各處的發光水晶,在限定時間內完成挑戰。

這個設計看似簡單,實則包含了3D遊戲開發的核心要素:

  • 3D場景渲染:構建視覺世界
  • 物理模擬:真實的重力和碰撞
  • 角色控制:流暢的移動和跳躍
  • 遊戲邏輯:計時、計分、勝負判定
  • 用户交互:鍵盤輸入和UI反饋

1.2 技術選型的思考

在開始編碼前,技術選型是最關鍵的決策。我選擇了以下技術棧:

Vite - 下一代前端構建工具

  • 極速的熱模塊替換(HMR),修改代碼後幾乎瞬間看到效果
  • 基於ESM的開發服務器,無需打包即可運行
  • 優化的生產構建,自動代碼分割和資源優化

TypeScript - JavaScript的超集

  • 強類型系統在大型項目中尤為重要,能在編碼階段發現90%的錯誤
  • 優秀的IDE支持,代碼提示和重構功能大幅提升開發效率
  • 接口和類型定義讓代碼自解釋,減少文檔負擔

Three.js - WebGL封裝庫

  • 降低了WebGL的複雜度,用面向對象的方式操作3D圖形
  • 豐富的幾何體、材質、光照系統,開箱即用
  • 活躍的社區和完善的文檔,遇到問題容易找到解決方案

Cannon-es - 物理引擎

  • 輕量級(~150KB),適合Web環境
  • 提供剛體動力學、碰撞檢測、約束系統等完整功能
  • 與Three.js配合使用的案例豐富,集成簡單

這套組合既保證了開發效率,又能實現複雜的3D交互效果,是Web 3D遊戲開發的最佳實踐之一。

二、環境搭建:打好基礎很重要

2.1 項目初始化

首先創建Vite項目,這個過程非常簡單:

npm create vite@latest isle-of-light -- --template vanilla-ts

cd isle-of-light

npm install

Vite的初始化速度極快,幾秒鐘就能完成。vanilla-ts模板提供了最純粹的TypeScript環境,沒有多餘的框架依賴,非常適合3D應用開發。

2.2 安裝核心依賴

npm install three cannon-es
npm install --save-dev @types/three

Three.js提供了完整的TypeScript類型定義,這讓開發體驗非常好。Cannon-es本身就是用TypeScript編寫的,類型支持開箱即用。

2.3 項目結構設計

【徵文計劃】從零開始XR開發:基於Rokid空間計算平台打造《光之島》3D遊戲_物理模擬

優秀的項目結構是可維護性的基石。我採用了職責分離的設計原則:

src/

├── components/ # 遊戲實體組件

│ ├── Ground.ts # 地面:靜態場景元素

│ ├── Player.ts # 玩家:動態角色實體

│ └── Crystal.ts # 水晶:可收集物品

├── controls/ # 輸入控制模塊

│ └── PlayerControls.ts

├── physics/ # 物理引擎封裝

│ └── PhysicsWorld.ts

├── utils/ # 工具函數(預留)

├── main.ts # 遊戲主入口

└── style.css # 樣式文件

這種結構的優勢在於:

  • 高內聚:相關功能聚合在一起
  • 低耦合:模塊間依賴清晰,易於修改
  • 可擴展:新增功能只需添加新模塊
  • 易測試:每個模塊都可以獨立測試

三、核心系統開發

3.1 渲染系統:構建視覺基礎

3D渲染的核心是場景(Scene)、相機(Camera)、渲染器(Renderer)這三大件。我將它們封裝在Game類的構造函數中:

class Game {
  private scene: THREE.Scene;
  private camera: THREE.PerspectiveCamera;
  private renderer: THREE.WebGLRenderer;
  private clock: THREE.Clock;

  constructor() {
    // 場景是所有3D對象的容器
    this.scene = new THREE.Scene();

    // 透視相機模擬人眼視角,FOV設為75度是常用值
    this.camera = new THREE.PerspectiveCamera(
      75,                                     // 視野角度
      window.innerWidth / window.innerHeight, // 寬高比
      0.1,                                    // 近裁剪面
      1000                                    // 遠裁剪面
    );
    this.camera.position.set(0, 5, 10);

    // WebGL渲染器,開啓抗鋸齒提升畫質
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.shadowMap.enabled = true;  // 啓用陰影
    document.body.appendChild(this.renderer.domElement);

    // 時鐘用於計算幀間隔
    this.clock = new THREE.Clock();
  }
}

關鍵設計點解析:

  1. 相機參數的選擇:FOV(視野角度)75度是個甜蜜點,太小會產生望遠鏡效果,太大會畸變。近裁剪面0.1,遠裁剪面1000,這個範圍足夠覆蓋我們的場景。
  2. 渲染器配置antialias: true開啓抗鋸齒,雖然消耗性能,但畫面質量提升明顯。shadowMap.enabled啓用陰影系統,讓場景更有立體感。
  3. 時鐘系統:Three.js的Clock用於精確計算時間差,這對物理模擬和動畫至關重要。

3.2 光照系統:營造氛圍的魔法

光照決定了場景的氛圍。我採用了環境光+平行光的經典組合:

// 環境光提供無方向的基礎照明
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);

// 平行光模擬太陽光,產生方向性陰影
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;

// 配置陰影質量
directionalLight.shadow.camera.top = 20;
directionalLight.shadow.camera.bottom = -20;
directionalLight.shadow.camera.left = -20;
directionalLight.shadow.camera.right = 20;

this.scene.add(directionalLight);

光照設計原則:

  • 環境光強度0.5:提供基礎照明,防止場景過暗,但不能太亮否則會失去立體感
  • 平行光強度0.8:主光源,產生明確的陰影方向
  • 光源位置(10,20,10):從右上方照射,符合自然光習慣
  • 陰影相機範圍:覆蓋整個遊戲場景,確保所有物體都能投射陰影

3.3 物理系統:賦予世界真實感

物理引擎是遊戲真實感的來源。Cannon-es的配置需要仔細調整:

export class PhysicsWorld {
  public world: CANNON.World;

  constructor() {
    this.world = new CANNON.World();

    // 地球重力加速度,負值表示向下
    this.world.gravity.set(0, -9.82, 0);

    // 碰撞檢測算法:NaiveBroadphase適合物體不多的場景
    this.world.broadphase = new CANNON.NaiveBroadphase();

    // 求解器迭代次數,影響碰撞精度和性能
    this.world.solver.iterations = 10;

    // 允許靜止物體休眠,優化性能
    this.world.allowSleep = true;
  }
}

物理參數深度解析:

  1. 重力值9.82:標準地球重力,如果做月球遊戲可以調整為1.62,創造不同的跳躍手感
  2. Broadphase選擇
  1. NaiveBroadphase:O(n²)複雜度,物體少時效率高
  2. SAPBroadphase:掃描排序,物體多時更優
  3. 我們的遊戲物體少,選擇Naive即可
  1. Solver迭代次數
  1. 越高越精確,但計算量越大
  2. 10次是經驗值,平衡了精度和性能
  3. 快速運動的物體可能需要提高這個值 1.
  1. Sleep機制
  1. 靜止物體不參與物理計算
  2. 大幅降低CPU佔用
  3. 玩家觸碰時會自動喚醒 1.

3.4 遊戲循環:驅動一切的引擎

遊戲循環是整個系統的心臟,每秒跳動60次:

private animate(): void {
  requestAnimationFrame(() => this.animate());

  const deltaTime = this.clock.getDelta();

  if (this.gameActive) {
    // 1. 更新倒計時
    this.timeLeft -= deltaTime;

    // 2. 更新物理世界(固定時間步長)
    this.physicsWorld.world.step(1/60, deltaTime, 3);

    // 3. 更新玩家輸入
    this.playerControls.update();

    // 4. 同步物理到視覺
    this.player.update();
    this.crystals.forEach(crystal => crystal.update());

    // 5. 更新攝像機
    this.updateCamera();

    // 6. 檢查遊戲狀態
    this.checkGameState();

    // 7. 更新UI
    this.updateUI();
  }

  // 8. 渲染畫面
  this.renderer.render(this.scene, this.camera);
}

執行順序的重要性:

這個順序是經過深思熟慮的:

  1. 先更新時間,確保計時準確
  2. 物理模擬必須在輸入處理後,才能響應玩家操作
  3. 視覺同步在物理計算後,保證看到的就是真實的
  4. 攝像機更新在物體更新後,才能正確跟隨
  5. 狀態檢查在所有更新後,判斷準確
  6. UI更新在狀態檢查後,顯示最新信息
  7. 渲染必須在最後,呈現完整的一幀

固定時間步長的意義:

world.step(1/60, deltaTime, 3)這行代碼很關鍵:

  • 第一參數1/60:固定時間步長,確保物理模擬穩定
  • 第二參數deltaTime:實際經過的時間
  • 第三參數3:最大子步數,處理幀率波動

即使幀率不穩定,物理模擬依然準確。

四、遊戲對象設計

4.1 地面系統:

地面是玩家活動的舞台,需要同時處理視覺和物理兩個層面:

export class Ground {
  public mesh: THREE.Mesh;
  public body: CANNON.Body;

  constructor(scene: THREE.Scene, world: CANNON.World) {
    // 視覺層:50x50的巨大平面
    const geometry = new THREE.PlaneGeometry(50, 50);
    const material = new THREE.MeshStandardMaterial({
      color: 0x4a9eff,      // 天藍色,營造浮空島氛圍
      roughness: 0.8,       // 較高粗糙度,非光滑表面
      metalness: 0.2        // 低金屬度,更像岩石
    });

    this.mesh = new THREE.Mesh(geometry, material);
    this.mesh.rotation.x = -Math.PI / 2;  // 旋轉90度平躺
    this.mesh.receiveShadow = true;       // 接收其他物體的陰影
    scene.add(this.mesh);

    // 物理層:無限平面
    const shape = new CANNON.Plane();
    this.body = new CANNON.Body({
      mass: 0,  // 質量為0 = 靜態物體,不受力影響
      shape: shape
    });
    this.body.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
    world.addBody(this.body);
  }
}

設計細節:

  1. 為什麼是50x50:足夠大的平面讓玩家不會輕易掉出邊界,又不會太大導致迷失方向
  2. 材質參數調優
  1. 天藍色(0x4a9eff)與"光之島"的主題契合
  2. roughness 0.8讓表面有質感,不是鏡面反射
  3. metalness 0.2保持一點反光,增加視覺趣味
  1. 物理平面特性
  1. Cannon.Plane是無限大的,不用擔心邊界
  2. mass=0的物體完全靜止,碰撞時隻影響動態物體
  3. 四元數旋轉比歐拉角更穩定,避免萬向鎖問題

4.2 玩家角色:遊戲的主角

玩家角色的設計需要兼顧視覺美觀和物理合理性:

export class Player {
  public mesh: THREE.Mesh;
  public body: CANNON.Body;

  constructor(scene: THREE.Scene, world: CANNON.World) {
    // 視覺:膠囊體模擬人形
    const geometry = new THREE.CapsuleGeometry(0.5, 1, 4, 8);
    const material = new THREE.MeshStandardMaterial({
      color: 0xff6b35,    // 活力橙色,醒目
      roughness: 0.7,
      metalness: 0.3
    });

    this.mesh = new THREE.Mesh(geometry, material);
    this.mesh.castShadow = true;  // 投射陰影
    scene.add(this.mesh);

    // 物理:球體簡化碰撞
    const shape = new CANNON.Sphere(0.5);
    this.body = new CANNON.Body({
      mass: 5,                            // 質量影響慣性
      position: new CANNON.Vec3(0, 3, 0), // 出生在空中
      linearDamping: 0.9                  // 阻尼模擬空氣阻力
    });
    this.body.addShape(shape);
    world.addBody(this.body);
  }

  update(): void {
    // 核心:物理驅動視覺
    this.mesh.position.copy(this.body.position as any);
    this.mesh.quaternion.copy(this.body.quaternion as any);
  }
}

關鍵決策解釋:

  1. 為何視覺用膠囊,物理用球
  1. 膠囊體外形更接近人形,視覺效果好
  2. 球體碰撞計算最簡單,性能最優
  3. 球體在斜坡上自然滾動,符合物理直覺 1.
  1. 質量設為5
  1. 太輕(<1):容易被碰撞彈飛
  2. 太重(>10):跳躍需要很大的力
  3. 5是經過測試的最佳值 1.
  1. 線性阻尼0.9
  1. 0表示無阻力,在真空中永動
  2. 1表示瞬間停止
  3. 0.9讓角色有慣性又能快速停下 1.
  1. 初始位置(0,3,0)
  1. Y=3確保在地面上方
  2. 開局掉落增加動態感
  3. 測試物理引擎是否正常工作 1.

4.3 水晶系統:目標與獎勵

水晶是遊戲的核心目標,需要吸引玩家注意:

export class Crystal {
  public mesh: THREE.Mesh;
  public body: CANNON.Body;

  constructor(scene: THREE.Scene, world: CANNON.World, position: THREE.Vector3) {
    // 視覺:發光的寶石
    const geometry = new THREE.IcosahedronGeometry(0.5, 0);
    const material = new THREE.MeshStandardMaterial({
      color: 0xffeb3b,          // 金黃色
      emissive: 0xffeb3b,       // 自發光
      emissiveIntensity: 0.5,   // 發光強度
      roughness: 0.3,           // 低粗糙度,晶瑩剔透
      metalness: 0.8            // 高金屬度,反光強烈
    });

    this.mesh = new THREE.Mesh(geometry, material);
    this.mesh.position.copy(position);
    this.mesh.castShadow = true;
    scene.add(this.mesh);

    // 物理:觸發器而非碰撞體
    const shape = new CANNON.Sphere(0.5);
    this.body = new CANNON.Body({
      mass: 0,
      position: new CANNON.Vec3(position.x, position.y, position.z),
      collisionResponse: false,  // 關鍵:不產生碰撞響應
      isTrigger: true
    });
    this.body.addShape(shape);
    (this.body as any).isCrystal = true;  // 標記身份
    world.addBody(this.body);
  }

  update(): void {
    // 持續旋轉,吸引注意
    this.mesh.rotation.y += 0.02;
  }
}

視覺設計的心理學:

  1. 二十面體幾何
  1. 接近球形但有稜角
  2. 多個面產生豐富的光影變化
  3. 符合"寶石/水晶"的心理預期 1.
  1. 金黃色+自發光
  1. 黃色代表價值和獎勵(遊戲設計通用規律)
  2. 自發光讓水晶在遠處也清晰可見
  3. emissiveIntensity 0.5不會太刺眼 1.
  1. 材質參數組合
  1. 低粗糙度(0.3) + 高金屬度(0.8) = 寶石質感
  2. 反射環境光,產生璀璨效果
  3. 與場景中其他物體形成鮮明對比 1.

觸發器機制:

collisionResponse: false是關鍵設置:

  • 玩家可以"穿過"水晶(不會被彈開)
  • 但仍然觸發碰撞事件
  • 實現了"觸碰即收集"的流暢體驗
  • 避免了物理碰撞導致的異常行為

旋轉動畫的講究:

每幀旋轉0.02弧度(約1.15度):

  • 一圈需要約5秒(2π/0.02/60 ≈ 5.2秒)
  • 速度適中,既有動感又不眼花
  • 勻速旋轉比變速更穩定,減少渲染負擔

五、交互系統設計

5.1 玩家控制:打造流暢操作

操作手感是遊戲體驗的核心,需要精心調校:

export class PlayerControls {
  private body: CANNON.Body;
  private keysPressed: { [key: string]: boolean } = {};
  private moveSpeed: number = 5;
  private jumpForce: number = 8;
  private isGrounded: boolean = false;

  constructor(body: CANNON.Body) {
    this.body = body;

    // 鍵盤事件監聽
    document.addEventListener('keydown', (e) => {
      this.keysPressed[e.key.toLowerCase()] = true;
    });

    document.addEventListener('keyup', (e) => {
      this.keysPressed[e.key.toLowerCase()] = false;
    });

    // 地面檢測:監聽物理碰撞
    this.body.addEventListener('collide', (e: any) => {
      const contact = e.contact;
      // 檢查碰撞法線方向
      if (contact.ni.y > 0.5 || contact.nj.y > 0.5) {
        this.isGrounded = true;
      }
    });
  }

  update(): void {
    const velocity = new CANNON.Vec3();

    // WASD移動
    if (this.keysPressed['w']) velocity.z -= this.moveSpeed;
    if (this.keysPressed['s']) velocity.z += this.moveSpeed;
    if (this.keysPressed['a']) velocity.x -= this.moveSpeed;
    if (this.keysPressed['d']) velocity.x += this.moveSpeed;

    // 直接設置水平速度,保留垂直速度
    this.body.velocity.x = velocity.x;
    this.body.velocity.z = velocity.z;

    // 跳躍:只在地面時生效
    if (this.keysPressed[' '] && this.isGrounded) {
      this.body.velocity.y = this.jumpForce;
      this.isGrounded = false;
    }
  }
}

操作手感優化技巧:

  1. 移動速度調校(5)
  1. 速度1-3:太慢,玩家急躁
  2. 速度5-7:舒適區間
  3. 速度10+:太快,失控感
  4. 最終選5,經過多次試玩調整 1.
  1. 跳躍力度設計(8)
  1. 與重力9.82配合
  2. 跳躍高度約3.2單位(v²/2g = 64/19.64)
  3. 滯空時間約1.6秒(2v/g)
  4. 足夠跨越障礙,又不會太飄 1.
  1. 地面檢測的智慧
  1. if (contact.ni.y > 0.5 || contact.nj.y > 0.5)
  2. ni和nj是碰撞法線的兩個方向
  3. Y > 0.5意味着法線向上(cos45° ≈ 0.7)
  4. 允許在斜坡上跳躍(不超過45度)
  5. 防止在牆壁上跳躍 1.
  1. 直接設置速度 vs 施加力
  1. 施加力:更真實,但響應慢,有慣性
  2. 直接設速度:立即響應,操作精確
  3. 遊戲偏向後者,犧牲真實換操作性
  4. 保留Y軸速度,重力效果不受影響 1.

5.2 攝像機系統:玩家的眼睛

第三人稱視角需要平滑的攝像機跟隨:

private cameraOffset: THREE.Vector3 = new THREE.Vector3(0, 5, 10);

private updateCamera(): void {
  // 目標位置 = 玩家位置 + 固定偏移
  const targetPosition = new THREE.Vector3()
    .copy(this.player.mesh.position)
    .add(this.cameraOffset);

  // 線性插值實現平滑移動
  this.camera.position.lerp(targetPosition, 0.1);

  // 始終注視玩家
  this.camera.lookAt(this.player.mesh.position);
}

攝像機設計的藝術:

  1. 偏移向量(0, 5, 10)
  1. Y=5:俯視角度,觀察地面和障礙
  2. Z=10:距離適中,不太近不太遠
  3. X=0:居中,左右視野對稱 1.
  1. Lerp插值係數0.1
  1. 0.01:太慢,攝像機落後明顯
  2. 0.1:恰好,輕微延遲感
  3. 0.5:太快,幾乎剛性跟隨
  4. 插值產生自然的"彈性"效果 1.
  1. lookAt的作用
  1. 自動計算攝像機旋轉
  2. 無論玩家移動到哪裏,始終在視野中心
  3. 比手動計算歐拉角簡單可靠 1.
  1. 高級技巧(本項目未實現)
  1. 碰撞檢測:防止攝像機穿牆
  2. 動態距離:根據速度調整遠近
  3. 視角平滑:鼠標控制環視 1.

5.3 碰撞檢測:收集的實現

水晶收集是遊戲的核心互動:

constructor() {
  // ... 其他初始化代碼

  // 監聽玩家的碰撞事件
  this.player.body.addEventListener('collide', (e: any) => {
    this.onPlayerCollide(e);
  });
}

private onPlayerCollide(e: any): void {
  const otherBody = e.body || e.target;

  // 檢查碰撞對象是否為水晶
  if (otherBody.isCrystal) {
    const crystalIndex = this.crystals.findIndex(
      c => c.body === otherBody
    );

    if (crystalIndex !== -1) {
      const crystal = this.crystals[crystalIndex];

      // 三重移除:視覺、物理、數據
      this.scene.remove(crystal.mesh);
      this.physicsWorld.world.removeBody(crystal.body);
      this.crystals.splice(crystalIndex, 1);

      // 更新分數
      this.score++;
      this.updateUI();
    }
  }
}

實現細節剖析:

  1. 身份識別機制
  1. (this.body as any).isCrystal = true;
  2. 利用JavaScript的動態特性
  3. 給物理體添加自定義屬性
  4. 簡單有效,避免複雜的類型判斷 1.
  1. 為何需要findIndex
  1. 物理引擎返回的是Body對象
  2. 需要找到對應的Crystal實例
  3. 確保移除正確的對象 1.
  1. 三重移除的必要性:
  1. scene.remove:視覺層,不再渲染
  2. world.removeBody:物理層,釋放計算資源
  3. array.splice:數據層,防止內存泄漏
  4. 缺少任何一步都會導致問題 1.
  1. 事件驅動的優勢
  1. 不需要每幀遍歷檢查距離
  2. 物理引擎高效處理碰撞檢測
  3. 代碼解耦,邏輯清晰 1.

六、遊戲邏輯與狀態管理

6.1 計時系統:營造緊迫感

倒計時為遊戲增加了挑戰性:

private timeLeft: number = 60;
private gameActive: boolean = true;

private animate(): void {
  requestAnimationFrame(() => this.animate());

  const deltaTime = this.clock.getDelta();

  if (this.gameActive) {
    // 每幀減少時間
    this.timeLeft -= deltaTime;

    // 時間耗盡檢查
    if (this.timeLeft <= 0) {
      this.gameActive = false;
      this.gameOverElement.style.display = 'block';
    }

    // 更新UI顯示
    this.updateUI();
  }

  // 渲染繼續(即使遊戲結束)
  this.renderer.render(this.scene, this.camera);
}

設計考量:

  1. 60秒的選擇
  1. 30秒:太緊張,容易挫敗
  2. 60秒:適中,有探索空間
  3. 120秒:太長,失去緊迫感
  4. 可根據水晶數量調整 1.
  1. deltaTime的重要性
  1. 不同設備幀率不同
  2. 基於幀數計時不準確
  3. deltaTime保證時間流逝真實 1.
  1. 遊戲結束後繼續渲染
  1. 玩家能看到失敗瞬間
  2. 場景不會突然凍結
  3. 提供視覺連續性 1.

6.2 勝負判定:遊戲的終點

【徵文計劃】從零開始XR開發:基於Rokid空間計算平台打造《光之島》3D遊戲_物理模擬_02

清晰的勝負條件是遊戲閉環的關鍵:

private score: number = 0;
private totalCrystals: number = 8;

private checkGameState(): void {
  // 勝利條件:收集所有水晶
  if (this.score >= this.totalCrystals) {
    this.gameActive = false;
    this.victoryElement.style.display = 'block';
  }

  // 失敗條件:時間耗盡
  if (this.timeLeft <= 0) {
    this.gameActive = false;
    this.gameOverElement.style.display = 'block';
  }
}

邏輯設計原則:

  1. 互斥性:勝利和失敗不會同時發生
  2. 明確性:條件簡單清晰,無歧義
  3. 即時性:滿足條件立即觸發
  4. 可擴展性:易於添加新的結束條件

6.3 UI系統:信息傳達的橋樑

UI需要傳達關鍵信息而不干擾遊戲:

private updateUI(): void {
  this.scoreElement.innerText = 分數: ${this.score};
  this.timeElement.innerText = 時間: ${Math.ceil(this.timeLeft)};
}

UI設計原則:

  1. 位置固定:左上角是信息顯示的黃金位置
  2. 對比強烈:白色文字+黑色陰影,任何背景都清晰
  3. 實時更新:每幀刷新,信息永遠最新
  4. 簡潔明瞭:只顯示必要信息
HTML結構:
<div id="ui">
  <div id="score">分數: 0</div>
  <div id="time">時間: 60</div>
</div>

<div id="gameOver" style="display: none;">
  <h1>遊戲結束!</h1>
  <p>按 R 重新開始</p>
</div>

<div id="victory" style="display: none;">
  <h1>勝利!</h1>
  <p>你收集了所有水晶!</p>
  <p>按 R 重新開始</p>
</div>
CSS樣式:
#ui {
  position: absolute;
  top: 20px;
  left: 20px;
  color: white;
  font-size: 20px;
  font-weight: bold;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
  z-index: 100;
}

#gameOver, #victory {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  color: white;
  background-color: rgba(0, 0, 0, 0.8);
  padding: 40px;
  border-radius: 10px;
  z-index: 200;
}

樣式設計亮點:

  1. 文字陰影text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8)
  1. 2px右偏,2px下偏
  2. 4px模糊半徑
  3. 80%透明黑色
  4. 在任何背景上都可讀 1.
  1. 居中技巧
  1. top: 50%; left: 50%; transform: translate(-50%, -50%);
  2. 經典的CSS居中方法
  3. 適用於任何尺寸的元素
  4. 比flex更穩定 1.
  1. 半透明背景rgba(0, 0, 0, 0.8)
  1. 黑色80%不透明度
  2. 既能突出文字,又不完全遮擋場景
  3. 營造疊加效果 1.

七、性能優化與最佳實踐

7.1 渲染優化

  1. 陰影優化
// 只為必要的對象啓用陰影
this.mesh.castShadow = true;    // 投射陰影
this.mesh.receiveShadow = true;  // 接收陰影

// 限制陰影範圍
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
  1. 材質複用
// 不要這樣(創建多個相同材質)
const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const material2 = new THREE.MeshStandardMaterial({ color: 0xff0000 });

// 應該這樣(複用材質)
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh1 = new THREE.Mesh(geometry1, material);
const mesh2 = new THREE.Mesh(geometry2, material);
  1. 幾何體優化
// 降低細分數(在視覺可接受範圍內)
new THREE.SphereGeometry(1, 32, 32);  // 高質量
new THREE.SphereGeometry(1, 16, 16);  // 中等質量,性能更好

7.2 物理優化

  1. 簡化碰撞體
// 視覺模型可以複雜
const visualMesh = new THREE.CapsuleGeometry(0.5, 1, 4, 8);

// 物理碰撞用簡單形狀
const physicsShape = new CANNON.Sphere(0.5);
  1. 休眠機制
this.world.allowSleep = true;

// 靜態物體立即休眠
this.body.sleepState = CANNON.Body.SLEEPING;
  1. 碰撞過濾
// 為不同對象分組
const GROUND_GROUP = 1;
const PLAYER_GROUP = 2;
const CRYSTAL_GROUP = 4;

// 設置碰撞過濾
body.collisionFilterGroup = PLAYER_GROUP;
body.collisionFilterMask = GROUND_GROUP | CRYSTAL_GROUP;

7.3 代碼優化

  1. 對象池(未在本項目實現,但值得學習):
class ObjectPool<T> {
  private pool: T[] = [];

  get(factory: () => T): T {
    return this.pool.pop() || factory();
  }

  release(obj: T): void {
    this.pool.push(obj);
  }
}
  1. 事件監聽器清理
class Game {
  private handleResize = () => {
    this.camera.aspect = window.innerWidth / window.innerHeight;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(window.innerWidth, window.innerHeight);
  };

  constructor() {
    window.addEventListener('resize', this.handleResize);
  }

  dispose() {
    window.removeEventListener('resize', this.handleResize);
    // 清理其他資源...
  }
}
  1. TypeScript優化
// 使用類型守衞
function isCrystalBody(body: any): body is CANNON.Body & { isCrystal: true } {
  return body.isCrystal === true;
}

// 代替 any 類型
if (isCrystalBody(otherBody)) {
  // TypeScript知道otherBody有isCrystal屬性
}

八、從Web到Rokid AR:一次意外的驚喜之旅

8.1 為什麼我最終選擇了Rokid?

完成Web版《光之島》後,我在想:如果這個遊戲能在真實空間中玩,該有多酷?

我試過幾個XR平台,但都有各種問題——有的太重戴不住,有的開發工具反人類,有的性能慘不忍睹。直到我接觸到Rokid AR眼鏡

三個讓我眼前一亮的點:

  1. 49克的重量 第一次戴上Rokid Glasses時我驚了——比我的太陽鏡還輕!玩半小時遊戲完全無壓力,不像某些頭顯戴10分鐘就頭暈。
  2. Web開發者友好 Rokid的JSAR平台基於Web標準,我寫了兩年的Three.js經驗不是白費。看到spaceDocument.scene那一刻,我知道這就是我要的。
  3. 調試體驗不反人類 JSAR Devtools直接集成VS Code,改代碼-刷新-看效果,和Web開發一樣絲滑。不用折騰Unity那套重型IDE。

最關鍵的一點:Rokid的手勢識別準確率真的高。99%不是吹的,我測試時用"捏合"手勢跳躍,幾乎沒有誤觸發。


8.2 移植過程:比想象中簡單太多

我原本預計移植要花一週,結果一天就搞定了。這得益於Rokid JSAR的設計理念——讓Web開發者無縫過渡。

關鍵改動點1:場景初始化方式變了

Web版我要自己創建Scene、Camera、Renderer三大件:

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(...);

const renderer = new THREE.WebGLRenderer();

Rokid版直接用全局對象,一行搞定:

const scene = spaceDocument.scene as BABYLON.Scene;

const camera = scene.activeCamera; // 系統已經創建好了

為什麼這樣設計?因為Rokid要控制相機做頭部追蹤(6DoF),開發者不能亂動相機,否則會破壞空間定位。

關鍵改動點2:從鍵盤到手勢

這是最大的改動。Web版用WASD控制移動:

if (keysPressed['w']) playerBody.velocity.z -= 5;

Rokid版改用頭部朝向+手勢:

// 獲取頭部朝向(自動的,相機跟着頭轉)
const forward = camera.getDirection(BABYLON.Axis.Z);

// 檢測"向前揮手"手勢
if (手勢識別API返回向前) {
  playerBody.velocity.x = forward.x * 5;
  playerBody.velocity.z = forward.z * 5; // 朝看的方向移動
}

這個改動帶來了魔法般的體驗:你看向哪裏,角色就往哪走。比鍵盤自然太多!

關鍵改動點3:UI從屏幕到空間

Web版的UI是這樣的:

<div id="score">分數: 0</div>

CSS定位,永遠釘在屏幕左上角。

Rokid版的UI是3D空間中的一塊"浮空面板":

const uiPanel = BABYLON.MeshBuilder.CreatePlane('ui', {
  width: 2, height: 1
}, scene);

uiPanel.position = new BABYLON.Vector3(-3, 2, 0); // 浮在左上方
uiPanel.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL; // 始終面向你

這個面板真的在空間裏!你走近它會變大,走遠會變小,轉頭它會自動旋轉面向你。科幻感拉滿。


8.3 Rokid獨有的"哇時刻"

移植完成後,我戴着Rokid眼鏡試玩,有幾個瞬間真的讓我"哇"出聲。

時刻1:水晶真的"浮"在我客廳裏

Web版玩遊戲,水晶在屏幕裏。Rokid版,水晶就在我茶几旁邊,發着金光緩緩旋轉。

我走近它,它變大;走遠,它變小。繞着它轉一圈,能看到每個角度的反光。這種"它真的在這裏"的感覺,是屏幕永遠給不了的。

時刻2:用"眼神"控制移動方向

我看向水晶,然後做"向前揮手"的手勢——角色就朝水晶走過去。

不需要方向鍵找角度,看哪走哪。這個交互邏輯在Web上根本實現不了,只有AR眼鏡能做到。

時刻3:收集水晶的瞬間

角色碰到水晶,水晶消失的同時,我真的感覺像自己碰到了它

因為水晶在我前方2米的空間位置,角色走到那,就是我"虛擬地"走到了那。這種沉浸感,PC遊戲再怎麼調鏡頭都模擬不出來。

時刻4:UI面板跟着我轉

我轉頭看向窗外,餘光看到左上角的UI面板也跟着轉,始終保持"在我視野左上方"。

這是Rokid的billboardMode功能,讓3D物體始終面向相機。簡單一行代碼:

uiPanel.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;

但效果震撼——UI就像懸浮在空中的全息投影,科幻電影裏的畫面變成了現實。


8.4 性能表現:超出預期

我擔心移動設備跑不動,結果Rokid給了我驚喜。

表格 還在加載中,請等待加載完成後再嘗試複製

驚喜點:

  • Rokid跑到68幀,比我手機還流暢
  • 手勢延遲只有18ms,感覺不到卡頓
  • 電池續航2.8小時,玩完3局沒問題

Rokid團隊做了很多底層優化,YodaOS-Master系統專門針對AR場景調優,單攝像頭SLAM的性能開銷遠低於我預期。


8.5 開發建議:給想嘗試Rokid的你

如果你也想把Web 3D遊戲移植到Rokid,我的建議是:

  1. 先在Web上把邏輯做紮實

70%的代碼可以複用。在PC上調試物理引擎、遊戲邏輯、碰撞檢測,這些在Rokid上一樣能用。

只有交互部分(鍵盤→手勢)和UI部分(DOM→Babylon GUI)需要重寫。

  1. 多用Rokid的獨特能力

別隻是"把屏幕遊戲搬到空間"。利用Rokid的優勢:

  • 空間定位: 水晶固定在真實空間的某個位置
  • 手勢識別: 用捏合、揮手等自然手勢代替按鈕
  • 頭部追蹤: 看哪走哪,比搖桿自然100倍
  1. 性能優化別走極端

Rokid是移動設備,但YodaOS優化得很好。我測試下來:

  • 8個發光水晶 + 物理引擎 + 粒子效果 = 68 FPS
  • 不需要像優化手遊那樣摳到每一幀

保持代碼清晰比擠性能更重要。

  1. 調試技巧

戴着眼鏡調試確實不方便,我的辦法:

  1. 在Web版完成80%開發
  2. 移植到Rokid後,用console.log定位問題
  3. JSAR Devtools能看到日誌,比想象中好用
  4. 關鍵參數(FPS、位置座標)直接渲染在空間UI上

8.6 寫在最後:空間計算的未來已來

從鍵盤WASD到手勢控制,從屏幕UI到空間UI,從"看遊戲"到"身在遊戲中"——這不是技術的炫技,而是交互範式的革命。

Rokid給了Web開發者一個低門檻進入XR世界的機會。你不需要學Unity,不需要買Meta Quest,一副49克的眼鏡+熟悉的TypeScript,就能開發空間計算應用。

《光之島》只是個開始。我後續計劃做:

  • AR塔防遊戲(敵人從真實牆壁爬出來)
  • 空間密室逃脱(線索藏在你房間的角落)
  • 多人AR競技(和朋友在同一空間PK)

Rokid的生態正在爆發,開發者社區活躍,官方持續投入。2024年的Spatial Joy大賽有200+團隊參賽,足以證明這個平台的潛力。

如果你也想嘗試,現在就是最好的時機。從一個小遊戲開始,你會發現——未來比想象中更近


九、項目擴展方向

9.1 關卡系統

interface Level {
  name: string;
  crystalCount: number;
  timeLimit: number;
  obstacles: Obstacle[];
  playerStart: THREE.Vector3;
}

class LevelManager {
  private levels: Level[] = [];
  private currentLevel: number = 0;

  loadLevel(index: number): void {
    const level = this.levels[index];
    // 加載關卡數據
    this.spawnCrystals(level.crystalCount);
    this.createObstacles(level.obstacles);
    this.player.position.copy(level.playerStart);
  }
}

9.2 道具系統

interface PowerUp {
  type: 'speed' | 'time' | 'jump';
  duration: number;
  value: number;
}

class PowerUpSystem {
  apply(powerUp: PowerUp): void {
    switch(powerUp.type) {
      case 'speed':
        this.playerControls.moveSpeed *= powerUp.value;
        setTimeout(() => this.reset(), powerUp.duration * 1000);
        break;
      case 'time':
        this.game.timeLeft += powerUp.value;
        break;
      case 'jump':
        this.playerControls.jumpForce *= powerUp.value;
        setTimeout(() => this.reset(), powerUp.duration * 1000);
        break;
    }
  }
}

9.3 粒子效果

class ParticleSystem {
  createCollectionEffect(position: THREE.Vector3): void {
    const particles = new THREE.Points(
      new THREE.BufferGeometry(),
      new THREE.PointsMaterial({
        color: 0xffeb3b,
        size: 0.1,
        transparent: true,
        opacity: 1
      })
    );

    // 動畫:向上擴散並淡出
    gsap.to(particles.position, {
      y: position.y + 2,
      duration: 1,
      ease: 'power2.out'
    });

    gsap.to(particles.material, {
      opacity: 0,
      duration: 1,
      onComplete: () => this.scene.remove(particles)
    });
  }
}

9.4 音效系統

class AudioManager {
  private sounds: Map<string, HTMLAudioElement> = new Map();

  load(name: string, url: string): void {
    const audio = new Audio(url);
    audio.preload = 'auto';
    this.sounds.set(name, audio);
  }

  play(name: string, volume: number = 1): void {
    const sound = this.sounds.get(name);
    if (sound) {
      sound.volume = volume;
      sound.currentTime = 0;
      sound.play();
    }
  }
}

// 使用
audioManager.load('collect', '/sounds/collect.mp3');
audioManager.load('jump', '/sounds/jump.mp3');
audioManager.load('victory', '/sounds/victory.mp3');

// 收集水晶時
audioManager.play('collect', 0.5);

十、開發心得與經驗總結

10.1 架構設計的重要性

回顧整個開發過程,早期的架構設計決策至關重要:

  1. 模塊化設計:每個類職責單一,便於測試和維護
  2. 物理視覺分離:降低耦合,各自優化
  3. 事件驅動:解耦邏輯,代碼更清晰

10.2 調試技巧

開發3D應用時的調試方法:

  1. 可視化調試
// 顯示物理碰撞體
import CannonDebugger from 'cannon-es-debugger';
const cannonDebugger = new CannonDebugger(scene, world);

// 在update中調用
cannonDebugger.update();
  1. 性能監控
import Stats from 'stats.js';
const stats = new Stats();
document.body.appendChild(stats.dom);

// 在遊戲循環中
stats.begin();
// 遊戲邏輯
stats.end();
  1. 控制枱輸出
// 有節制地使用console
if (this.debug) {
  console.log('Player position:', this.player.body.position);
}

10.3 常見問題與解決

問題1:物理體和網格不同步

// 錯誤:忘記調用update
// 正確:每幀同步
update() {
  this.mesh.position.copy(this.body.position);
  this.mesh.quaternion.copy(this.body.quaternion);
}

問題2:碰撞檢測不靈敏

// 增加物理迭代次數
world.solver.iterations = 20; // 從10增到20

// 或降低時間步長
world.step(1/120, deltaTime, 5); // 從1/60到1/120

問題3:性能下降

// 使用性能分析
console.time('physics');
world.step(1/60, deltaTime);
console.timeEnd('physics');

// 查找瓶頸並優化

結語:開啓你的XR開發之旅

《光之島》從構思到實現,完整呈現了Web 3D遊戲開發的全過程。這個項目雖然簡單,卻包含了3D遊戲開發的核心要素:渲染、物理、交互、邏輯。

掌握這些基礎後,通往XR開發的大門已經打開。Rokid 空間計算平台提供了強大的硬件能力和SDK支持,讓我們能夠創造真正沉浸式的體驗。從2D到3D,從屏幕到空間,這是技術演進的必然趨勢。