Files
nanocode/happy-games/games/slither-io/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

1288 lines
50 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Slither.io Clone - Poki SDK Demo</title>
<!-- Poki SDK (async load) -->
<script src="https://game-cdn.poki.com/scripts/v2/poki-sdk.js" async></script>
<script src="https://code.playcanvas.com/playcanvas-1.66.0.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #1a1a2e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
touch-action: none;
}
#application {
width: 100vw;
height: 100vh;
display: block;
}
#ui {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
}
#score {
position: absolute;
top: 20px;
left: 20px;
color: white;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
#length {
position: absolute;
top: 50px;
left: 20px;
color: #aaa;
font-size: 14px;
}
#menu {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: auto;
}
#menu h1 {
color: #00d4ff;
font-size: 48px;
margin-bottom: 20px;
text-shadow: 0 0 20px rgba(0,212,255,0.5);
}
.btn {
padding: 15px 40px;
font-size: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 30px;
cursor: pointer;
margin: 10px;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: scale(1.05);
box-shadow: 0 10px 30px rgba(102,126,234,0.4);
}
#gameOver {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0,0,0,0.8);
padding: 40px;
border-radius: 20px;
pointer-events: auto;
}
#gameOver h2 {
color: #ff6b6b;
font-size: 36px;
margin-bottom: 20px;
}
#finalScore {
color: white;
font-size: 24px;
margin-bottom: 30px;
}
#joystick {
position: absolute;
bottom: 100px;
left: 100px;
width: 120px;
height: 120px;
background: rgba(255,255,255,0.1);
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
display: none;
pointer-events: auto;
}
#joystick-knob {
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
background: rgba(0,212,255,0.6);
border-radius: 50%;
transform: translate(-50%, -50%);
}
@media (max-width: 768px) {
#joystick { display: block; }
}
</style>
</head>
<body>
<canvas id="application" style="display:block;width:100%;height:100%;"></canvas>
<div id="ui">
<div id="score">得分: 0</div>
<div id="length">长度: 10</div>
<div id="menu">
<h1>🐍 Snake IO</h1>
<button class="btn" onclick="startGame()">开始游戏</button>
<button class="btn" onclick="watchAd()">看广告得奖励</button>
<!-- 游戏规则 -->
<div style="margin-top: 30px; padding: 20px; background: rgba(0,0,0,0.6); border-radius: 15px; max-width: 320px; text-align: left;">
<h3 style="color: #00d4ff; margin-bottom: 15px; text-align: center;">📜 游戏规则</h3>
<div style="color: #ccc; font-size: 14px; line-height: 1.8;">
<p style="margin-bottom: 8px;"><span style="color: #4ecdc4;">🎮 控制:</span> 鼠标/手指拖动控制方向</p>
<p style="margin-bottom: 8px;"><span style="color: #ffeaa7;">🍎 吃食物:</span> 吃掉彩色食物变长得分</p>
<p style="margin-bottom: 8px;"><span style="color: #ff6b6b;">⚠️ 注意:</span> 撞到其他蛇身体 → 死亡</p>
<p style="margin-bottom: 8px;"><span style="color: #fd79a8;">💀 头碰头:</span> 双方同时死亡</p>
<p style="margin-bottom: 0;"><span style="color: #a29bfe;">⭐ 提示:</span> 大蛇被击败后会掉落大量食物!</p>
</div>
</div>
</div>
<div id="gameOver">
<h2>游戏结束!</h2>
<div id="finalScore">最终得分: 0</div>
<button class="btn" onclick="restartGame()">再玩一次</button>
</div>
<div id="joystick">
<div id="joystick-knob"></div>
</div>
</div>
<script>
// ==================== 等待页面加载完成 ====================
document.addEventListener('DOMContentLoaded', function() {
initGame();
});
// ==================== Poki SDK 集成 ====================
let pokiReady = false;
function initPoki() {
// 初始化 Poki SDK(包装在 try-catch 中防止出错)
if (typeof PokiSDK !== 'undefined') {
return PokiSDK.init().then(() => {
console.log('✅ Poki SDK 初始化成功');
pokiReady = true;
PokiSDK.gameLoadingStart();
// 模拟加载完成后通知 Poki
setTimeout(() => {
PokiSDK.gameLoadingFinished();
}, 1000);
}).catch(err => {
console.log('⚠️ Poki SDK 初始化失败(非 Poki 环境):', err);
pokiReady = false;
});
} else {
console.log('⚠️ Poki SDK 未加载(正常,非 Poki 环境)');
return Promise.resolve();
}
}
// 广告功能
window.watchAd = function() {
if (!pokiReady) {
alert('广告功能仅在 Poki 平台可用');
return;
}
PokiSDK.commercialBreak().then(() => {
console.log('广告结束,给予奖励');
// 给玩家奖励
if (snake) {
snake.grow(5);
window.updateUI();
}
alert('观看完成!获得 +5 长度奖励');
});
};
// 游戏暂停/恢复(广告用)
function pauseForAd() {
if (gameRunning) {
PokiSDK.gameplayStop();
}
}
function resumeAfterAd() {
if (gameRunning) {
PokiSDK.gameplayStart();
}
}
// ==================== PlayCanvas 游戏 ====================
function initGame() {
console.log('🎮 初始化 PlayCanvas...');
const canvas = document.getElementById('application');
if (!canvas) {
console.error('❌ Canvas 元素未找到!');
alert('游戏加载失败:Canvas 未找到');
return;
}
let app;
try {
app = new pc.Application(canvas, {
mouse: new pc.Mouse(canvas),
keyboard: new pc.Keyboard(window),
touch: new pc.TouchDevice(canvas),
elementInput: new pc.ElementInput(canvas)
});
console.log('✅ PlayCanvas 应用创建成功');
} catch (e) {
console.error('❌ PlayCanvas 初始化失败:', e);
alert('游戏引擎初始化失败: ' + e.message);
throw e;
}
app.start();
console.log('✅ PlayCanvas 已启动');
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
window.addEventListener('resize', () => app.resizeCanvas());
// 游戏状态
let gameRunning = false;
let score = 0;
let playerSnake = null; // 人类玩家
let aiSnakes = []; // AI玩家数组
let maxAIPlayers = 8; // 最大AI数量(增加到8个)
let foods = [];
let camera = null;
let maxFoods = 60; // 最大食物数量(增加到60个)
// 音频系统 - Web Audio API
let audioContext = null;
let soundEnabled = true;
function initAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
// 播放音效函数
function playSound(type) {
if (!soundEnabled || !audioContext) return;
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
osc.connect(gain);
gain.connect(audioContext.destination);
const now = audioContext.currentTime;
switch(type) {
case 'eat': // 吃食物 - 清脆的"叮"声
osc.type = 'sine';
osc.frequency.setValueAtTime(880, now); // A5
osc.frequency.exponentialRampToValueAtTime(440, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
break;
case 'eatBig': // 吃大食物 - 更饱满的音效
osc.type = 'triangle';
osc.frequency.setValueAtTime(660, now); // E5
osc.frequency.exponentialRampToValueAtTime(330, now + 0.2);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
break;
case 'die': // 死亡 - 低沉的"咚"声
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.3);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
break;
case 'headBump': // 头碰头 - 撞击声
osc.type = 'square';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.15);
gain.gain.setValueAtTime(0.5, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
break;
}
}
// 场景设置
function setupScene() {
// 相机
camera = new pc.Entity('Camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.08, 0.08, 0.12),
orthographic: true,
orthoHeight: 12,
nearClip: 0.1,
farClip: 100
});
camera.setPosition(0, 8, 0);
camera.setEulerAngles(-90, 0, 0); // 直接向下看
app.root.addChild(camera);
console.log('📷 相机创建完成,位置:', camera.getPosition().toString());
// 灯光
const light = new pc.Entity('Light');
light.addComponent('light', {
type: 'directional',
color: new pc.Color(1, 1, 0.95),
intensity: 1
});
light.setEulerAngles(45, 45, 0);
app.root.addChild(light);
// 环境光
const ambient = new pc.Entity('Ambient');
ambient.addComponent('light', {
type: 'point',
color: new pc.Color(0.3, 0.3, 0.4),
intensity: 0.5,
range: 50
});
ambient.setPosition(0, 5, 0);
app.root.addChild(ambient);
// 地面
const ground = new pc.Entity('Ground');
ground.addComponent('render', { type: 'plane' });
ground.setLocalScale(40, 1, 40);
ground.setPosition(0, 0, 0);
const groundMat = new pc.StandardMaterial();
groundMat.diffuse = new pc.Color(0.15, 0.18, 0.22);
groundMat.update();
ground.render.material = groundMat;
app.root.addChild(ground);
console.log('🟫 地面创建完成');
// 网格装饰
createGrid();
}
function createGrid() {
const gridSize = 20;
const gridSpacing = 2;
for (let x = -gridSize; x <= gridSize; x += gridSpacing) {
const line = new pc.Entity();
line.addComponent('render', { type: 'box' });
line.setLocalScale(0.02, 0.01, gridSize * 2);
line.setPosition(x, 0, 0);
const mat = new pc.StandardMaterial();
mat.diffuse = new pc.Color(0.2, 0.25, 0.3);
mat.update();
line.render.material = mat;
app.root.addChild(line);
}
for (let z = -gridSize; z <= gridSize; z += gridSpacing) {
const line = new pc.Entity();
line.addComponent('render', { type: 'box' });
line.setLocalScale(gridSize * 2, 0.01, 0.02);
line.setPosition(0, 0, z);
const mat = new pc.StandardMaterial();
mat.diffuse = new pc.Color(0.2, 0.25, 0.3);
mat.update();
line.render.material = mat;
app.root.addChild(line);
}
}
// 蛇类
class Snake {
constructor(isAI = false, startPos = null, snakeColor = null) {
this.segments = [];
this.history = [];
this.isAI = isAI;
this.isAlive = true;
this.speed = isAI ? 3.2 : 3.5; // 降低初始速度
this.baseRadius = 0.22; // 基础半径(稍微小一点)
this.radius = this.baseRadius; // 当前半径(会变胖)
this.maxRadius = 0.6; // 最大半径限制
this.segmentDistance = 0.32; // 段间距(稍微紧凑)
this.totalEaten = 0; // 总共吃了多少食物(用于计算变胖)
// 人类玩家用青色,AI用不同颜色(增加更多颜色)
if (snakeColor) {
this.color = snakeColor;
} else if (isAI) {
// AI颜色:更多颜色选择
const aiColors = [
new pc.Color(1, 0.3, 0.3), // 红色
new pc.Color(1, 0.8, 0.2), // 黄色
new pc.Color(0.8, 0.3, 1), // 紫色
new pc.Color(0.3, 0.8, 0.3), // 绿色
new pc.Color(1, 0.5, 0.2), // 橙色
new pc.Color(0.3, 0.5, 1), // 蓝色
new pc.Color(1, 0.3, 0.6), // 粉红
new pc.Color(0.6, 0.3, 0.1) // 棕色
];
this.color = aiColors[Math.floor(Math.random() * aiColors.length)];
} else {
this.color = new pc.Color(0, 0.8, 1); // 人类玩家:青色
}
this.direction = new pc.Vec3(1, 0, 0);
this.targetDirection = new pc.Vec3(1, 0, 0);
this.angle = 0;
this.invulnerableTime = 2.0;
this.startPos = startPos || new pc.Vec3(0, this.radius, 0);
this.createHead();
this.createInitialBody(10);
}
createHead() {
this.head = new pc.Entity(this.isAI ? 'AI_SnakeHead' : 'Player_SnakeHead');
this.head.addComponent('render', { type: 'sphere' });
this.head.setLocalScale(this.radius * 2, this.radius * 2, this.radius * 2);
this.head.setPosition(this.startPos.x, this.startPos.y, this.startPos.z);
const mat = new pc.StandardMaterial();
mat.diffuse = this.color;
mat.shininess = 80;
mat.emissive = new pc.Color(this.color.r * 0.5, this.color.g * 0.5, this.color.b * 0.5);
mat.update();
this.head.render.material = mat;
// 眼睛
this.createEyes();
app.root.addChild(this.head);
if (!this.isAI) {
console.log('🎯 玩家蛇头创建完成,位置:', this.head.getPosition().toString());
}
}
createEyes() {
const leftEye = new pc.Entity('LeftEye');
leftEye.addComponent('render', { type: 'sphere' });
leftEye.setLocalScale(0.1, 0.1, 0.1);
leftEye.setPosition(-0.15, 0.1, 0.15);
const eyeMat = new pc.StandardMaterial();
eyeMat.diffuse = new pc.Color(1, 1, 1);
eyeMat.update();
leftEye.render.material = eyeMat;
this.head.addChild(leftEye);
const rightEye = new pc.Entity('RightEye');
rightEye.addComponent('render', { type: 'sphere' });
rightEye.setLocalScale(0.1, 0.1, 0.1);
rightEye.setPosition(0.15, 0.1, 0.15);
rightEye.render.material = eyeMat;
this.head.addChild(rightEye);
}
createInitialBody(count) {
// 创建初始身体,确保不会与头部重叠
const headPos = this.head.getPosition();
for (let i = 0; i < count; i++) {
const segment = new pc.Entity('Segment' + i);
segment.addComponent('render', { type: 'sphere' });
const scale = this.radius * 1.8 * (1 - i * 0.02);
segment.setLocalScale(scale, scale, scale);
// 渐变色
const t = i / 50;
const segColor = new pc.Color(
this.color.r * (1 - t * 0.5),
this.color.g * (1 - t * 0.3),
this.color.b
);
const mat = new pc.StandardMaterial();
mat.diffuse = segColor;
mat.shininess = 60;
mat.update();
segment.render.material = mat;
// 沿 X 轴负方向排开,确保不与头部重叠
const xOffset = -(i + 1) * this.segmentDistance * 2;
segment.setPosition(headPos.x + xOffset, this.radius, headPos.z);
app.root.addChild(segment);
this.segments.push(segment);
}
// 预填充历史位置
this.prepopulateHistory();
}
prepopulateHistory() {
const headPos = this.head.getPosition();
// 预填充足够多的历史位置
const totalNeeded = this.segments.length * 4 + 20;
for (let i = 0; i < totalNeeded; i++) {
const pos = headPos.clone();
// 沿 X 轴负方向延伸,确保身体段跟随头部
pos.x -= i * (this.segmentDistance / 4);
this.history.push(pos);
}
}
grow(count = 1) {
for (let i = 0; i < count; i++) {
const segment = new pc.Entity('Segment' + this.segments.length);
segment.addComponent('render', { type: 'sphere' });
const idx = this.segments.length;
const scale = this.radius * 1.8 * (1 - idx * 0.02);
segment.setLocalScale(scale, scale, scale);
// 渐变色
const t = idx / 50;
const segColor = new pc.Color(
this.color.r * (1 - t * 0.5),
this.color.g * (1 - t * 0.3),
this.color.b
);
const mat = new pc.StandardMaterial();
mat.diffuse = segColor;
mat.shininess = 60;
mat.update();
segment.render.material = mat;
// 在最后一个段后面添加
const lastSeg = this.segments[this.segments.length - 1];
const lastPos = lastSeg ? lastSeg.getPosition() : this.head.getPosition();
segment.setPosition(lastPos.x - this.segmentDistance, this.radius, lastPos.z);
app.root.addChild(segment);
this.segments.push(segment);
}
}
// 更新所有身体段的粗细(变胖效果)
updateThickness() {
// 头部
this.head.setLocalScale(this.radius * 2, this.radius * 2, this.radius * 2);
// 身体段
for (let i = 0; i < this.segments.length; i++) {
const scale = this.radius * 1.8 * (1 - i * 0.015);
this.segments[i].setLocalScale(scale, scale, scale);
// 更新Y位置(贴地)
const pos = this.segments[i].getPosition();
pos.y = this.radius;
this.segments[i].setPosition(pos);
}
}
// AI寻找最近的食物
findNearestFood() {
const headPos = this.head.getPosition();
let nearest = null;
let minDist = Infinity;
for (let food of foods) {
const dist = headPos.distance(food.getPosition());
if (dist < minDist) {
minDist = dist;
nearest = food;
}
}
return nearest;
}
// AI更新
aiUpdate(dt) {
if (!gameRunning || !this.isAlive) return;
// 减少无敌时间
if (this.invulnerableTime > 0) {
this.invulnerableTime -= dt;
}
const headPos = this.head.getPosition();
// 寻找最近的食物作为目标
const nearestFood = this.findNearestFood();
let targetPos = null;
if (nearestFood) {
targetPos = nearestFood.getPosition();
} else {
// 没有食物就随机游走
targetPos = new pc.Vec3(
headPos.x + this.direction.x * 5,
0,
headPos.z + this.direction.z * 5
);
}
// 简单的边界避让
if (headPos.x > 17) targetPos.x = headPos.x - 5;
if (headPos.x < -17) targetPos.x = headPos.x + 5;
if (headPos.z > 17) targetPos.z = headPos.z - 5;
if (headPos.z < -17) targetPos.z = headPos.z + 5;
// 计算目标方向
const toTarget = new pc.Vec3().sub2(targetPos, headPos);
toTarget.y = 0;
if (toTarget.length() > 0.1) {
toTarget.normalize();
this.direction.lerp(this.direction, toTarget, 2 * dt);
this.direction.normalize();
}
// 移动头部
const move = this.direction.clone().scale(this.speed * dt);
headPos.add(move);
// 边界限制
headPos.x = pc.math.clamp(headPos.x, -19, 19);
headPos.z = pc.math.clamp(headPos.z, -19, 19);
this.head.setPosition(headPos);
// 朝向
const angle = Math.atan2(this.direction.x, this.direction.z) * pc.math.RAD_TO_DEG;
this.head.setEulerAngles(0, angle, 0);
// 记录历史位置
this.history.unshift(headPos.clone());
if (this.history.length > this.segments.length * 4 + 20) {
this.history.pop();
}
// 更新身体段
for (let i = 0; i < this.segments.length; i++) {
const idx = Math.min((i + 1) * 4, this.history.length - 1);
if (this.history[idx]) {
const targetPos = this.history[idx];
const currentPos = this.segments[i].getPosition();
currentPos.lerp(currentPos, targetPos, 0.3);
this.segments[i].setPosition(currentPos);
}
}
}
update(dt, targetPos) {
if (!gameRunning || !this.isAlive) return;
// AI使用自己的更新逻辑
if (this.isAI) {
this.aiUpdate(dt);
return;
}
// 减少无敌时间
if (this.invulnerableTime > 0) {
this.invulnerableTime -= dt;
}
// 计算目标方向
const headPos = this.head.getPosition();
const toTarget = new pc.Vec3().sub2(targetPos, headPos);
toTarget.y = 0;
if (toTarget.length() > 0.1) {
toTarget.normalize();
// 平滑转向
this.direction.lerp(this.direction, toTarget, 3 * dt);
this.direction.normalize();
}
// 移动头部
const move = this.direction.clone().scale(this.speed * dt);
headPos.add(move);
// 边界限制
headPos.x = pc.math.clamp(headPos.x, -19, 19);
headPos.z = pc.math.clamp(headPos.z, -19, 19);
this.head.setPosition(headPos);
// 朝向
const angle = Math.atan2(this.direction.x, this.direction.z) * pc.math.RAD_TO_DEG;
this.head.setEulerAngles(0, angle, 0);
// 记录历史位置
this.history.unshift(headPos.clone());
if (this.history.length > this.segments.length * 4 + 20) {
this.history.pop();
}
// 更新身体段 - 使用更平滑的跟随
for (let i = 0; i < this.segments.length; i++) {
const idx = Math.min((i + 1) * 4, this.history.length - 1);
if (this.history[idx]) {
const targetPos = this.history[idx];
const currentPos = this.segments[i].getPosition();
// 平滑插值
currentPos.lerp(currentPos, targetPos, 0.3);
this.segments[i].setPosition(currentPos);
}
}
// 相机跟随(只有玩家蛇控制相机)
if (camera && !this.isAI) {
const camPos = camera.getPosition();
camPos.x += (headPos.x - camPos.x) * 2 * dt;
camPos.z += (headPos.z - camPos.z) * 2 * dt;
camera.setPosition(camPos);
}
}
checkCollision(allSnakes) {
if (!this.isAlive) return;
const headPos = this.head.getPosition();
// 检查食物碰撞
for (let i = foods.length - 1; i >= 0; i--) {
const food = foods[i];
const foodPos = food.getPosition();
const foodScale = food.getLocalScale().x;
const dist = headPos.distance(foodPos);
// 食物大小影响碰撞距离
if (dist < this.radius + foodScale * 0.3) {
this.eatFood(i, food.foodValue);
}
}
// 无敌时间内不检测碰撞
if (this.invulnerableTime > 0) {
return;
}
// 检查与其他蛇的碰撞
for (let otherSnake of allSnakes) {
if (!otherSnake.isAlive) continue;
const otherHeadPos = otherSnake.head.getPosition();
// 头部碰撞检测(双方都死)
if (otherSnake !== this) {
const headDist = headPos.distance(otherHeadPos);
if (headDist < this.radius + otherSnake.radius) {
console.log('💥 头碰头!双方死亡');
// 播放头碰头音效(只给人类玩家播放,避免混音太乱)
if (!this.isAI || !otherSnake.isAI) {
playSound('headBump');
}
this.die();
otherSnake.die();
if (!this.isAI) window.gameOver();
return;
}
}
// 身体碰撞检测
// 检查对方身体(如果是自己,跳过前70%)
const startIdx = (otherSnake === this) ? Math.floor(otherSnake.segments.length * 0.7) : 0;
for (let i = startIdx; i < otherSnake.segments.length; i++) {
const segPos = otherSnake.segments[i].getPosition();
const dist = headPos.distance(segPos);
if (dist < this.radius * 0.7) {
if (otherSnake === this) {
console.log('💥 撞到自己!');
} else {
console.log('💥 撞到其他蛇!');
}
// 播放死亡音效(只给人类玩家)
if (!this.isAI) {
playSound('die');
}
this.die();
if (!this.isAI) window.gameOver();
return;
}
}
}
}
die() {
this.isAlive = false;
// 死亡效果:变暗
const mat = this.head.render.material;
mat.emissive = new pc.Color(0.1, 0.1, 0.1);
mat.update();
}
eatFood(index, value = 10) {
const food = foods[index];
app.root.removeChild(food);
foods.splice(index, 1);
// 更新总吃食物计数
this.totalEaten += value;
// 增长长度:比以前慢(每20分增加1段,而不是每10分)
const growth = Math.max(1, Math.floor(value / 20));
this.grow(growth);
// 增长速度变慢:每100分才增加0.1速度
this.speed += 0.01;
// 变胖效果:每吃100分,半径增加0.02,但有最大值限制
// 使用对数增长,避免太快变胖
const newRadius = this.baseRadius + Math.log(this.totalEaten / 50 + 1) * 0.05;
this.radius = Math.min(newRadius, this.maxRadius);
this.updateThickness();
// 播放吃食物音效(人类玩家吃大/小食物不同音效,AI简单音效)
if (!this.isAI) {
if (value >= 20) {
playSound('eatBig');
} else {
playSound('eat');
}
score += value;
window.updateUI();
}
// 生成新食物保持数量
spawnFood();
}
destroy() {
app.root.removeChild(this.head);
this.segments.forEach(seg => app.root.removeChild(seg));
}
}
// 食物生成 - 分大中小三种
function spawnFood() {
const food = new pc.Entity('Food' + foods.length);
food.addComponent('render', { type: 'sphere' });
// 随机决定食物大小:70%小、25%中、5%大
const rand = Math.random();
let scale, value, color;
if (rand < 0.05) {
// 大食物:5%概率,10分,橙色
scale = 0.8;
value = 30;
color = new pc.Color(1, 0.6, 0.1); // 橙色
} else if (rand < 0.3) {
// 中食物:25%概率,5分,粉色
scale = 0.6;
value = 20;
color = new pc.Color(1, 0.4, 0.7); // 粉色
} else {
// 小食物:70%概率,1分,随机颜色
scale = 0.4;
value = 10;
const colors = [
new pc.Color(1, 0.3, 0.3), // 红
new pc.Color(0.3, 1, 0.3), // 绿
new pc.Color(1, 1, 0.3), // 黄
new pc.Color(0.3, 1, 1) // 青
];
color = colors[Math.floor(Math.random() * colors.length)];
}
food.setLocalScale(scale, scale, scale);
food.foodValue = value;
// 在玩家周围生成食物,但不要太近
let x, z;
if (playerSnake && playerSnake.isAlive) {
const playerPos = playerSnake.head.getPosition();
// 在玩家周围15-30单位范围内生成
const angle = Math.random() * Math.PI * 2;
const dist = 15 + Math.random() * 15;
x = playerPos.x + Math.cos(angle) * dist;
z = playerPos.z + Math.sin(angle) * dist;
} else {
x = (Math.random() - 0.5) * 36;
z = (Math.random() - 0.5) * 36;
}
// 限制在地图范围内
x = pc.math.clamp(x, -19, 19);
z = pc.math.clamp(z, -19, 19);
food.setPosition(x, scale * 0.3, z);
const mat = new pc.StandardMaterial();
mat.diffuse = color;
mat.emissive = new pc.Color(color.r * 0.4, color.g * 0.4, color.b * 0.4);
mat.update();
food.render.material = mat;
// 浮动动画
food.floatOffset = Math.random() * Math.PI * 2;
app.root.addChild(food);
foods.push(food);
}
function updateFoods(dt) {
foods.forEach(food => {
const pos = food.getPosition();
pos.y = 0.2 + Math.sin(Date.now() * 0.003 + food.floatOffset) * 0.1;
food.setPosition(pos);
food.rotate(0, 60 * dt, 0);
});
}
// 输入处理
let targetPosition = new pc.Vec3(0, 0, 5);
let useJoystick = false;
let joystickCenter = { x: 0, y: 0 };
let joystickActive = false;
function setupInput() {
// 鼠标输入
app.mouse.on(pc.EVENT_MOUSEMOVE, (event) => {
if (!gameRunning || useJoystick) return;
const from = camera.camera.screenToWorld(event.x, event.y, camera.camera.nearClip);
const to = camera.camera.screenToWorld(event.x, event.y, camera.camera.farClip);
// 射线与地面交点
const dir = new pc.Vec3().sub2(to, from);
const t = -from.y / dir.y;
if (t > 0) {
targetPosition.set(
from.x + dir.x * t,
0,
from.z + dir.z * t
);
}
});
// 触摸/摇杆输入
const joystick = document.getElementById('joystick');
const knob = document.getElementById('joystick-knob');
joystick.addEventListener('touchstart', (e) => {
e.preventDefault();
useJoystick = true;
joystickActive = true;
const touch = e.touches[0];
const rect = joystick.getBoundingClientRect();
joystickCenter = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
});
joystick.addEventListener('touchmove', (e) => {
e.preventDefault();
if (!joystickActive) return;
const touch = e.touches[0];
const dx = touch.clientX - joystickCenter.x;
const dy = touch.clientY - joystickCenter.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const maxDist = 35;
const clampedDist = Math.min(dist, maxDist);
const angle = Math.atan2(dy, dx);
knob.style.transform = `translate(calc(-50% + ${Math.cos(angle) * clampedDist}px), calc(-50% + ${Math.sin(angle) * clampedDist}px))`;
// 设置目标方向
if (playerSnake) {
const headPos = playerSnake.head.getPosition();
targetPosition.set(
headPos.x + Math.cos(angle) * 10,
0,
headPos.z + Math.sin(angle) * 10
);
}
});
joystick.addEventListener('touchend', () => {
joystickActive = false;
knob.style.transform = 'translate(-50%, -50%)';
});
}
// 创建AI玩家
function spawnAIPlayers() {
// 清理已死亡的AI
aiSnakes = aiSnakes.filter(ai => {
if (!ai.isAlive) {
ai.destroy();
return false;
}
return true;
});
// 补充AI到最大数量
let attempts = 0;
while (aiSnakes.length < maxAIPlayers && attempts < 10) {
attempts++;
// 随机位置,避免与玩家太近,也避免与其他AI太近
let x, z;
let validPosition = false;
let tryCount = 0;
while (!validPosition && tryCount < 20) {
tryCount++;
if (playerSnake && playerSnake.isAlive) {
const playerPos = playerSnake.head.getPosition();
const angle = Math.random() * Math.PI * 2;
const dist = 12 + Math.random() * 15;
x = playerPos.x + Math.cos(angle) * dist;
z = playerPos.z + Math.sin(angle) * dist;
} else {
x = (Math.random() - 0.5) * 35;
z = (Math.random() - 0.5) * 35;
}
x = pc.math.clamp(x, -18, 18);
z = pc.math.clamp(z, -18, 18);
// 检查是否与其他AI太近
validPosition = true;
const newPos = new pc.Vec3(x, 0, z);
for (let ai of aiSnakes) {
if (ai.isAlive) {
const dist = newPos.distance(ai.head.getPosition());
if (dist < 5) {
validPosition = false;
break;
}
}
}
}
// 只在找到有效位置时才创建AI
if (validPosition) {
const ai = new Snake(true, new pc.Vec3(x, 0.25, z));
aiSnakes.push(ai);
console.log('🤖 AI玩家生成,位置:', x.toFixed(1), z.toFixed(1));
}
}
}
// 游戏控制(定义为全局函数供HTML调用)
window.startGame = function() {
console.log('🎮 开始多人对战游戏...');
// 初始化音频(需要用户交互后才能播放)
initAudio();
try {
document.getElementById('menu').style.display = 'none';
gameRunning = true;
score = 0;
// 清理旧游戏
if (playerSnake) playerSnake.destroy();
aiSnakes.forEach(ai => ai.destroy());
aiSnakes = [];
foods.forEach(f => {
if (f && f.parent) app.root.removeChild(f);
});
foods = [];
// 重置目标位置
targetPosition.set(5, 0, 0);
// 创建玩家蛇(青色)
console.log('🐍 创建玩家...');
playerSnake = new Snake(false);
console.log('✅ 玩家创建成功,长度:', playerSnake.segments.length);
// 创建AI玩家
console.log('🤖 创建AI玩家...');
spawnAIPlayers();
// 相机聚焦到玩家
if (camera && playerSnake) {
const headPos = playerSnake.head.getPosition();
camera.setPosition(headPos.x, 8, headPos.z);
}
// 生成食物
console.log('🍎 生成食物...');
for (let i = 0; i < maxFoods; i++) {
spawnFood();
}
console.log('✅ 生成', foods.length, '个食物');
updateUI();
console.log('✅ 游戏启动完成!');
console.log('🎮 玩法:移动鼠标控制蛇,吃食物变大,撞其他蛇身体会死,头碰头双方死');
// 通知 Poki 游戏开始
if (pokiReady && typeof PokiSDK !== 'undefined') {
PokiSDK.gameplayStart();
}
} catch (e) {
console.error('❌ 开始游戏失败:', e);
console.error(e.stack);
alert('游戏启动失败: ' + e.message);
}
};
window.showGameOver = function() {
document.getElementById('gameOver').style.display = 'block';
document.getElementById('finalScore').textContent = '最终得分: ' + score;
};
window.gameOver = function() {
if (!gameRunning) return; // 防止重复调用
console.log('💀 游戏结束!得分:', score);
gameRunning = false;
// 通知 Poki 游戏结束
if (pokiReady && typeof PokiSDK !== 'undefined') {
try {
PokiSDK.gameplayStop();
} catch(e) {}
}
// 显示广告
if (pokiReady && typeof PokiSDK !== 'undefined') {
PokiSDK.commercialBreak().then(() => {
window.showGameOver();
}).catch(() => {
window.showGameOver();
});
} else {
window.showGameOver();
}
};
window.restartGame = function() {
document.getElementById('gameOver').style.display = 'none';
window.startGame();
};
window.updateUI = function() {
if (playerSnake && playerSnake.isAlive) {
const thickness = Math.round((playerSnake.radius / playerSnake.baseRadius - 1) * 100);
document.getElementById('score').textContent = '得分: ' + score;
document.getElementById('length').textContent = '长度: ' + playerSnake.segments.length + ' | 胖瘦: +' + thickness + '%';
} else {
document.getElementById('score').textContent = '得分: ' + score;
document.getElementById('length').textContent = '长度: 0 | 胖瘦: 0%';
}
};
// 游戏循环
try {
setupScene();
console.log('✅ 场景设置完成');
setupInput();
console.log('✅ 输入设置完成');
} catch (e) {
console.error('❌ 场景初始化失败:', e);
}
// 调试计数器
let updateCount = 0;
let lastDebugTime = 0;
let lastAISpawnTime = 0;
app.on('update', (dt) => {
try {
if (gameRunning) {
// 获取所有活着的蛇
const allSnakes = [];
if (playerSnake && playerSnake.isAlive) allSnakes.push(playerSnake);
aiSnakes.forEach(ai => { if (ai.isAlive) allSnakes.push(ai); });
// 更新玩家
if (playerSnake && playerSnake.isAlive) {
playerSnake.update(dt, targetPosition);
playerSnake.checkCollision(allSnakes);
}
// 更新AI
aiSnakes.forEach(ai => {
if (ai.isAlive) {
ai.update(dt, null); // AI自己找目标
ai.checkCollision(allSnakes);
}
});
// 更新食物动画
updateFoods(dt);
// 定期补充AI(每3秒检查一次)
const now = Date.now();
if (now - lastAISpawnTime > 3000) {
spawnAIPlayers();
lastAISpawnTime = now;
}
// 保持食物数量
while (foods.length < maxFoods) {
spawnFood();
}
// 调试信息
updateCount++;
if (now - lastDebugTime > 5000) {
const aliveAI = aiSnakes.filter(ai => ai.isAlive).length;
console.log('🔄 游戏状态 - 食物:', foods.length, 'AI存活:', aliveAI, 'Update:', updateCount);
lastDebugTime = now;
updateCount = 0;
}
}
} catch (e) {
console.error('❌ 游戏循环错误:', e);
console.error(e.stack);
}
});
console.log('🐍 Snake IO 游戏已加载!');
console.log('Poki SDK 状态:', pokiReady ? '✅ 已就绪' : '⚠️ 未就绪');
}
// 启动游戏初始化
initPoki().then(() => {
console.log('🎮 开始初始化游戏...');
});
</script>
<!-- 登录认证 -->
<script src="../auth.js?v=1"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
GAME_AUTH.init('slither-io', '贪吃蛇');
});
</script>
</body>
</html>