在現代遊戲開發中 NPC(非玩家角色)互動是提升遊戲沉浸感的重要元素之一。當玩家靠近 NPC 時,通常會觸發對話或任務提示,這種互動機制能讓遊戲世界更加生動。本文將介紹如何利用 HTML 和 VIVERSE EXTENSION 的 TriggerAndAction 功能,快速實現 NPC 對話面板效果,並提供完整操作流程與範例程式碼,讓開發者可以在 VIVERSE WORLD 中輕鬆製作類似功能。
我們將示範的 NPC 對話效果包括:當玩家進入觸發區域時彈出對話框、玩家點擊滑鼠可進行逐句對話,並支援角色頭像與文字顯示。無論是新手還是有經驗的遊戲開發者,都能透過本文的教學快速掌握 NPC 互動面板的實作技巧。
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251109212442_0_e7359a-1200x630.jpg)
效果展示
- 玩家觸發 Trigger Box 後跳出對話框
- 點擊滑鼠可進行對話直到播放完畢
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251109212731_0_909ab4-1200x630.jpg)
概念說明
- 顯示機制
- 對話面板透過
this.app.graphicsDevice.canvas.parentNode建立 - 即使遊戲切換至全螢幕模式,面板仍能正常顯示與運作
- 對話面板透過
- 使用方式
- 在場景中放置一個空的物件(Empty Entity)
- 為該物件附加此 Script
- 直接於屬性面板中設定以下參數:
- 對話組數
- 角色圖像
- 角色名稱
- 對話內容
- 開發優點
- 無需額外撰寫 UI 相關程式碼
- 設計師可直接透過編輯器完成對話配置
- 系統可重複使用,方便在不同場景或專案中套用
- 未來擴充
- 可進一步將任務提示(Quest Hint)設計為 ICON 形式的側邊面板
- 用以紀錄並顯示任務進度或提示資訊(此部分將於後續文章補充)
- 無法重複觸發播放對話
- 思考增加對話的複雜度可能性
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110030415_0_e2ae0d-1200x630.jpg)
流程操作
VIVERSE EXTENSION
本操作需要使用 VIVERSE EXTENSION 的功能,並於 VIVERSE WORLD 使用,因此需要先下載 VIVERSE EXTENSION。請參考教學〈為瀏覽器安裝 VIVERSE EXTENSION〉。
ASSETS
- 上傳 ICON
- player.png
- NPC1.png
- NPC2.png
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110031039_0_8f9c11-1200x630.jpg)
- 新增檔案
- DialogManager.js
- npc.js
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110035000_0_233720-1200x630.jpg)
SCRIPTS
將剛剛新增的 DialogManager.js、npc.js 分別貼入以下的程式碼
var DialogManager = pc.createScript('dialogManager');
// 靜態單例
DialogManager.instance = null;
// Inspector 可設定角色圖片
DialogManager.attributes.add('characterImages', {
type: 'json',
title: '角色圖片',
description: '設定角色名稱與對應圖片',
schema: [
{ name: 'name', type: 'string', title: '角色名稱' },
{ name: 'image', type: 'asset', assetType: 'texture', title: '頭像圖片' }
],
array: true,
default: [
{ name: 'NPC', image: null },
{ name: 'Player', image: null }
]
});
// 初始化
DialogManager.prototype.initialize = function () {
DialogManager.instance = this;
// 建立對話框 Overlay
this.overlay = document.createElement('div');
Object.assign(this.overlay.style, {
position: 'fixed',
bottom: '15%',
left: '50%',
transform: 'translateX(-50%)',
minWidth: '60%',
minHeight: '20%',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
color: 'white',
display: 'none',
flexDirection: 'row',
alignItems: 'center',
padding: '16px 24px',
fontSize: '18px',
zIndex: '9999',
fontFamily: 'sans-serif',
borderRadius: '12px',
boxShadow: '0 0 12px rgba(0,0,0,0.5)',
transition: 'opacity 0.3s ease',
opacity: '0'
});
// 頭像區
this.imageContainer = document.createElement('div');
Object.assign(this.imageContainer.style, {
width: '100px',
height: '100px',
flexShrink: '0',
marginRight: '16px',
borderRadius: '8px',
overflow: 'hidden',
backgroundColor: 'rgba(255,255,255,0.1)'
});
this.imageElement = document.createElement('img');
Object.assign(this.imageElement.style, {
width: '100%',
height: '100%',
objectFit: 'cover'
});
this.imageContainer.appendChild(this.imageElement);
this.overlay.appendChild(this.imageContainer);
// 文字區
this.textContainer = document.createElement('div');
Object.assign(this.textContainer.style, {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flex: '1'
});
this.nameElement = document.createElement('p');
Object.assign(this.nameElement.style, {
fontWeight: 'bold',
fontSize: '20px',
margin: '0 0 6px 0',
color: '#FFD700'
});
this.textContainer.appendChild(this.nameElement);
this.textElement = document.createElement('p');
Object.assign(this.textElement.style, {
margin: '0',
lineHeight: '1.4em',
fontSize: '18px'
});
this.textContainer.appendChild(this.textElement);
this.overlay.appendChild(this.textContainer);
document.body.appendChild(this.overlay);
// 屬性
this.typingSpeed = 40;
this.isTyping = false;
this.dialogQueue = [];
this.currentDialog = 0;
// 點擊控制
this._onClick = () => {
if (!this.dialogQueue.length) return;
if (this.isTyping) {
this.textElement.textContent = this.fullText;
this.isTyping = false;
} else {
this.showNextDialog();
}
};
document.addEventListener('click', this._onClick);
// 廣播初始化完成事件
this.app.fire('dialogmanager:ready');
// 點擊提示文字
this.clickHint = document.createElement('p');
Object.assign(this.clickHint.style, {
position: 'absolute',
bottom: '8px',
left: '50%',
transform: 'translateX(-50%)',
fontSize: '14px',
color: '#FFFFFF',
opacity: '0.7',
margin: '0',
pointerEvents: 'none',
fontFamily: 'sans-serif'
});
this.clickHint.textContent = '(請點擊滑鼠繼續)';
this.overlay.appendChild(this.clickHint);
};
// 開始對話
DialogManager.prototype.startDialog = function(dialogArray, onComplete) {
if (!dialogArray || !dialogArray.length) return;
this.dialogQueue = dialogArray;
this.currentDialog = 0;
this.onComplete = onComplete || null;
this.isTyping = false;
this.showNextDialog();
};
// 顯示單句對話
DialogManager.prototype.showDialog = function (name, text, imageAsset) {
// 左右角色切換
if (name.toLowerCase() === '玩家') {
this.overlay.style.flexDirection = 'row-reverse';
this.imageContainer.style.marginRight = '0';
this.imageContainer.style.marginLeft = '16px';
this.nameElement.style.textAlign = 'right';
this.textElement.style.textAlign = 'right';
} else {
this.overlay.style.flexDirection = 'row';
this.imageContainer.style.marginRight = '16px';
this.imageContainer.style.marginLeft = '0';
this.nameElement.style.textAlign = 'left';
this.textElement.style.textAlign = 'left';
}
// 顯示對話框(淡入)
this.overlay.style.display = 'flex';
setTimeout(() => (this.overlay.style.opacity = '1'), 10);
this.nameElement.textContent = name;
this.textElement.textContent = '';
this.fullText = text;
// 設定頭像圖片
if (imageAsset && imageAsset.resource) {
this.imageElement.src = imageAsset.getFileUrl();
this.imageContainer.style.display = 'block';
} else {
const found = this.characterImages.find((c) => c.name === name);
if (found && found.image && found.image.resource) {
this.imageElement.src = found.image.getFileUrl();
this.imageContainer.style.display = 'block';
} else {
this.imageContainer.style.display = 'none';
}
}
// 打字效果
this.isTyping = true;
let i = 0;
const chars = text.split('');
const typeNext = () => {
if (i < chars.length) {
this.textElement.textContent += chars[i];
i++;
setTimeout(typeNext, this.typingSpeed);
} else {
this.isTyping = false;
}
};
typeNext();
};
// 顯示下一句
DialogManager.prototype.showNextDialog = function () {
if (this.currentDialog < this.dialogQueue.length) {
const d = this.dialogQueue[this.currentDialog];
this.showDialog(d.name, d.text, d.image);
this.currentDialog++;
} else {
// 對話結束 — 只淡出動畫
this.overlay.style.opacity = '0';
}
};var Npc = pc.createScript('npc');
// Editor 屬性:對話列表
Npc.attributes.add('dialogLines', {
type: 'json',
array: true,
title: '對話列表',
schema: [
{ name: 'image', type: 'asset', assetType: 'texture', title: '角色圖片(可選)' },
{ name: 'name', type: 'string', default: 'NPC', title: '角色名稱' },
{ name: 'text', type: 'string', default: '你好!', title: '對話文字' }
],
default: [
{ image: null, name: 'NPC', text: '你好,旅行者!' },
{ image: null, name: 'Player', text: '你好,請問這裡是哪裡?' },
{ image: null, name: 'NPC', text: '這裡是新手村。' }
]
});
// 初始化
Npc.prototype.initialize = function () {
// 等待 DialogManager 初始化完成再啟動對話
this.app.once('dialogmanager:ready', this.startDialog, this);
// 若 DialogManager 已存在,立即啟動對話
if (DialogManager.instance) {
this.startDialog();
}
};
// 開始對話
Npc.prototype.startDialog = function () {
if (!this.dialogLines || !this.dialogLines.length) return;
// 將對話列表轉成 DialogManager 可用格式
const dialogQueue = this.dialogLines.map(line => ({
image: line.image,
name: line.name,
text: line.text
}));
if (DialogManager.instance) {
DialogManager.instance.startDialog(dialogQueue, () => {
// 對話結束自動停用 NPC
this.entity.enabled = false;
});
} else {
console.warn('DialogManager not found yet!');
}
};
HIERACHY
新增物件與對應階層如下,底下有每個物件詳細的設定參數。
Root
├─ DialogManager
├─ Plane
├─ NPC1
│ └─ Talking
├─ NPC2
│ └─ Talking- Type: Plane
- Collision: ✅
- Rigidbody: ✅
- Type: Static
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110035345_0_65e3e6-1200x630.jpg)
- Type: Box
- Enabled: ✅
- Name: NPC1
- Collison: ✅
- VIVERSE EXTENSION:
- Select Pluggins: TriggerAndAction
- Triggers: +
- Selected module: Trigger [+]
- type: Trigger 1.EntitySubscribeTriggerEnter
- tags to filter: local-player
- Action EntityEnableById: Talking
- type: Trigger 1.EntitySubscribeTriggerEnter
- Selected module: Trigger [+]
- Triggers: +
- Select Pluggins: TriggerAndAction
- Type: None
- Enabled: ❎
- Name: Talking
- Scripts: npc.js
- Parse
- 輸入對話的組數
- 將物件與對話填入
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110035050_0_18ee68-1200x630.jpg)
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110035021_0_4aa893-1200x630.jpg)
- Type: Box
- Enabled: ✅
- Name: NPC2
- Collison: ✅
- VIVERSE EXTENSION:
- Select Pluggins: TriggerAndAction
- Triggers: +
- Selected module: Trigger [+]
- type: Trigger 1.EntitySubscribeTriggerEnter
- tags to filter: local-player
- Action EntityEnableById: Talking
- type: Trigger 1.EntitySubscribeTriggerEnter
- Selected module: Trigger [+]
- Triggers: +
- Select Pluggins: TriggerAndAction
- Type: None
- Enabled: ❎
- Name: Talking
- Scripts: npc.js
- Parse
- 輸入對話的組數
- 將物件與對話填入
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110035050_0_18ee68-1200x630.jpg)
![[PlayCanvas + VIVERSE] 一次性 NPC 對話框 [PlayCanvas + VIVERSE] 一次性 NPC 對話框](https://img.jfsblog.com/2025/11/20251110035021_0_4aa893-1200x630.jpg)
使用素材
- People icons created by Creartive – Flaticon
- Skeleton icons created by Creartive – Flaticon
- People icons created by Creartive – Flaticon
相關文章
PlayCanvas & Viverse World
| 類型 | 文章 |
|---|---|
| 相關文件 | Viverse Docs.PlayCanvas Docs |
| 基礎操作 | Viverse World.Claude Playcanvas Editor MCP Server.Viverse PlayCanvas Extension (安裝.觸發.媒體.撿拾.坐下) |
| 開發者工具 | 登入與身分驗證.排行榜 |
| 開發筆記 | 滿版畫面.場景切換.密碼面板.模組化迷宮.科技感效果.對話框.碰撞計分.碰撞計分+遊戲重生.子彈閃避 |



![[PlayCanvas] Claude Desktop 的 Playcanvas Editor MCP Server 設定教學 [PlayCanvas] Claude Desktop 的 Playcanvas Editor MCP Server 設定教學](https://i1.wp.com/img.jfsblog.com/2025/10/20251103022428_0_f74320.jpg?quality=90&zoom=2&ssl=1&resize=350%2C233)






