一場關於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 項目結構設計
優秀的項目結構是可維護性的基石。我採用了職責分離的設計原則:
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();
}
}
關鍵設計點解析:
- 相機參數的選擇:FOV(視野角度)75度是個甜蜜點,太小會產生望遠鏡效果,太大會畸變。近裁剪面0.1,遠裁剪面1000,這個範圍足夠覆蓋我們的場景。
- 渲染器配置:
antialias: true開啓抗鋸齒,雖然消耗性能,但畫面質量提升明顯。shadowMap.enabled啓用陰影系統,讓場景更有立體感。 - 時鐘系統: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;
}
}
物理參數深度解析:
- 重力值9.82:標準地球重力,如果做月球遊戲可以調整為1.62,創造不同的跳躍手感
- Broadphase選擇:
- NaiveBroadphase:O(n²)複雜度,物體少時效率高
- SAPBroadphase:掃描排序,物體多時更優
- 我們的遊戲物體少,選擇Naive即可
- Solver迭代次數:
- 越高越精確,但計算量越大
- 10次是經驗值,平衡了精度和性能
- 快速運動的物體可能需要提高這個值 1.
- Sleep機制:
- 靜止物體不參與物理計算
- 大幅降低CPU佔用
- 玩家觸碰時會自動喚醒 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);
}
執行順序的重要性:
這個順序是經過深思熟慮的:
- 先更新時間,確保計時準確
- 物理模擬必須在輸入處理後,才能響應玩家操作
- 視覺同步在物理計算後,保證看到的就是真實的
- 攝像機更新在物體更新後,才能正確跟隨
- 狀態檢查在所有更新後,判斷準確
- UI更新在狀態檢查後,顯示最新信息
- 渲染必須在最後,呈現完整的一幀
固定時間步長的意義:
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);
}
}
設計細節:
- 為什麼是50x50:足夠大的平面讓玩家不會輕易掉出邊界,又不會太大導致迷失方向
- 材質參數調優:
- 天藍色(0x4a9eff)與"光之島"的主題契合
- roughness 0.8讓表面有質感,不是鏡面反射
- metalness 0.2保持一點反光,增加視覺趣味
- 物理平面特性:
- Cannon.Plane是無限大的,不用擔心邊界
- mass=0的物體完全靜止,碰撞時隻影響動態物體
- 四元數旋轉比歐拉角更穩定,避免萬向鎖問題
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.
- 質量設為5:
- 太輕(<1):容易被碰撞彈飛
- 太重(>10):跳躍需要很大的力
- 5是經過測試的最佳值 1.
- 線性阻尼0.9:
- 0表示無阻力,在真空中永動
- 1表示瞬間停止
- 0.9讓角色有慣性又能快速停下 1.
- 初始位置(0,3,0):
- Y=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.
- 金黃色+自發光:
- 黃色代表價值和獎勵(遊戲設計通用規律)
- 自發光讓水晶在遠處也清晰可見
- emissiveIntensity 0.5不會太刺眼 1.
- 材質參數組合:
- 低粗糙度(0.3) + 高金屬度(0.8) = 寶石質感
- 反射環境光,產生璀璨效果
- 與場景中其他物體形成鮮明對比 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;
}
}
}
操作手感優化技巧:
- 移動速度調校(5):
- 速度1-3:太慢,玩家急躁
- 速度5-7:舒適區間
- 速度10+:太快,失控感
- 最終選5,經過多次試玩調整 1.
- 跳躍力度設計(8):
- 與重力9.82配合
- 跳躍高度約3.2單位(v²/2g = 64/19.64)
- 滯空時間約1.6秒(2v/g)
- 足夠跨越障礙,又不會太飄 1.
- 地面檢測的智慧:
- if (contact.ni.y > 0.5 || contact.nj.y > 0.5)
- ni和nj是碰撞法線的兩個方向
- Y > 0.5意味着法線向上(cos45° ≈ 0.7)
- 允許在斜坡上跳躍(不超過45度)
- 防止在牆壁上跳躍 1.
- 直接設置速度 vs 施加力:
- 施加力:更真實,但響應慢,有慣性
- 直接設速度:立即響應,操作精確
- 遊戲偏向後者,犧牲真實換操作性
- 保留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);
}
攝像機設計的藝術:
- 偏移向量(0, 5, 10):
- Y=5:俯視角度,觀察地面和障礙
- Z=10:距離適中,不太近不太遠
- X=0:居中,左右視野對稱 1.
- Lerp插值係數0.1:
- 0.01:太慢,攝像機落後明顯
- 0.1:恰好,輕微延遲感
- 0.5:太快,幾乎剛性跟隨
- 插值產生自然的"彈性"效果 1.
- lookAt的作用:
- 自動計算攝像機旋轉
- 無論玩家移動到哪裏,始終在視野中心
- 比手動計算歐拉角簡單可靠 1.
- 高級技巧(本項目未實現):
- 碰撞檢測:防止攝像機穿牆
- 動態距離:根據速度調整遠近
- 視角平滑:鼠標控制環視 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();
}
}
}
實現細節剖析:
- 身份識別機制:
- (this.body as any).isCrystal = true;
- 利用JavaScript的動態特性
- 給物理體添加自定義屬性
- 簡單有效,避免複雜的類型判斷 1.
- 為何需要findIndex:
- 物理引擎返回的是Body對象
- 需要找到對應的Crystal實例
- 確保移除正確的對象 1.
- 三重移除的必要性:
scene.remove:視覺層,不再渲染world.removeBody:物理層,釋放計算資源array.splice:數據層,防止內存泄漏- 缺少任何一步都會導致問題 1.
- 事件驅動的優勢:
- 不需要每幀遍歷檢查距離
- 物理引擎高效處理碰撞檢測
- 代碼解耦,邏輯清晰 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);
}
設計考量:
- 60秒的選擇:
- 30秒:太緊張,容易挫敗
- 60秒:適中,有探索空間
- 120秒:太長,失去緊迫感
- 可根據水晶數量調整 1.
- deltaTime的重要性:
- 不同設備幀率不同
- 基於幀數計時不準確
- deltaTime保證時間流逝真實 1.
- 遊戲結束後繼續渲染:
- 玩家能看到失敗瞬間
- 場景不會突然凍結
- 提供視覺連續性 1.
6.2 勝負判定:遊戲的終點
清晰的勝負條件是遊戲閉環的關鍵:
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';
}
}
邏輯設計原則:
- 互斥性:勝利和失敗不會同時發生
- 明確性:條件簡單清晰,無歧義
- 即時性:滿足條件立即觸發
- 可擴展性:易於添加新的結束條件
6.3 UI系統:信息傳達的橋樑
UI需要傳達關鍵信息而不干擾遊戲:
private updateUI(): void {
this.scoreElement.innerText = 分數: ${this.score};
this.timeElement.innerText = 時間: ${Math.ceil(this.timeLeft)};
}
UI設計原則:
- 位置固定:左上角是信息顯示的黃金位置
- 對比強烈:白色文字+黑色陰影,任何背景都清晰
- 實時更新:每幀刷新,信息永遠最新
- 簡潔明瞭:只顯示必要信息
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;
}
樣式設計亮點:
- 文字陰影:
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8)
- 2px右偏,2px下偏
- 4px模糊半徑
- 80%透明黑色
- 在任何背景上都可讀 1.
- 居中技巧:
- top: 50%; left: 50%; transform: translate(-50%, -50%);
- 經典的CSS居中方法
- 適用於任何尺寸的元素
- 比flex更穩定 1.
- 半透明背景:
rgba(0, 0, 0, 0.8)
- 黑色80%不透明度
- 既能突出文字,又不完全遮擋場景
- 營造疊加效果 1.
七、性能優化與最佳實踐
7.1 渲染優化
- 陰影優化:
// 只為必要的對象啓用陰影
this.mesh.castShadow = true; // 投射陰影
this.mesh.receiveShadow = true; // 接收陰影
// 限制陰影範圍
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
- 材質複用:
// 不要這樣(創建多個相同材質)
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);
- 幾何體優化:
// 降低細分數(在視覺可接受範圍內)
new THREE.SphereGeometry(1, 32, 32); // 高質量
new THREE.SphereGeometry(1, 16, 16); // 中等質量,性能更好
7.2 物理優化
- 簡化碰撞體:
// 視覺模型可以複雜
const visualMesh = new THREE.CapsuleGeometry(0.5, 1, 4, 8);
// 物理碰撞用簡單形狀
const physicsShape = new CANNON.Sphere(0.5);
- 休眠機制:
this.world.allowSleep = true;
// 靜態物體立即休眠
this.body.sleepState = CANNON.Body.SLEEPING;
- 碰撞過濾:
// 為不同對象分組
const GROUND_GROUP = 1;
const PLAYER_GROUP = 2;
const CRYSTAL_GROUP = 4;
// 設置碰撞過濾
body.collisionFilterGroup = PLAYER_GROUP;
body.collisionFilterMask = GROUND_GROUP | CRYSTAL_GROUP;
7.3 代碼優化
- 對象池(未在本項目實現,但值得學習):
class ObjectPool<T> {
private pool: T[] = [];
get(factory: () => T): T {
return this.pool.pop() || factory();
}
release(obj: T): void {
this.pool.push(obj);
}
}
- 事件監聽器清理:
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);
// 清理其他資源...
}
}
- 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眼鏡。
三個讓我眼前一亮的點:
- 49克的重量 第一次戴上Rokid Glasses時我驚了——比我的太陽鏡還輕!玩半小時遊戲完全無壓力,不像某些頭顯戴10分鐘就頭暈。
- Web開發者友好 Rokid的JSAR平台基於Web標準,我寫了兩年的Three.js經驗不是白費。看到
spaceDocument.scene那一刻,我知道這就是我要的。 - 調試體驗不反人類 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,我的建議是:
- 先在Web上把邏輯做紮實
70%的代碼可以複用。在PC上調試物理引擎、遊戲邏輯、碰撞檢測,這些在Rokid上一樣能用。
只有交互部分(鍵盤→手勢)和UI部分(DOM→Babylon GUI)需要重寫。
- 多用Rokid的獨特能力
別隻是"把屏幕遊戲搬到空間"。利用Rokid的優勢:
- 空間定位: 水晶固定在真實空間的某個位置
- 手勢識別: 用捏合、揮手等自然手勢代替按鈕
- 頭部追蹤: 看哪走哪,比搖桿自然100倍
- 性能優化別走極端
Rokid是移動設備,但YodaOS優化得很好。我測試下來:
- 8個發光水晶 + 物理引擎 + 粒子效果 = 68 FPS
- 不需要像優化手遊那樣摳到每一幀
保持代碼清晰比擠性能更重要。
- 調試技巧
戴着眼鏡調試確實不方便,我的辦法:
- 在Web版完成80%開發
- 移植到Rokid後,用
console.log定位問題 - JSAR Devtools能看到日誌,比想象中好用
- 關鍵參數(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 架構設計的重要性
回顧整個開發過程,早期的架構設計決策至關重要:
- 模塊化設計:每個類職責單一,便於測試和維護
- 物理視覺分離:降低耦合,各自優化
- 事件驅動:解耦邏輯,代碼更清晰
10.2 調試技巧
開發3D應用時的調試方法:
- 可視化調試:
// 顯示物理碰撞體
import CannonDebugger from 'cannon-es-debugger';
const cannonDebugger = new CannonDebugger(scene, world);
// 在update中調用
cannonDebugger.update();
- 性能監控:
import Stats from 'stats.js';
const stats = new Stats();
document.body.appendChild(stats.dom);
// 在遊戲循環中
stats.begin();
// 遊戲邏輯
stats.end();
- 控制枱輸出:
// 有節制地使用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,從屏幕到空間,這是技術演進的必然趨勢。