使用最新threejs復刻經典貪吃蛇游戲的3D版,附完整源碼

基類Entity

建立基類Entity,實現投影能力、動畫入場效果(從小變大的彈性動畫)、計算自己在地圖格位置的方法。

// 導入gsap動畫庫(用于創建補間動畫)
import gsap from 'gsap'// 定義Entity基類
export default class Entity {constructor(mesh, resolution, option = { size: 1.5, number: 0.5 }) {// 保存傳入的3D網格對象this.mesh = mesh// 配置網格的陰影屬性mesh.castShadow = true   // 允許投射陰影mesh.receiveShadow = true // 允許接收陰影// 保存分辨率參數和相關配置選項this.resolution = resolution  // 可能包含屏幕/布局分辨率信息this.option = option          // 動畫參數,默認size=1.5, number=0.5}// 位置屬性的getter,代理訪問mesh的位置get position() {return this.mesh.position  // 返回THREE.Vector3對象}// 根據坐標計算索引的方法(可能用于網格布局)getIndexByCoord() {const { x, y } = this.resolution  // 解構分辨率,可能代表網格列/行數// 計算索引公式:z坐標 * x軸分辨率 + x坐標// 注意:此處可能存在問題,通常3D空間索引需要考慮y坐標return this.position.z * x + this.position.x}// 進入動畫方法in() {// 使用gsap創建縮放動畫gsap.from(this.mesh.scale, {  // 從指定狀態動畫到當前狀態duration: 1,                // 動畫時長1秒x: 0,                       // 初始X縮放為0y: 0,                       // 初始Y縮放為0 z: 0,                       // 初始Z縮放為0ease: `elastic.out(${this.option.size}, ${this.option.number})`, // 彈性緩動函數})}// 離開動畫方法(暫未實現)out() {}
}

開發糖果類Candy

Candy這個類繼承了之前Entity,這里用threejs內置的SphereGeometry(0.3, 20, 20)做了個小圓球模型。然后用MeshStandardMaterial做材質,并可以傳入顏色,所有糖果都會用這個模具和顏色來制作。并且可以隨機生成大小不一的糖果,代表不同的分數。

// 導入Three.js的3D對象相關模塊
import {Mesh,            // 網格對象(幾何體+材質的組合)MeshNormalMaterial, // 顯示法向量的標準材質MeshStandardMaterial, // PBR標準材質SphereGeometry,  // 球體幾何體
} from 'three'// 導入自定義的Entity基類
import Entity from './Entity'// 創建球體幾何體(半徑0.3,經緯分段數各20)
const GEOMETRY = new SphereGeometry(0.3, 20, 20)
// 創建標準材質并設置基礎顏色為紫色(#614bdd)
const MATERIAL = new MeshStandardMaterial({color: 0x614bdd,
})// 定義Candy類,繼承自Entity基類
export default class Candy extends Entity {constructor(resolution, color) {// 創建網格對象(使用共享的幾何體和材質)const mesh = new Mesh(GEOMETRY, MATERIAL)// 調用父類Entity的構造函數super(mesh, resolution)  // 假設Entity處理了場景添加、位置初始化等// 如果有傳入顏色參數,則覆蓋材質的默認顏色// 注意:這里會修改共享材質,影響所有Candy實例!if (color) {MATERIAL.color.set(color)}// 生成隨機點數(1-3點)this.points = Math.floor(Math.random() * 3) + 1// 根據點數設置縮放比例:點數越多,糖果越大// 基礎縮放0.5 + (點數*0.5)/3 → 范圍在0.5~1.0之間this.mesh.scale.setScalar(0.5 + (this.points * 0.5) / 3)}
}

創建LinkedKList與ListNode類,實現鏈表結構,來模擬貪吃蛇相連的每節節點。

// 鏈表容器類(管理整個鏈)
export default class LinkedList {constructor(head) { // 初始化必須傳入頭節點this.head = head // 存儲鏈表頭部this.end = head  // 存儲鏈表尾部(初始頭就是尾)}// 添加新節點方法(注意:只能向后追加)addNode(node) {this.end.linkTo(node) // 讓當前尾部連接新節點this.end = node // 更新尾部為新節點}
}
// 鏈表節點類(每個節點像一節火車車廂)
export default class ListNode {next = null // 指向下一個車廂的鉤子prev = null // 指向前一個車廂的鉤子constructor(data) {this.data = data // 當前車廂}// 連接節點方法(像車廂掛鉤的動作)linkTo(node) {this.next = node // 當前節點鉤住下一個node.prev = this // 下一個節點反鉤回來(形成雙向連接)}
}

Snake類

創建貪吃蛇本體,實現轉向,移動,吃糖果加尾部節點,碰到障礙物檢測的邏輯。需要用到之前的LinkedList、ListNode、Entity類。

import {EventDispatcher,Mesh,MeshNormalMaterial,MeshStandardMaterial,SphereGeometry,Vector2,Vector3,
} from 'three'
import LinkedList from './LinkedList'
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry'
import ListNode from './ListNode'
import Entity from './Entity'const NODE_GEOMETRY = new RoundedBoxGeometry(0.9, 0.9, 0.9, 5, 0.1) // 用threejs內置函數創建圓角四方塊
const NODE_MATERIAL = new MeshStandardMaterial({color: 0xff470a,
}) // 創建材質并給初始顏色const UP = new Vector3(0, 0, -1)
const DOWN = new Vector3(0, 0, 1)
const LEFT = new Vector3(-1, 0, 0)
const RIGHT = new Vector3(1, 0, 0)
export default class Snake extends EventDispatcher {direction = RIGHT  // 初始方向朝右indexes = []       // 存儲蛇身占用的格子坐標speedInterval = 240 // 移動速度(數值越大越慢)constructor({ scene, resolution = new Vector2(10, 10), color, mouthColor }) {super()// 把游戲場景、地圖尺寸、顏色存起來this.scene = scenethis.resolution = resolutionthis.mouthColor = mouthColor// 如果有顏色參數,給所有蛇節點刷顏色if (color) {NODE_MATERIAL.color.set(color)}this.init() // 開始組裝蛇}get head() {return this.body.head}get end() {return this.body.end}//組裝蛇頭createHeadMesh() {const headMesh = this.body.head.data.mesh // 獲取蛇頭模型// 造左眼:白眼球+黑眼珠const leftEye = new Mesh(new SphereGeometry(0.2, 10, 10),new MeshStandardMaterial({ color: 0xffffff }))leftEye.scale.x = 0.1leftEye.position.x = 0.5leftEye.position.y = 0.12leftEye.position.z = -0.1let leftEyeHole = new Mesh(new SphereGeometry(0.22, 10, 10),new MeshStandardMaterial({ color: 0x333333 }))leftEyeHole.scale.set(1, 0.6, 0.6)leftEyeHole.position.x += 0.05leftEye.add(leftEyeHole)// 造右眼:同上const rightEye = leftEye.clone()rightEye.position.x = -0.5rightEye.rotation.y = Math.PI// 造嘴巴:紫色圓角矩形const mouthMesh = new Mesh(new RoundedBoxGeometry(1.05, 0.1, 0.6, 5, 0.1),new MeshStandardMaterial({color: this.mouthColor, //0x614bdd,}))mouthMesh.rotation.x = -Math.PI * 0.07mouthMesh.position.z = 0.23mouthMesh.position.y = -0.19this.mouth = mouthMesh// 把零件裝到蛇頭上headMesh.add(rightEye, leftEye, mouthMesh)/* 調整頭部朝向當前方向 */headMesh.lookAt(headMesh.position.clone().add(this.direction))}init() {// 重置方向this.direction = RIGHTthis.iMoving = null// 造第一個蛇頭節點(放在地圖中心)const head = new ListNode(new SnakeNode(this.resolution))head.data.mesh.position.x = this.resolution.x / 2head.data.mesh.position.z = this.resolution.y / 2this.body = new LinkedList(head)this.createHeadMesh()this.indexes.push(this.head.data.getIndexByCoord())// 添加初始三節身體(類似火車車廂)for (let i = 0; i < 3; i++) {const position = this.end.data.mesh.position.clone()position.sub(this.direction) // 每節身體往后挪一格this.addTailNode() // 掛載到鏈表末尾this.end.data.mesh.position.copy(position)this.indexes.push(this.end.data.getIndexByCoord())}head.data.in()this.scene.add(head.data.mesh)}setDirection(keyCode) {let newDirection// 根據按鍵計算新方向(上下左右)switch (keyCode) {case 'ArrowUp':case 'KeyW':newDirection = UPbreakcase 'ArrowDown':case 'KeyS':newDirection = DOWNbreakcase 'ArrowLeft':case 'KeyA':newDirection = LEFTbreakcase 'ArrowRight':case 'KeyD':newDirection = RIGHTbreakdefault:return}const dot = this.direction.dot(newDirection)// 禁止180度掉頭(比如正在右走不能直接左轉)if (dot === 0) {this.newDirection = newDirection}}// 每幀判斷update() {// 應用新方向if (this.newDirection) {this.direction = this.newDirectionthis.newDirection = null}let currentNode = this.end// 處理吃糖果后的尾巴生長if (this.end.data.candy) {this.end.data.candy = nullthis.end.data.mesh.scale.setScalar(1)this.addTailNode()}// 身體像波浪一樣跟隨頭部移動while (currentNode.prev) {const candy = currentNode.prev.data.candyif (candy) {currentNode.data.candy = candycurrentNode.data.mesh.scale.setScalar(1.15)currentNode.prev.data.candy = nullcurrentNode.prev.data.mesh.scale.setScalar(1)}const position = currentNode.prev.data.mesh.position// 每節身體移動到前一節的位置currentNode.data.mesh.position.copy(position)currentNode = currentNode.prev // 往前遍歷}const headPos = currentNode.data.mesh.positionheadPos.add(this.direction)// currentNode.data.mesh.position.add(this.direction)const headMesh = this.body.head.data.meshheadMesh.lookAt(headMesh.position.clone().add(this.direction))// 移動頭部if (headPos.z < 0) {headPos.z = this.resolution.y - 1} else if (headPos.z > this.resolution.y - 1) {headPos.z = 0}// 邊界穿越處理(從地圖左邊消失就從地圖右邊出來...其他邊界類似)if (headPos.x < 0) {headPos.x = this.resolution.x - 1} else if (headPos.x > this.resolution.x - 1) {headPos.x = 0}this.updateIndexes()// 向上拋出事件this.dispatchEvent({ type: 'updated' })}// 死亡die() {let node = this.body.head// 移除所有身體部件do {this.scene.remove(node.data.mesh) // 從場景刪除模型node = node.next} while (node)this.init() // 重新初始化this.addEventListener({ type: 'die' }) // 向上拋出死亡事件}checkSelfCollision() {// 檢查頭部坐標是否和身體坐標重疊const headIndex = this.indexes.pop()const collide = this.indexes.includes(headIndex)this.indexes.push(headIndex)return collide // 撞到自己返回true}checkEntitiesCollision(entities) {// 檢查頭部坐標是否和障礙物坐標重疊const headIndex = this.indexes.at(-1)const entity = entities.find((entity) => entity.getIndexByCoord() === headIndex)return !!entity // 撞到障礙物返回true}// 更新蛇身所有節點的網格坐標索引,用于碰撞檢測等需要知道蛇身位置的功能updateIndexes() {// 清空舊索引(相當于擦除之前的身體痕跡)this.indexes = []// 從蛇尾開始遍歷(相當于從火車最后一節車廂開始檢查)let node = this.body.end// 循環向前遍歷所有節點(直到沒有前一節車廂)while (node) {// 獲取當前節點的網格坐標(比如把3D坐標轉換為地圖網格的[x,y])// 假設地圖是10x10網格,坐標(5.3, 0, 5.7)會被轉換為索引[5,5]// 將索引推入數組(記錄這節身體占據的格子)this.indexes.push(node.data.getIndexByCoord())// 移動到前一節身體(向蛇頭方向移動)node = node.prev// 最終得到的indexes數組示例:// [[3,5], [4,5], [5,5]] 表示蛇身占據這三個網格// 其中最后一個元素[5,5]是蛇頭位置}}// 添加尾部節點addTailNode(position) {const node = new ListNode(new SnakeNode(this.resolution))if (position) {node.data.mesh.position.copy(position)} else {node.data.mesh.position.copy(this.end.data.mesh.position)}this.body.addNode(node)node.data.in()this.scene.add(node.data.mesh)}
}
// 蛇身體節點類
class SnakeNode extends Entity {constructor(resolution) {const mesh = new Mesh(NODE_GEOMETRY, NODE_MATERIAL)super(mesh, resolution)}
}

實現障礙物,樹Tree與石頭Rock的類

// 導入Three.js相關模塊
import {IcosahedronGeometry,   // 二十面體幾何體(適合制作復雜形狀)Mesh,                  // 網格對象(用于組合幾何體與材質)MeshNormalMaterial,    // 法線材質(調試用,顯示表面朝向)MeshStandardMaterial,  // PBR標準材質(支持金屬/粗糙度等特性)
} from 'three'
import Entity from './Entity' // 基礎實體類// 創建共享幾何體(優化性能,所有樹實例共用同一個幾何體)
const GEOMETRY = new IcosahedronGeometry(0.3) // 基礎半徑為0.3的二十面體
GEOMETRY.rotateX(Math.random() * Math.PI * 2) // 隨機繞X軸旋轉(避免重復感)
GEOMETRY.scale(1, 6, 1) // Y軸拉伸6倍,形成細長形狀(類似樹干)// 創建共享材質(所有樹實例默認使用相同材質)
const MATERIAL = new MeshStandardMaterial({flatShading: true,    // 平面著色(增強低多邊形風格)color: 0xa2d109,     // 默認黃綠色(類似樹葉顏色)
})// 定義樹類(繼承自基礎實體類)
export default class Tree extends Entity {constructor(resolution, color) {// 創建網格實例(組合幾何體與材質)const mesh = new Mesh(GEOMETRY, MATERIAL)// 隨機縮放(0.6~1.2倍原始尺寸,制造大小差異)mesh.scale.setScalar(0.6 + Math.random() * 0.6)// 隨機Y軸旋轉(讓樹木朝向不同方向)mesh.rotation.y = Math.random() * Math.PI * 2// 如果指定顏色,覆蓋默認材質顏色if (color) {MATERIAL.color.set(color)}// 調用父類構造函數(處理坐標轉換等)super(mesh, resolution)}
}
// 導入Three.js相關模塊
import {IcosahedronGeometry,      // 二十面體幾何體(用于創建復雜形狀)Mesh,                     // 網格對象(組合幾何體與材質)MeshNormalMaterial,       // 法線材質(調試用)MeshStandardMaterial,     // PBR標準材質(支持金屬/粗糙度)
} from 'three'
import Entity from './Entity' // 基礎實體類// 創建共享幾何體(所有巖石實例共用)
const GEOMETRY = new IcosahedronGeometry(0.5)  // 基礎半徑0.5的二十面體// 創建共享材質(所有巖石實例默認使用相同材質)
const MATERIAL = new MeshStandardMaterial({color: 0xacacac,          // 默認巖石灰色flatShading: true,        // 平面著色(增強低多邊形風格)
})// 巖石類(繼承基礎實體類)
export default class Rock extends Entity {constructor(resolution, color) {// 創建網格實例(幾何體+材質)const mesh = new Mesh(GEOMETRY, MATERIAL)// X軸:0.5~1倍隨機縮放(橫向隨機寬度)// Y軸:0.5~2.4倍縮放(使用平方讓更多巖石較矮)// Z軸:保持1倍(前后方向不變形)mesh.scale.set(Math.random() * 0.5 + 0.5, 0.5 + Math.random() ** 2 * 1.9, 1)mesh.rotation.y = Math.random() * Math.PI * 2 // 隨機Y軸旋轉(0-360度)mesh.rotation.x = Math.random() * Math.PI * 0.1 // 輕微X軸傾斜(最大18度)mesh.rotation.order = 'YXZ' // 旋轉順序:先Y后X最后Z(避免萬向鎖問題)// 下沉位置(使巖石看起來半埋在地面)mesh.position.y = -0.5// 如果指定顏色,覆蓋默認材質顏色if (color) {MATERIAL.color.set(color)}// 調用父類構造函數(處理坐標轉換等)super(mesh, resolution)}
}

最后,實現地圖尺寸配置Params.js,光照Lights等環境變量

// 導入Three.js二維向量模塊
import { Vector2 } from 'three'// 定義場景地圖尺寸參數 20*20的格子
const resolution = new Vector2(20, 20)
/*** 參數說明:* x: 場景橫向尺* y: 場景縱向尺寸*/// 定義顏色配置集合
const colors = {groundColor: '#ff7438',  // 地面基礎色(暖橙色)fogColor: '#d68a4c'      // 霧效顏色(與地面色協調的淺棕色)
}// 導出配置參數(供其他模塊統一訪問)
export { resolution, colors }
// 導入Three.js光源相關模塊
import { AmbientLight, DirectionalLight } from 'three'
import { resolution } from './Params'  // 場景尺寸參數// 創建環境光(提供基礎照明,無方向性)
const ambLight = new AmbientLight(0xffffff,   // 白光(十六進制顏色值)0.6          // 光照強度(范圍0-1,相當于60%亮度)
)// 創建平行光(模擬太陽光,產生方向性陰影)
const dirLight = new DirectionalLight(0xffffff,    // 白光0.7          // 主光源強度(70%亮度)
)// 設置平行光參數
dirLight.position.set(20, 20, 18)     // 光源三維坐標(模擬高空太陽位置)
dirLight.target.position.set(         // 光照焦點位置(場景中心點)resolution.x / 2,                   // X軸中心(地圖寬度的一半)0,                                  // Y軸保持地面高度resolution.y / 2                    // Z軸中心(地圖深度的一半)
)// 陰影質量配置
dirLight.shadow.mapSize.set(1024, 1024)  // 陰影貼圖分辨率(值越大越清晰,但更耗性能)
dirLight.shadow.radius = 7              // 陰影模糊半徑(軟化陰影邊緣)
dirLight.shadow.blurSamples = 20        // 模糊采樣次數(提升陰影邊緣平滑度)// 設置陰影相機的視錐范圍(控制產生陰影的區域)
dirLight.shadow.camera.top = 30     // 可見區域頂部邊界
dirLight.shadow.camera.bottom = -30 // 可見區域底部邊界
dirLight.shadow.camera.left = -30   // 可見區域左邊界
dirLight.shadow.camera.right = 30   // 可見區域右邊界dirLight.castShadow = true  // 啟用該光源的陰影投射// 組合光源(通常場景需要多個光源配合)
const lights = [dirLight, ambLight]export default lights  // 導出光源配置集合

最后是入口文件main.js的主邏輯,包含游戲循環,按鍵監聽,主題切換,游戲開始與失敗邏輯

import './style.css'// 導入Three.js核心庫和擴展模塊
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' // 攝像機軌道控制器
import Snake from './Snake' // 貪吃蛇游戲角色
import Candy from './Candy' // 糖果道具
import Rock from './Rock'   // 巖石障礙物
import Tree from './Tree'   // 樹木裝飾
import lights from './Lights' // 光照系統
import { resolution } from './Params' // 場景分辨率參數
import gsap from 'gsap' // 動畫庫(用于平滑過渡)
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader' // 字體加載器
import fontSrc from 'three/examples/fonts/helvetiker_bold.typeface.json?url' // 3D字體文件
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry' // 3D文字幾何體
import Entity from './Entity' // 基礎實體類// 設備檢測(移動端適配)
const isMobile = window.innerWidth <= 768
const loader = new FontLoader()
let font
// 字體加載系統
loader.load(fontSrc, function (loadedFont) {font = loadedFontprintScore() // 字體加載完成后初始化計分板
})// 配色方案集合(支持主題切換)
const palettes = {green: { // 綠色主題groundColor: 0x56f854,fogColor: 0x39c09f,rockColor: 0xebebeb,treeColor: 0x639541,candyColor: 0x1d5846,snakeColor: 0x1d5846,mouthColor: 0x39c09f,},orange: { // 橙色主題groundColor: 0xd68a4c,fogColor: 0xffac38,rockColor: 0xacacac,treeColor: 0xa2d109,candyColor: 0x614bdd,snakeColor: 0xff470a,mouthColor: 0x614bdd,},lilac: { // 紫色主題groundColor: 0xd199ff,fogColor: 0xb04ce6,rockColor: 0xebebeb,treeColor: 0x53d0c1,candyColor: 0x9900ff,snakeColor: 0xff2ed2,mouthColor: 0x614bdd,},
}// 主題管理系統
let paletteName = localStorage.getItem('paletteName') || 'green'
let selectedPalette = palettes[paletteName]const params = {...selectedPalette, // 當前生效的配色參數
}// 應用配色方案(支持運行時切換)
function applyPalette(paletteName) {const palette = palettes[paletteName]localStorage.setItem('paletteName', paletteName)selectedPalette = paletteif (!palette) return// 更新場景元素顏色const {groundColor,fogColor,rockColor,treeColor,candyColor,snakeColor,mouthColor,} = paletteplaneMaterial.color.set(groundColor) // 地面scene.fog.color.set(fogColor) // 霧效scene.background.set(fogColor) // 背景// 更新實體顏色(巖石、樹木等)entities.find((entity) => entity instanceof Rock)?.mesh.material.color.set(rockColor)entities.find((entity) => entity instanceof Tree)?.mesh.material.color.set(treeColor)// 更新游戲元素candies[0].mesh.material.color.set(candyColor)snake.body.head.data.mesh.material.color.set(snakeColor)snake.body.head.data.mesh.material.color.set(snakeColor)snake.mouthColor = mouthColorsnake.mouth.material.color.set(mouthColor)// 更新UI按鈕btnPlayImg.src = `/btn-play-bg-${paletteName}.png`
}
// 游戲核心參數
let score = 0 // 得分統計// 網格輔助線(可視化場景坐標系)
const gridHelper = new THREE.GridHelper(resolution.x,     // 橫向分割數resolution.y,     // 縱向分割數0xffffff,         // 主網格顏色0xffffff          // 次級網格顏色
)
gridHelper.position.set(resolution.x / 2 - 0.5, -0.49, resolution.y / 2 - 0.5) // 居中定位
gridHelper.material.transparent = true
gridHelper.material.opacity = isMobile ? 0.75 : 0.3 // 移動端降低透明度// 場景初始化
const scene = new THREE.Scene()
scene.background = new THREE.Color(params.fogColor) // 背景scene.fog = new THREE.Fog(params.fogColor, 5, 40) // 添加霧效scene.add(gridHelper)  // 添加輔助網格// 視窗尺寸管理
const sizes = {width: window.innerWidth,height: window.innerHeight,
}// 攝像機系統
const fov = 60 // 正交相機(類似人眼)
const camera = new THREE.PerspectiveCamera(fov, sizes.width / sizes.height, 0.1)// 攝像機位置(移動端與PC不同)
const finalPosition = isMobile? new THREE.Vector3(resolution.x / 2 - 0.5, resolution.x + 15, resolution.y): new THREE.Vector3(-8 + resolution.x / 2,resolution.x / 2 + 4,resolution.y + 6)
const initialPosition = new THREE.Vector3(resolution.x / 2 + 5,4,resolution.y / 2 + 4
)
camera.position.copy(initialPosition)// 渲染器配置
const renderer = new THREE.WebGLRenderer({antialias: window.devicePixelRatio < 2, // 抗鋸齒(高清屏關閉)logarithmicDepthBuffer: true, // 解決遠距離渲染問題
})
document.body.appendChild(renderer.domElement)
handleResize() // 初始自適應// 高級渲染特性
renderer.toneMapping = THREE.ACESFilmicToneMapping // 電影級色調映射
renderer.toneMappingExposure = 1.2 // 曝光強度
renderer.shadowMap.enabled = true // 啟用陰影
renderer.shadowMap.type = THREE.VSMShadowMap // 柔和陰影算法// 攝像機控制器(限制移動方式)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true    // 慣性滑動
controls.enableZoom = false      // 禁用縮放
controls.enablePan = false       // 禁用平移
controls.enableRotate = false    // 禁用旋轉
controls.target.set(resolution.x/2 -2, 0, resolution.y/2 + (isMobile ? 0 : 2))// 地面
const planeGeometry = new THREE.PlaneGeometry(resolution.x*50, resolution.y*50)
planeGeometry.rotateX(-Math.PI * 0.5)  // 創建水平面為地面
const planeMaterial = new THREE.MeshStandardMaterial({color: params.groundColor
})
const plane = new THREE.Mesh(planeGeometry, planeMaterial)
plane.position.set(resolution.x/2-0.5, -0.5, resolution.y/2-0.5) // 對齊網格
plane.receiveShadow = true  // 接收陰影
scene.add(plane)// 游戲角色初始化
const snake = new Snake({scene,resolution,color: selectedPalette.snakeColor,mouthColor: selectedPalette.mouthColor,
})// 游戲事件系統
snake.addEventListener('updated', function () {// 碰撞檢測,碰撞上障礙就死亡,重置游戲if (snake.checkSelfCollision() || snake.checkEntitiesCollision(entities)) {snake.die()resetGame()}// 糖果收集檢測const headIndex = snake.indexes.at(-1)const candyIndex = candies.findIndex((candy) => candy.getIndexByCoord() === headIndex)// 吃到糖果if (candyIndex >= 0) {const candy = candies[candyIndex]scene.remove(candy.mesh) // 從場景移除被吃掉的糖果candies.splice(candyIndex, 1)snake.body.head.data.candy = candyaddCandy() // 生成新的糖果score += candy.points // 加分printScore() // 更新分數板}
})let scoreEntity // 當前分數板function printScore() {// 等待字體加載完成if (!font) {return}if (!score) {score = 0}// 清理舊分數對象(避免內存泄漏)if (scoreEntity) {scene.remove(scoreEntity.mesh)scoreEntity.mesh.geometry.dispose() // 釋放幾何體內存scoreEntity.mesh.material.dispose() // 釋放材質內存}// 創建帶倒角的3D文字幾何體const geometry = new TextGeometry(`${score}`, {font: font,size: 1,depth: 0.1,curveSegments: 12,bevelEnabled: true,   // 啟用倒角bevelThickness: 0.1,  // 倒角深度bevelSize: 0.1,       // 倒角寬度bevelOffset: 0,bevelSegments: 5,})geometry.center() // 幾何體居中if (isMobile) {geometry.rotateX(-Math.PI * 0.5) // 移動端縱向旋轉90度}// 復用蛇頭材質創建網格const mesh = new THREE.Mesh(geometry, snake.body.head.data.mesh.material)// 定位在場景上方mesh.position.x = resolution.x / 2 - 0.5mesh.position.z = -4mesh.position.y = 1.8// 啟用陰影投射mesh.castShadow = true// 創建分數實體并加入場景scoreEntity = new Entity(mesh, resolution, { size: 0.8, number: 0.3 })// 播放入場動畫scoreEntity.in()scene.add(scoreEntity.mesh)
}// 移動端觸摸控制系統 虛擬方向鍵DOM
const mobileArrows = document.getElementById('mobile-arrows')function registerEventListener() {if (isMobile) {// 觸摸邏輯核心參數const prevTouch = new THREE.Vector2()  // 記錄上次觸摸位置let middle = 1.55     // 屏幕中心參考線let scale = 1         // 方向判斷靈敏度系數// 觸摸事件監聽window.addEventListener('touchstart', (event) => {const touch = event.targetTouches[0]middle = THREE.MathUtils.clamp(middle, 1.45, 1.65)// 將屏幕坐標轉換為歸一化坐標(-1到1范圍)let x = (2 * touch.clientX) / window.innerWidth - 1let y = (2 * touch.clientY) / window.innerHeight - middle// 游戲啟動檢測if (!isRunning) startGame()// 方向判斷算法(根據坐標象限)if (x * scale > y) {if (x * scale < -y) {snake.setDirection('ArrowUp')scale = 3} else {snake.setDirection('ArrowRight')middle += yscale = 0.33}} else {if (-x * scale > y) {snake.setDirection('ArrowLeft')middle += yscale = 0.33} else {snake.setDirection('ArrowDown')scale = 3}}prevTouch.x = xprevTouch.y = y // 記錄本次觸摸位置})} else {// 桌面端鍵盤事件監聽window.addEventListener('keydown', function (e) {const keyCode = e.codesnake.setDirection(keyCode) // 方向鍵控制if (keyCode === 'Space') { // 空格鍵暫停/繼續!isRunning ? startGame() : stopGame()} else if (!isRunning) {startGame()}})}
}let isRunning // 游戲運行狀態標記function startGame() {if (!snake.isMoving) {// 設置240ms間隔的蛇移動循環(約4幀/秒)isRunning = setInterval(() => {snake.update()}, 240)}
}// 暫停游戲
function stopGame() {clearInterval(isRunning) // 清除計時器isRunning = null // 重置狀態
}// 重置游戲
function resetGame() {stopGame() // 停止游戲循環score = 0 // 重置積分// 清理所有糖果對象let candy = candies.pop()while (candy) {scene.remove(candy.mesh)candy = candies.pop()}// 清理所有實體對象(巖石/樹木)let entity = entities.pop()while (entity) {scene.remove(entity.mesh)entity = entities.pop()}// 重新初始化游戲元素addCandy()generateEntities()
}const candies = [] // 糖果對象池
const entities = [] // 實體對象池(障礙物)// 糖果生成邏輯
function addCandy() {const candy = new Candy(resolution, selectedPalette.candyColor)let index = getFreeIndex() // 獲取可用位置索引// 設置三維坐標(基于索引的網格布局)candy.mesh.position.x = index % resolution.xcandy.mesh.position.z = Math.floor(index / resolution.x)candies.push(candy) // 加入對象池candy.in() // 播放生成動畫scene.add(candy.mesh)
}addCandy()// 隨機位置生成算法
function getFreeIndex() {let indexlet candyIndexes = candies.map((candy) => candy.getIndexByCoord())let entityIndexes = entities.map((entity) => entity.getIndexByCoord())// 生成隨機索引(范圍:0到場景總網格數)do {index = Math.floor(Math.random() * resolution.x * resolution.y)} while (snake.indexes.includes(index) || // 不與蛇身重疊candyIndexes.includes(index) || // 不與現有糖果重疊entityIndexes.includes(index) // 不與障礙物重疊)return index
}// 障礙物生成邏輯
function addEntity() {// 隨機生成巖石或樹木(50%概率)const entity =Math.random() > 0.5? new Rock(resolution, selectedPalette.rockColor): new Tree(resolution, selectedPalette.treeColor)let index = getFreeIndex() // 獲取安全位置// 設置坐標(與糖果生成邏輯相同)entity.mesh.position.x = index % resolution.xentity.mesh.position.z = Math.floor(index / resolution.x)entities.push(entity) // 加入對象池scene.add(entity.mesh) // 加入場景
}// 初始實體生成器
function generateEntities() {// 生成20個障礙物for (let i = 0; i < 20; i++) {addEntity()}// 按距離場景中心排序(優化渲染順序)entities.sort((a, b) => {const c = new THREE.Vector3(resolution.x / 2 - 0.5,0,resolution.y / 2 - 0.5)const distanceA = a.position.clone().sub(c).length()const distanceB = b.position.clone().sub(c).length()return distanceA - distanceB})// 使用GSAP實現彈性入場動畫gsap.from(entities.map((entity) => entity.mesh.scale),{x: 0,y: 0,z: 0,duration: 1,ease: 'elastic.out(1.5, 0.5)',stagger: {grid: [20, 20],amount: 0.7,},})
}generateEntities()// 添加光照
scene.add(...lights)// 生成裝飾性樹,地圖外的
const treeData = [new THREE.Vector4(-5, 0, 10, 1),new THREE.Vector4(-6, 0, 15, 1.2),new THREE.Vector4(-5, 0, 16, 0.8),new THREE.Vector4(-10, 0, 4, 1.3),new THREE.Vector4(-5, 0, -3, 2),new THREE.Vector4(-4, 0, -4, 1.5),new THREE.Vector4(-2, 0, -15, 1),new THREE.Vector4(5, 0, -20, 1.2),new THREE.Vector4(24, 0, -12, 1.2),new THREE.Vector4(2, 0, -6, 1.2),new THREE.Vector4(3, 0, -7, 1.8),new THREE.Vector4(1, 0, -9, 1.0),new THREE.Vector4(15, 0, -8, 1.8),new THREE.Vector4(17, 0, -9, 1.1),new THREE.Vector4(18, 0, -7, 1.3),new THREE.Vector4(24, 0, -1, 1.3),new THREE.Vector4(26, 0, 0, 1.8),new THREE.Vector4(32, 0, 0, 1),new THREE.Vector4(28, 0, 6, 1.7),new THREE.Vector4(24, 0, 15, 1.1),new THREE.Vector4(16, 0, 23, 1.1),new THREE.Vector4(12, 0, 24, 0.9),new THREE.Vector4(-13, 0, -13, 0.7),new THREE.Vector4(35, 0, 10, 0.7),
]
const tree = new Tree(resolution)treeData.forEach(({ x, y, z, w }) => {let clone = tree.mesh.clone()clone.position.set(x, y, z)clone.scale.setScalar(w)scene.add(clone)
})const rock = new Rock(resolution)
const resX = resolution.x
const rexY = resolution.y// 生成裝飾性石頭,地圖外的
const rockData = [[new THREE.Vector3(-7, -0.5, 2), new THREE.Vector4(2, 8, 3, 2.8)],[new THREE.Vector3(-3, -0.5, -10), new THREE.Vector4(3, 2, 2.5, 1.5)],[new THREE.Vector3(-5, -0.5, 3), new THREE.Vector4(1, 1.5, 2, 0.8)],[new THREE.Vector3(resX + 5, -0.5, 3), new THREE.Vector4(4, 1, 3, 1)],[new THREE.Vector3(resX + 4, -0.5, 2), new THREE.Vector4(2, 2, 1, 1)],[new THREE.Vector3(resX + 8, -0.5, 16), new THREE.Vector4(6, 2, 4, 4)],[new THREE.Vector3(resX + 6, -0.5, 13), new THREE.Vector4(3, 2, 2.5, 3.2)],[new THREE.Vector3(resX + 5, -0.5, -8), new THREE.Vector4(1, 1, 1, 0)],[new THREE.Vector3(resX + 6, -0.5, -7), new THREE.Vector4(2, 4, 1.5, 0.5)],[new THREE.Vector3(-5, -0.5, 14), new THREE.Vector4(1, 3, 2, 0)],[new THREE.Vector3(-4, -0.5, 15), new THREE.Vector4(0.8, 0.6, 0.7, 0)],[new THREE.Vector3(resX / 2 + 5, -0.5, 25),new THREE.Vector4(2.5, 0.8, 4, 2),],[new THREE.Vector3(resX / 2 + 9, -0.5, 22),new THREE.Vector4(1.2, 2, 1.2, 1),],[new THREE.Vector3(resX / 2 + 8, -0.5, 21.5),new THREE.Vector4(0.8, 1, 0.8, 2),],
]rockData.forEach(([position, { x, y, z, w }]) => {let clone = new Rock(resolution).meshclone.position.copy(position)clone.scale.set(x, y, z)clone.rotation.y = wscene.add(clone)
})// 音效
const audio = document.getElementById('audio')
const btnVolume = document.getElementById('btn-volume')
const btnPlay = document.getElementById('btn-play')
const btnPlayImg = document.getElementById('btn-play-img')
// 音效按鈕效果
gsap.fromTo(btnPlay,{ autoAlpha: 0, scale: 0, yPercent: -50, xPercent: -50 },{duration: 0.8,autoAlpha: 1,scale: 1,yPercent: -50,xPercent: -50,delay: 0.3,ease: `elastic.out(1.2, 0.7)`,}
)// 開始游戲
btnPlay.addEventListener('click', function () {audio.play()gsap.to(camera.position, { ...finalPosition, duration: 2 })if (isMobile) {gsap.to(controls.target, {x: resolution.x / 2 - 0.5,y: 0,z: resolution.y / 2 - 0.5,})}gsap.to(scene.fog, { duration: 2, near: isMobile ? 30 : 20, far: 55 })gsap.to(this, {duration: 1,scale: 0,ease: `elastic.in(1.2, 0.7)`,onComplete: () => {this.style.visibility = 'hidden'},})registerEventListener()
})const userVolume = localStorage.getItem('volume')
if (userVolume === 'off') {muteVolume()
}// 音量
const initialVolume = audio.volumebtnVolume.addEventListener('click', function () {if (audio.volume === 0) {unmuteVolume()} else {muteVolume()}
})// 靜音
function muteVolume() {localStorage.setItem('volume', 'off')gsap.to(audio, { volume: 0, duration: 1 })btnVolume.classList.remove('after:hidden')btnVolume.querySelector(':first-child').classList.remove('animate-ping')btnVolume.classList.add('after:block')
}// 解除靜音
function unmuteVolume() {localStorage.setItem('volume', 'on')btnVolume.classList.add('after:hidden')btnVolume.querySelector(':first-child').classList.add('animate-ping')btnVolume.classList.remove('after:block')gsap.to(audio, { volume: initialVolume, duration: 1 })
}// 主題選擇
const topBar = document.querySelector('.top-bar')
const paletteSelectors = document.querySelectorAll('[data-color]')
gsap.to(topBar, {opacity: 1,delay: 0.5,onComplete: () => {gsap.to(paletteSelectors, {duration: 1,x: 0,autoAlpha: 1,ease: `elastic.out(1.2, 0.9)`,stagger: {amount: 0.2,},})},
})paletteSelectors.forEach((selector) =>selector.addEventListener('click', function () {const paletteName = this.dataset.colorapplyPalette(paletteName)})
)// 加載器
const manager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(manager)// 按鍵新手引導
const wasd = textureLoader.load('/wasd.png')
const arrows = textureLoader.load('/arrows.png')const wasdGeometry = new THREE.PlaneGeometry(3.5, 2)
wasdGeometry.rotateX(-Math.PI * 0.5)const planeWasd = new THREE.Mesh(wasdGeometry,new THREE.MeshStandardMaterial({transparent: true,map: wasd,opacity: isMobile ? 0 : 0.5,})
)const planeArrows = new THREE.Mesh(wasdGeometry,new THREE.MeshStandardMaterial({transparent: true,map: arrows,opacity: isMobile ? 0 : 0.5,})
)planeArrows.position.set(8.7, 0, 21)
planeWasd.position.set(13, 0, 21)// 添加按鍵新手引導
scene.add(planeArrows, planeWasd)// 使用主題
applyPalette(paletteName)// 游戲主循環
function tic() {controls.update()renderer.render(scene, camera)requestAnimationFrame(tic)
}requestAnimationFrame(tic)// 監聽屏幕尺寸變化
window.addEventListener('resize', handleResize)function handleResize() {sizes.width = window.innerWidthsizes.height = window.innerHeightcamera.aspect = sizes.width / sizes.heightcamera.updateProjectionMatrix()renderer.setSize(sizes.width, sizes.height)const pixelRatio = Math.min(window.devicePixelRatio, 2)renderer.setPixelRatio(pixelRatio)
}

游戲 截圖如上

游戲源碼地址:GitCode - 全球開發者的開源社區,開源代碼托管平臺

游戲預覽地址:3D貪吃蛇

創作不易,點個贊再走吧

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/77449.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/77449.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/77449.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Redis——網絡模型之IO講解

目錄 前言 1.用戶空間和內核空間 1.2用戶空間和內核空間的切換 1.3切換過程 2.阻塞IO 3.非阻塞IO 4.IO多路復用 4.1.IO多路復用過程 4.2.IO多路復用監聽方式 4.3.IO多路復用-select 4.4.IO多路復用-poll 4.5.IO多路復用-epoll 4.6.select poll epoll總結 4.7.IO多…

Jenkins 多分支流水線: 如何創建用于 Jenkins 狀態檢查的 GitHub 應用

使用 Jenkins 多分支流水線時&#xff0c;您可以將狀態檢查與 GitHub 拉取請求集成。 以下是狀態檢查的示例 要實現這些類型的狀態檢查&#xff0c;您需要創建一個與 Jenkins 主實例集成的 GitHub 應用。 在本博客中&#xff0c;我們將介紹如何創建一個 GitHub 應用&#xff…

大模型如何突破“知識盲區”?一場靜悄悄的技術革命正在發生

大模型如何突破“知識盲區”&#xff1f;一場靜悄悄的技術革命正在發生 凌晨三點&#xff0c;程序員李然盯著屏幕上的報錯信息苦笑。他正在調試的智能客服系統&#xff0c;又一次把"北京今日體感溫度"回答成了"建議穿羽絨服"。這不是代碼錯誤&#xff0c;…

【SQL Server】數據探查工具1.0研發可行性方案

&#x1f449; 點擊關注不迷路 &#x1f449; 點擊關注不迷路 &#x1f449; 點擊關注不迷路 想搶先解鎖數據自由的寶子&#xff0c;速速戳我&#xff01;評論區蹲一波 “蹲蹲”&#xff0c;揪人嘮嘮你的超實用需求&#xff01; 【SQL Server】數據探查工具1.0研發可行性方案…

Qt GUI 庫總結

Qt GUI 庫總結 Qt GUI 庫&#xff08;QtGui&#xff09;是 Qt 框架中負責圖形用戶界面&#xff08;GUI&#xff09;開發的核心模塊。本文將一步步詳解 QtGui&#xff0c;從基礎入門到高級應用&#xff0c;幫助你全面掌握其功能。以下內容包括環境配置、基本功能、核心特性及進…

如何在米爾-STM32MP257開發板上部署環境監測系統

本文將介紹基于米爾電子MYD-LD25X開發板&#xff08;米爾基于STM35MP257開發板&#xff09;的環境監測系統方案測試。 摘自優秀創作者-lugl4313820 一、前言 環境監測是當前很多場景需要的項目&#xff0c;剛好我正在論壇參與的一個項目&#xff1a;Thingy:91X 蜂窩物聯網原型…

網絡互連與互聯網3

1.SMTP簡單郵件傳輸協議&#xff0c;用于發送電子郵件&#xff0c;默認情況下是明文傳輸&#xff0c;沒有加密機制。 SSL是一種安全協議&#xff0c;對電子郵件進行加密傳輸。 POP3主要用于接收電子郵件 IMAP用于接收電子郵件 2.采用存儲-轉發方式處理信號的設備是交換機 …

DICOM通訊(ACSE->DIMSE->Worklist)

DICOM 通訊協議中的 ACSE → DIMSE → Worklist 這條通訊鏈路。DICOM 通訊棧本身是一個多層的協議結構&#xff0c;就像 OSI 模型一樣&#xff0c;逐層封裝功能。 一、DICOM 通訊協議棧總體架構 DICOM 通訊使用 TCP/IP 建立連接&#xff0c;其上面封裝了多個協議層次&#xf…

優化自旋鎖的實現

在《C11實現一個自旋鎖》介紹了分別使用TAS和CAS算法實現自旋鎖的方案&#xff0c;以及它們的優缺點。TAS算法雖然實現簡單&#xff0c;但是因為每次自旋時都要導致一場內存總線流量風暴&#xff0c;對全局系統影響很大&#xff0c;一般都要對它進行優化&#xff0c;以降低對全…

Excel 中讓表格內容自適應列寬和行高

Excel 中讓表格內容自適應列寬和行高 目錄 Excel 中讓表格內容自適應列寬和行高自適應列寬自適應行高在Excel中讓表格內容自適應列寬和行高,可參考以下操作: 自適應列寬 方法一:手動調整 選中需要調整列寬的列(如果是整個表格,可點擊表格左上角行號和列號交叉處的三角形全…

JWT令牌:實現安全會話跟蹤與登錄認證的利器

摘要&#xff1a;本文深入探討了JWT令牌在實現會話跟蹤和登錄認證方面的應用&#xff0c;詳細介紹了JWT令牌的概念、組成、生成與校驗方法&#xff0c;以及在實際案例中如何通過JWT令牌進行會話跟蹤和登錄認證的具體實現步驟&#xff0c;為系統的安全認證機制提供了全面且深入的…

Mybtis和Mybatis-Plus區別

MyBatis 和 MyBatis-Plus 是 Java 中常用的持久層框架&#xff0c;MyBatis-Plus 是在 MyBatis 基礎上增強的工具包&#xff0c;讓開發更便捷、高效。下面是兩者主要的區別&#xff1a; ? 核心區別總結&#xff1a; 特性MyBatisMyBatis-Plus配置復雜度需要手寫大量 XML 或注解…

JavaScript 性能優化實戰

一、代碼執行效率優化 1. 減少全局變量的使用 全局變量在 JavaScript 中會掛載在全局對象(瀏覽器環境下是window,Node.js 環境下是global)上,頻繁訪問全局變量會增加作用域鏈的查找時間。 // 反例:使用全局變量 var globalVar = example; function someFunction() {con…

學習筆記十六——Rust Monad從頭學

&#x1f9e0; 零基礎也能懂的 Rust Monad&#xff1a;逐步拆解 三大定律通俗講解 實戰技巧 &#x1f4e3; 第一部分&#xff1a;Monad 是什么&#xff1f; Monad 是一種“包值 鏈操作 保持結構”的代碼模式&#xff0c;用來處理帶上下文的值&#xff0c;并方便連續處理。 …

PL/SQL登錄慢,程序連接Oracle 提示無法連接或無監聽

PL/SQL登錄慢&#xff0c;程序連接Oracle 提示無法連接或無監聽 錯誤提示&#xff1a;ORA-12541: TNS: 無監聽程序 的解決辦法&#xff0c; 現象&#xff1a;PL/SQL登錄慢&#xff0c;程序連接Oracle 提示無法連接或無監聽 監聽已經正常開起&#xff0c;但還是PL/SQL登錄慢或…

Windows10,11賬戶管理,修改密碼,創建帳戶...

在這里&#xff0c;我們使用微軟操作系統的一款工具:netplwiz 它可以非常便捷的管理用戶賬戶. 一:修改密碼(無需現在密碼) 01修改注冊表 運行命令&#xff1a;regedit 在地址欄輸入&#xff1a; HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Passwor…

電腦 BIOS 操作指南(Computer BIOS Operation Guide)

電腦 BIOS 操作指南 電腦的BIOS界面&#xff08;應為“BIOS”&#xff09;是一個固件界面&#xff0c;允許用戶配置電腦的硬件設置。 進入BIOS后&#xff0c;你可以進行多種設置&#xff0c;具體包括&#xff1a; 1.啟動配置 啟動順序&#xff1a;設置從哪個設備啟動&#x…

iOS 冷啟動時間監控:啟動起點有哪些選擇?

?? iOS 冷啟動時間監控&#xff1a;啟動起點有哪些選擇&#xff1f; 作者&#xff1a;侯仕奇 來源&#xff1a;sqi.io 在監控 iOS 冷啟動性能時&#xff0c;一個關鍵問題是&#xff1a;如何精確記錄 App 冷啟動的開始時間&#xff1f; 本文將對不同的“冷啟動起點”監控方式…

onlyoffice關閉JWT后依然報錯如何解決?

onlyoffice關閉JWT后依然報錯如何解決&#xff1f; 一、部署方式 我是以docker方式部署的&#xff0c;直接通過環境變量禁用了JWT&#xff0c;命令如下&#xff1a; docker run -d \--name onlyoffice-no-jwt \--restartalways \-p 8069:80 \-e JWT_ENABLEDfalse \onlyoffic…

rk3588 驅動開發(一)字符設備開發

3.字符設備驅動開發 3.1 什么是字符設備驅動 字符設備&#xff1a;就是一個個字節&#xff0c;按照字節流進行讀寫操作的設備&#xff0c;讀寫是按照先后順序的。 舉例子&#xff1a;IIC 按鍵 LED SPI LCD 等 Linux 應用程序調用驅動程序流程&#xff1a; Linux中驅動加載成功…