昨天已經能正確判斷「點到的是不是彩蛋」,但用的是 alert() 來跳出資訊
介面非常陽春,而且每點一次彩蛋就跳一次,照理說出現過後就代表一切結束,不應該再讓使用者繼續點擊跟跳出找到資訊。今天就來處理以下兩件事:
userData 技巧昨天用 characterArmature.userData.isEgg = true 貼了一個「這是彩蛋」的標籤。今天用同樣的手法,再貼一個「有沒有被找到過」的標籤:
let characterArmature; // ★ 提升到外層,點擊事件才拿得到
loader.load('blender_output.glb', (gltf) => {
scene.add(gltf.scene);
characterArmature = gltf.scene.getObjectByName('CharacterArmature');
if (characterArmature) {
characterArmature.userData.isEgg = true;
characterArmature.userData.found = false; // ★ 新增:一開始還沒被找到
}
// ...(Box3、相機定位、controls.target 都不動)
});
跟昨天不一樣的地方:characterArmature 這個變數要搬到 loader.load 外面宣告,因為點擊事件(在外層)要能修改它的 userData.found,不能只留在 callback 裡面用完就丟。
昨天寫的 isPartOfEgg(hit) 只回傳 true/false,今天需要的是「找到的話,把那個節點本身傳回來」,才能去改它的 userData.found:
function findEggRoot(object) {
if (object.userData.isEgg) return object;
let root = null;
object.traverseAncestors((ancestor) => {
if (ancestor.userData.isEgg) root = ancestor;
});
return root;
}
跟昨天的 isPartOfEgg 幾乎一樣,只是回傳值從 true/false 改成「那個物件」或 null。
不用 alert(),改用一個藏在頁面裡、平常看不到的 <div>,需要時才讓它淡入顯示:
HTML(加在 index.html 的 <body> 裡):
<div id="toast"></div>
CSS:
#toast {
position: fixed;
top: 40px;
left: 50%;
transform: translateX(-50%) translateY(-20px) scale(0.9);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 16px 32px;
border-radius: 12px;
font-size: 20px;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s ease, transform 0.4s ease;
z-index: 1000;
}
#toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
依 MDN 的 CSS transitions 說明,transition 這個屬性,是讓瀏覽器在某個 CSS 數值改變時,自動畫出「中間過程」的動畫,不用自己一幀一幀計算。 這裡設定的是 opacity(透明度)和 transform(位置/縮放)這兩個屬性,只要加上 .show 這個 class,CSS 數值從「隱藏的樣子」變成「顯示的樣子」,瀏覽器就會自動補完中間 0.4 秒的漸變過程,看起來才會有「淡入、往下滑」的動畫感,不是一瞬間硬生生跳出來。
JavaScript(控制什麼時候顯示、顯示多久):
const toast = document.getElementById('toast');
let toastTimer;
function showToast(message, duration = 2500) {
toast.textContent = message;
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toast.classList.remove('show');
}, duration);
}
toast.classList.add('show'):加上 .show 這個 class,觸發前面 CSS 寫好的動畫setTimeout(...):等 2.5 秒之後,自動把 .show 拿掉,提示框淡出消失clearTimeout(toastTimer):如果提示框還在顯示中,又被叫出來一次,先取消上一次的倒數計時,避免兩次顯示的計時器互相搶著把它關掉window.addEventListener('click', (event) => {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length === 0) return;
const hit = intersects[0].object;
const eggRoot = findEggRoot(hit);
if (!eggRoot) {
console.log('點到公園本體:', hit.name);
return;
}
// ★ 關鍵:已經找到過,就不要再觸發
if (eggRoot.userData.found) {
console.log('這個彩蛋已經找過了');
return;
}
eggRoot.userData.found = true; // ★ 標記成「已找到」
showToast('🎉 恭喜你找到彩蛋了!');
});
防止重複觸發的關鍵只有這三行:
if (eggRoot.userData.found) {
return; // 已經找過,直接結束,不執行下面顯示提示的程式碼
}
eggRoot.userData.found = true;
第一次點擊:found 是 false,跳過 if,往下執行,顯示提示、並把 found 改成 true。
第二次點擊同一個彩蛋:found 已經是 true,if 成立,直接 return,不會再跳提示。

如圖所示,找到彩蛋的提示做好了,
接下來要加計分或計時功能,讓這個不只是「找到就跳提示」,而是可以紀錄多久找出來。讓這個專案變得更有遊戲感。