效果图
1. 项目概述
本文将详细介绍如何使用TRAE工具(Trae IDE)从空文件开始,一步步构建一个完整的Android原生贪吃蛇游戏。我们将实现经典贪吃蛇游戏的核心功能,并添加商业化升级特性,最终打造一个具有现代感和良好用户体验的游戏应用。
2. 开发环境准备
2.1 工具要求
- TRAE IDE:最新版本,支持Android项目开发
- JDK:Java Development Kit 8或更高版本
- Android SDK:包含最新的Android平台和构建工具
2.2 项目初始化
首先,我们需要在TRAE IDE中创建一个新的Android项目。选择"Empty Activity"模板,设置项目名称为"SnakeGame",包名为"com.example.snake"。
3. 项目结构搭建
3.1 目录结构设计
我们将采用模块化的项目结构,便于代码管理和功能扩展:
SnakeGame/
├── app/
│ ├── src/main/
│ │ ├── java/com/example/snake/
│ │ │ ├── core/ # 核心游戏逻辑
│ │ │ ├── effects/ # 视觉效果
│ │ │ ├── managers/ # 管理器类
│ │ │ ├── models/ # 数据模型
│ │ │ ├── utils/ # 工具类
│ │ │ ├── MainActivity.java # 主活动
│ │ │ └── SnakeView.java # 游戏视图
│ │ └── res/ # 资源文件
│ └── build.gradle # 应用级构建配置
└── build.gradle # 项目级构建配置
3.2 基础配置
修改build.gradle文件,添加必要的依赖:
// app/build.gradle
plugins {
id 'com.android.application'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.example.snake"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
// MMKV 存储库
implementation 'com.tencent:mmkv:1.3.1'
}
4. 核心功能实现
4.1 游戏常量定义
创建utils/Constants.java文件,集中管理游戏相关的常量:
package com.example.snake.utils;
/**
* 游戏常量类
* 集中管理所有游戏相关的常量
*/
public class Constants {
// ==================== 游戏速度相关 ====================
public static final int SPEED_EASY = 250; // 简单难度速度(毫秒)
public static final int SPEED_NORMAL = 200; // 普通难度速度(毫秒)
public static final int SPEED_HARD = 150; // 困难难度速度(毫秒)
public static final int SPEED_INSANE = 100; // 疯狂难度速度(毫秒)
public static final int SPEED_MIN = 60; // 最小速度(毫秒)
public static final int SPEED_DECREMENT = 5; // 每增加3节身体速度减少量(毫秒)
// ==================== 分数相关 ====================
public static final int SCORE_FOOD = 10; // 普通食物分数
public static final int SCORE_BONUS = 25; // bonus食物分数
public static final int SCORE_GOLDEN = 50; // 金色食物分数
// ==================== 游戏模式相关 ====================
public static final int TIME_CHALLENGE_30 = 30; // 30秒挑战
public static final int TIME_CHALLENGE_60 = 60; // 60秒挑战
public static final int TIME_CHALLENGE_120 = 120; // 120秒挑战
// ==================== 食物生成相关 ====================
public static final int FOOD_SPAWN_INTERVAL = 5; // 食物生成间隔(游戏tick)
public static final int FOOD_LIFETIME = 10000; // 食物生命周期(毫秒)
public static final int BONUS_FOOD_CHANCE = 15; // bonus食物生成概率(%)
public static final int GOLDEN_FOOD_CHANCE = 5; // 金色食物生成概率(%)
public static final int POWER_UP_CHANCE = 8; // 道具生成概率(%)
// ==================== 道具相关 ====================
public static final int POWER_UP_DURATION = 8000; // 道具持续时间(毫秒)
public static final float SLOW_DOWN_FACTOR = 0.5f; // 减速道具效果
// ==================== 动画相关 ====================
public static final int CRASH_EFFECT_DURATION = 1000; // 碰撞特效持续时间(毫秒)
}
4.2 游戏配置模型
创建models/GameConfig.java文件,定义游戏的难度和模式:
package com.example.snake.models;
import com.example.snake.utils.Constants;
/**
* 游戏配置类
* 包含难度、模式和皮肤配置
*/
public class GameConfig {
// ==================== 难度枚举 ====================
public enum Difficulty {
EASY(Constants.SPEED_EASY, false), // 简单难度,不支持穿墙
NORMAL(Constants.SPEED_NORMAL, false), // 普通难度,不支持穿墙
HARD(Constants.SPEED_HARD, false), // 困难难度,不支持穿墙
INSANE(Constants.SPEED_INSANE, false); // 疯狂难度,不支持穿墙
private final int baseSpeed; // 基础速度(毫秒)
private final boolean wrapAround; // 是否支持穿墙
Difficulty(int baseSpeed, boolean wrapAround) {
this.baseSpeed = baseSpeed;
this.wrapAround = wrapAround;
}
public int getBaseSpeed() {
return baseSpeed;
}
public boolean isWrapAround() {
return wrapAround;
}
}
// ==================== 游戏模式枚举 ====================
public enum GameMode {
ENDLESS(0), // 无尽模式
TIME_CHALLENGE(30); // 时间挑战模式,默认30秒
private final int defaultTimeLimit; // 默认时间限制(秒)
GameMode(int defaultTimeLimit) {
this.defaultTimeLimit = defaultTimeLimit;
}
public int getDefaultTimeLimit() {
return defaultTimeLimit;
}
}
// ==================== 皮肤ID枚举 ====================
public enum SkinId {
NEON_GREEN, // 霓虹绿(默认风格)
FIRE, // 火焰(红橙渐变)
ICE, // 冰雪(蓝白冷色)
GOLD, // 金色
CLASSIC // 经典(绿底黑格)
}
// ==================== 配置属性 ====================
private final Difficulty difficulty;
private final GameMode gameMode;
private final int timeLimit; // 时间挑战模式的时间限制(秒)
private final SkinId skinId;
// ==================== 构造函数 ====================
private GameConfig(Builder builder) {
this.difficulty = builder.difficulty;
this.gameMode = builder.gameMode;
this.timeLimit = builder.timeLimit;
this.skinId = builder.skinId;
}
// ==================== Getter方法 ====================
public Difficulty getDifficulty() {
return difficulty;
}
public GameMode getGameMode() {
return gameMode;
}
public int getTimeLimit() {
return timeLimit;
}
public SkinId getSkinId() {
return skinId;
}
// ==================== Builder类 ====================
public static class Builder {
private Difficulty difficulty = Difficulty.NORMAL;
private GameMode gameMode = GameMode.ENDLESS;
private int timeLimit = GameMode.TIME_CHALLENGE.getDefaultTimeLimit();
private SkinId skinId = SkinId.NEON_GREEN;
public Builder setDifficulty(Difficulty difficulty) {
this.difficulty = difficulty;
return this;
}
public Builder setGameMode(GameMode gameMode) {
this.gameMode = gameMode;
return this;
}
public Builder setTimeLimit(int timeLimit) {
this.timeLimit = timeLimit;
return this;
}
public Builder setSkinId(SkinId skinId) {
this.skinId = skinId;
return this;
}
public GameConfig build() {
return new GameConfig(this);
}
}
// ==================== 静态方法 ====================
public static GameConfig getDefault() {
return new Builder().build();
}
}
4.3 食物模型
创建models/FoodItem.java文件,定义食物的类型和属性:
package com.example.snake.models;
import android.graphics.Rect;
import android.graphics.Color;
import com.example.snake.utils.Constants;
/**
* 食物项类
* 定义游戏中的食物类型和属性
*/
public class FoodItem {
// ==================== 食物类型枚举 ====================
public enum FoodType {
NORMAL(Color.parseColor("#00FF88"), Constants.SCORE_FOOD), // 普通食物,绿色
BONUS(Color.parseColor("#FFAA00"), Constants.SCORE_BONUS), // bonus食物,橙色
GOLDEN(Color.parseColor("#FFFF00"), Constants.SCORE_GOLDEN), // 金色食物,黄色
SLOW_DOWN(Color.parseColor("#00AAFF"), Constants.SCORE_FOOD), // 减速道具,蓝色
INVINCIBLE(Color.parseColor("#FF44FF"), Constants.SCORE_FOOD), // 无敌道具,紫色
DOUBLE_SCORE(Color.parseColor("#FF4444"), Constants.SCORE_FOOD); // 双倍分数,红色
private final int color; // 食物颜色
private final int score; // 食物分数
FoodType(int color, int score) {
this.color = color;
this.score = score;
}
public int getColor() {
return color;
}
public int getScore() {
return score;
}
}
// ==================== 成员变量 ====================
private final FoodType type; // 食物类型
private final Rect rect; // 食物位置和大小
private final long spawnTime; // 生成时间
private final long lifetime; // 生命周期
private float pulsePhase; // 脉冲动画相位
// ==================== 构造函数 ====================
public FoodItem(FoodType type, Rect rect, long spawnTime, long lifetime) {
this.type = type;
this.rect = rect;
this.spawnTime = spawnTime;
this.lifetime = lifetime;
this.pulsePhase = 0;
}
// ==================== Getter方法 ====================
public FoodType getType() {
return type;
}
public Rect getRect() {
return rect;
}
public long getSpawnTime() {
return spawnTime;
}
public long getLifetime() {
return lifetime;
}
public float getPulsePhase() {
return pulsePhase;
}
public int getColor() {
return type.getColor();
}
public int getScore() {
return type.getScore();
}
// ==================== 方法 ====================
/**
* 设置脉冲相位
*/
public void setPulsePhase(float phase) {
this.pulsePhase = phase;
}
/**
* 检查食物是否过期
*/
public boolean isExpired(long currentTime) {
return currentTime - spawnTime > lifetime;
}
}
4.4 食物生成器
创建core/FoodSpawner.java文件,负责生成和管理游戏中的食物:
package com.example.snake.core;
import android.graphics.Rect;
import com.example.snake.models.FoodItem;
import com.example.snake.utils.Constants;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 食物生成器
* 负责生成和管理游戏中的食物
*/
public class FoodSpawner {
private final Random random;
private final int width;
private final int height;
private final int blockSize;
private final int offsetX;
private final int offsetY;
private final int gridWidth;
private final int gridHeight;
private List<FoodItem> activeFood;
private int spawnTick;
// ==================== 构造函数 ====================
public FoodSpawner(int width, int height, int blockSize, int offsetX, int offsetY, int gridWidth, int gridHeight) {
this.random = new Random();
this.width = width;
this.height = height;
this.blockSize = blockSize;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.gridWidth = gridWidth;
this.gridHeight = gridHeight;
this.activeFood = new ArrayList<>();
this.spawnTick = 0;
}
// ==================== 方法 ====================
/**
* 更新食物生成器
*/
public void update(long currentTime, List<Rect> snakeBody) {
// 增加生成tick
spawnTick++;
// 移除过期食物
removeExpiredFood(currentTime);
// 检查是否需要生成新食物
if (spawnTick >= Constants.FOOD_SPAWN_INTERVAL) {
spawnTick = 0;
spawnFood(currentTime, snakeBody);
}
// 更新食物动画
updateFoodAnimation(currentTime);
}
/**
* 生成食物
*/
private void spawnFood(long currentTime, List<Rect> snakeBody) {
// 确保同时只存在一个食物
if (!activeFood.isEmpty()) {
return; // 已有食物,不生成新的
}
// 计算可用的网格位置
int maxX = gridWidth / blockSize;
int maxY = gridHeight / blockSize;
// 随机生成位置
int gridX = random.nextInt(maxX);
int gridY = random.nextInt(maxY);
int x = offsetX + gridX * blockSize;
int y = offsetY + gridY * blockSize;
Rect rect = new Rect(x, y, x + blockSize, y + blockSize);
// 检查是否与蛇身重叠
for (Rect segment : snakeBody) {
if (Rect.intersects(rect, segment)) {
return; // 与蛇身重叠,不生成
}
}
// 确定食物类型
FoodItem.FoodType type = determineFoodType();
// 创建并添加食物
FoodItem foodItem = new FoodItem(type, rect, currentTime, Constants.FOOD_LIFETIME);
activeFood.add(foodItem);
}
/**
* 确定食物类型
*/
private FoodItem.FoodType determineFoodType() {
int chance = random.nextInt(100);
if (chance < Constants.GOLDEN_FOOD_CHANCE) {
return FoodItem.FoodType.GOLDEN;
} else if (chance < Constants.GOLDEN_FOOD_CHANCE + Constants.BONUS_FOOD_CHANCE) {
return FoodItem.FoodType.BONUS;
} else if (chance < Constants.GOLDEN_FOOD_CHANCE + Constants.BONUS_FOOD_CHANCE + Constants.POWER_UP_CHANCE) {
// 随机选择一种道具
int powerUpType = random.nextInt(3);
switch (powerUpType) {
case 0:
return FoodItem.FoodType.SLOW_DOWN;
case 1:
return FoodItem.FoodType.INVINCIBLE;
case 2:
default:
return FoodItem.FoodType.DOUBLE_SCORE;
}
} else {
return FoodItem.FoodType.NORMAL;
}
}
/**
* 移除过期食物
*/
private void removeExpiredFood(long currentTime) {
List<FoodItem> expiredFood = new ArrayList<>();
for (FoodItem food : activeFood) {
if (food.isExpired(currentTime)) {
expiredFood.add(food);
}
}
activeFood.removeAll(expiredFood);
}
/**
* 更新食物动画
*/
private void updateFoodAnimation(long currentTime) {
for (FoodItem food : activeFood) {
// 更新脉冲相位
float deltaTime = (currentTime - food.getSpawnTime()) / 1000f;
food.setPulsePhase(deltaTime * 3f % (float) Math.PI * 2);
}
}
/**
* 检查蛇是否吃到食物
*/
public FoodItem checkFoodCollision(Rect snakeHead) {
for (int i = 0; i < activeFood.size(); i++) {
FoodItem food = activeFood.get(i);
if (Rect.intersects(snakeHead, food.getRect())) {
activeFood.remove(i);
return food;
}
}
return null;
}
// ==================== Getter方法 ====================
public List<FoodItem> getActiveFood() {
return activeFood;
}
/**
* 清空所有食物
*/
public void clearFood() {
activeFood.clear();
}
/**
* 重置生成器
*/
public void reset() {
activeFood.clear();
spawnTick = 0;
}
}
4.5 道具管理器
创建core/PowerUpManager.java文件,管理游戏中的道具效果:
package com.example.snake.core;
import com.example.snake.models.FoodItem;
import com.example.snake.utils.Constants;
/**
* 道具管理器
* 负责管理游戏中的道具效果
*/
public class PowerUpManager {
private boolean isSlowDownActive;
private boolean isInvincibleActive;
private boolean isDoubleScoreActive;
private long slowDownEndTime;
private long invincibleEndTime;
private long doubleScoreEndTime;
// ==================== 构造函数 ====================
public PowerUpManager() {
reset();
}
// ==================== 方法 ====================
/**
* 更新道具状态
*/
public void update(long currentTime) {
// 检查减速道具是否过期
if (isSlowDownActive && currentTime > slowDownEndTime) {
isSlowDownActive = false;
}
// 检查无敌道具是否过期
if (isInvincibleActive && currentTime > invincibleEndTime) {
isInvincibleActive = false;
}
// 检查双倍分数道具是否过期
if (isDoubleScoreActive && currentTime > doubleScoreEndTime) {
isDoubleScoreActive = false;
}
}
/**
* 激活道具
*/
public void activatePowerUp(FoodItem.FoodType type, long currentTime) {
switch (type) {
case SLOW_DOWN:
isSlowDownActive = true;
slowDownEndTime = currentTime + Constants.POWER_UP_DURATION;
break;
case INVINCIBLE:
isInvincibleActive = true;
invincibleEndTime = currentTime + Constants.POWER_UP_DURATION;
break;
case DOUBLE_SCORE:
isDoubleScoreActive = true;
doubleScoreEndTime = currentTime + Constants.POWER_UP_DURATION;
break;
}
}
/**
* 获取游戏速度因子
*/
public float getSpeedFactor() {
if (isSlowDownActive) {
return Constants.SLOW_DOWN_FACTOR;
}
return 1.0f;
}
// ==================== Getter方法 ====================
public boolean isSlowDown() {
return isSlowDownActive;
}
public boolean isInvincible() {
return isInvincibleActive;
}
public boolean isDoubleScore() {
return isDoubleScoreActive;
}
/**
* 重置道具状态
*/
public void reset() {
isSlowDownActive = false;
isInvincibleActive = false;
isDoubleScoreActive = false;
slowDownEndTime = 0;
invincibleEndTime = 0;
doubleScoreEndTime = 0;
}
}
4.6 粒子系统
创建effects/ParticleSystem.java文件,实现游戏中的粒子特效:
package com.example.snake.effects;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 粒子系统
* 负责生成和管理游戏中的粒子特效
*/
public class ParticleSystem {
private List<Particle> particles;
private Random random;
private Paint paint;
// ==================== 构造函数 ====================
public ParticleSystem() {
this.particles = new ArrayList<>();
this.random = new Random();
this.paint = new Paint();
this.paint.setAntiAlias(true);
}
// ==================== 方法 ====================
/**
* 更新粒子系统
*/
public void update(long currentTime) {
List<Particle> expiredParticles = new ArrayList<>();
for (Particle particle : particles) {
if (!particle.update(currentTime)) {
expiredParticles.add(particle);
}
}
particles.removeAll(expiredParticles);
}
/**
* 绘制粒子系统
*/
public void draw(Canvas canvas) {
for (Particle particle : particles) {
particle.draw(canvas, paint);
}
}
/**
* 发射食物被吃特效
*/
public void emitFoodEaten(float x, float y, int color) {
for (int i = 0; i < 8; i++) {
float angle = (float) (Math.PI * 2 * i / 8);
float speed = 200 + random.nextFloat() * 100;
particles.add(new ColorParticle(x, y, angle, speed, color, System.currentTimeMillis(), 500));
}
}
/**
* 发射死亡特效
*/
public void emitDeath(float x, float y) {
for (int i = 0; i < 12; i++) {
float angle = (float) (Math.PI * 2 * i / 12);
float speed = 300 + random.nextFloat() * 200;
int color = Color.RED;
particles.add(new ColorParticle(x, y, angle, speed, color, System.currentTimeMillis(), 800));
}
}
/**
* 发射道具特效
*/
public void emitPowerUp(float x, float y, int color) {
for (int i = 0; i < 6; i++) {
float angle = (float) (Math.PI * 2 * i / 6);
float speed = 150 + random.nextFloat() * 50;
particles.add(new ColorParticle(x, y, angle, speed, color, System.currentTimeMillis(), 600));
}
}
/**
* 发射分数特效
*/
public void emitScore(float x, float y, int score) {
particles.add(new TextParticle(x, y, score, System.currentTimeMillis(), 1000));
}
/**
* 清除所有粒子
*/
public void clear() {
particles.clear();
}
// ==================== 粒子基类 ====================
private abstract static class Particle {
protected float x;
protected float y;
protected long spawnTime;
protected long lifetime;
public Particle(float x, float y, long spawnTime, long lifetime) {
this.x = x;
this.y = y;
this.spawnTime = spawnTime;
this.lifetime = lifetime;
}
public abstract boolean update(long currentTime);
public abstract void draw(Canvas canvas, Paint paint);
}
// ==================== 彩色粒子 ====================
private static class ColorParticle extends Particle {
private float angle;
private float speed;
private int color;
private float size;
public ColorParticle(float x, float y, float angle, float speed, int color, long spawnTime, long lifetime) {
super(x, y, spawnTime, lifetime);
this.angle = angle;
this.speed = speed;
this.color = color;
this.size = 8;
}
@Override
public boolean update(long currentTime) {
long elapsed = currentTime - spawnTime;
if (elapsed > lifetime) {
return false;
}
float progress = (float) elapsed / lifetime;
float deltaTime = 0.016f; // 假设60fps
// 更新位置
x += Math.cos(angle) * speed * deltaTime;
y += Math.sin(angle) * speed * deltaTime;
// 更新大小
size = 8 * (1 - progress);
return true;
}
@Override
public void draw(Canvas canvas, Paint paint) {
paint.setColor(color);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, size, paint);
}
}
// ==================== 文字粒子 ====================
private static class TextParticle extends Particle {
private int score;
private float speedY;
public TextParticle(float x, float y, int score, long spawnTime, long lifetime) {
super(x, y, spawnTime, lifetime);
this.score = score;
this.speedY = -200;
}
@Override
public boolean update(long currentTime) {
long elapsed = currentTime - spawnTime;
if (elapsed > lifetime) {
return false;
}
float deltaTime = 0.016f; // 假设60fps
// 更新位置
y += speedY * deltaTime;
return true;
}
@Override
public void draw(Canvas canvas, Paint paint) {
paint.setColor(Color.YELLOW);
paint.setTextSize(24);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("+" + score, x, y, paint);
}
}
}
4.7 音效管理器
创建managers/SoundManager.java文件,管理游戏中的音效:
package com.example.snake.managers;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.SoundPool;
/**
* 音效管理器
* 负责游戏中音效的加载和播放
*/
public class SoundManager {
// ==================== 音效ID ====================
public static final int SOUND_EAT_FOOD = 1; // 吃食物音效
public static final int SOUND_GAME_OVER = 2; // 游戏结束音效
public static final int SOUND_POWER_UP = 3; // 获得道具音效
// ==================== 成员变量 ====================
private SoundPool soundPool;
private boolean soundLoaded = false;
// ==================== 构造函数 ====================
public SoundManager(Context context) {
// 配置SoundPool
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
soundPool = new SoundPool.Builder()
.setMaxStreams(5)
.setAudioAttributes(audioAttributes)
.build();
// 加载音效
loadSounds(context);
}
// ==================== 方法 ====================
/**
* 加载音效资源
*/
private void loadSounds(Context context) {
try {
// 尝试加载音效资源
// 注意:这里使用反射来避免编译错误
// 实际使用时需要添加对应的音效文件到raw文件夹
soundLoaded = false;
} catch (Exception e) {
// 音效文件不存在时不影响游戏运行
soundLoaded = false;
}
}
/**
* 播放音效
*/
public void playSound(int soundId) {
if (soundLoaded) {
soundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f);
}
}
/**
* 释放资源
*/
public void release() {
if (soundPool != null) {
soundPool.release();
soundPool = null;
}
}
}
4.8 游戏视图
创建SnakeView.java文件,实现游戏的核心逻辑和渲染:
package com.example.snake;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import com.example.snake.core.FoodSpawner;
import com.example.snake.core.PowerUpManager;
import com.example.snake.effects.ParticleSystem;
import com.example.snake.managers.SoundManager;
import com.example.snake.models.FoodItem;
import com.example.snake.models.GameConfig;
import com.example.snake.utils.Constants;
import java.util.ArrayList;
import java.util.List;
/**
* 贪吃蛇游戏视图类
* 负责游戏的渲染、逻辑更新和用户交互
*/
public class SnakeView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
// ==================== 游戏核心变量 ====================
private SurfaceHolder holder; // SurfaceView的持有者,用于获取Canvas进行绘制
private Thread gameThread; // 游戏线程,负责游戏循环
private boolean isRunning; // 游戏是否正在运行
private boolean isGameStarted; // 游戏是否已开始(用于控制初始界面显示)
private boolean isPaused; // 游戏是否暂停
// ==================== 游戏对象 ====================
private List<Rect> snake; // 蛇身体列表,每个元素是一个矩形
private int direction; // 当前移动方向
private int score; // 游戏分数
private boolean isGameOver; // 游戏是否结束
private int foodEaten; // 本局吃的食物数量
private int goldenFoodEaten; // 本局吃的金色食物数量
private long gameStartTime; // 游戏开始时间
private int gameDuration; // 游戏时长(秒)
private int timeRemaining; // 时间挑战模式剩余时间(秒)
// ==================== 方向常量 ====================
public static final int UP = 0; // 向上移动
public static final int DOWN = 1; // 向下移动
public static final int LEFT = 2; // 向左移动
public static final int RIGHT = 3; // 向右移动
// ==================== 游戏参数 ====================
private static final int GRID_COUNT_X = 25; // 水平方向网格数量(增加以减小方块大小)
private static final int GRID_COUNT_Y = 30; // 垂直方向网格数量(增加以减小方块大小)
// ==================== 动态计算的游戏参数 ====================
private int blockSize; // 实际使用的方块大小
private int offsetX; // 网格水平偏移量(用于居中显示)
private int offsetY; // 网格垂直偏移量(用于居中显示)
private int gridWidth; // 网格区域宽度
private int gridHeight; // 网格区域高度
// ==================== 游戏配置 ====================
private GameConfig gameConfig; // 游戏配置
private int gameSpeed; // 当前游戏速度(毫秒)
private GameListener gameListener; // 游戏监听器
private SoundManager soundManager; // 音效管理器
// ==================== 核心组件 ====================
private FoodSpawner foodSpawner; // 食物生成器
private PowerUpManager powerUpManager; // 道具管理器
private ParticleSystem particleSystem; // 粒子系统
// ==================== 碰撞特效 ====================
private Rect crashLocation; // 碰撞位置
private long crashTime; // 碰撞时间
// ==================== 绘制相关 ====================
private Paint snakeHeadPaint; // 蛇头画笔
private Paint snakeBodyPaint; // 蛇身体画笔
private Paint foodPaint; // 食物画笔
private Paint powerUpPaint; // 道具画笔
private Paint gridPaint; // 网格画笔
private Paint wallPaint; // 墙体画笔
private Paint textPaint; // 文字画笔
private Paint crashPaint; // 碰撞特效画笔
// ==================== 构造函数 ====================
public SnakeView(Context context) {
super(context);
init();
}
public SnakeView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SnakeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
// ==================== 初始化方法 ====================
private void init() {
// 初始化SurfaceHolder
holder = getHolder();
holder.addCallback(this);
// 初始化游戏状态
resetGameState();
// 初始化核心组件
foodSpawner = new FoodSpawner(getWidth(), getHeight(), 20, 0, 0, getWidth(), getHeight());
powerUpManager = new PowerUpManager();
particleSystem = new ParticleSystem();
// 初始化游戏配置
gameConfig = GameConfig.getDefault();
gameSpeed = gameConfig.getDifficulty().getBaseSpeed();
// 初始化画笔
initPaints();
}
/**
* 初始化画笔
*/
private void initPaints() {
// 蛇头画笔
snakeHeadPaint = new Paint();
snakeHeadPaint.setColor(Color.parseColor("#00FF88"));
snakeHeadPaint.setStyle(Paint.Style.FILL);
snakeHeadPaint.setAntiAlias(true);
// 蛇身体画笔
snakeBodyPaint = new Paint();
snakeBodyPaint.setColor(Color.parseColor("#00CC66"));
snakeBodyPaint.setStyle(Paint.Style.FILL);
snakeBodyPaint.setAntiAlias(true);
// 食物画笔
foodPaint = new Paint();
foodPaint.setStyle(Paint.Style.FILL);
foodPaint.setAntiAlias(true);
// 道具画笔
powerUpPaint = new Paint();
powerUpPaint.setStyle(Paint.Style.FILL);
powerUpPaint.setAntiAlias(true);
// 网格画笔
gridPaint = new Paint();
gridPaint.setColor(Color.parseColor("#222222"));
gridPaint.setStyle(Paint.Style.STROKE);
gridPaint.setStrokeWidth(1);
// 墙体画笔
wallPaint = new Paint();
wallPaint.setColor(Color.parseColor("#00FF88"));
wallPaint.setStyle(Paint.Style.STROKE);
wallPaint.setStrokeWidth(3);
wallPaint.setAntiAlias(true);
// 文字画笔
textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
// 碰撞特效画笔
crashPaint = new Paint();
crashPaint.setStyle(Paint.Style.FILL);
crashPaint.setAntiAlias(true);
}
// ==================== 游戏配置方法 ====================
/**
* 配置游戏
*/
public void configure(GameConfig config) {
this.gameConfig = config;
this.gameSpeed = config.getDifficulty().getBaseSpeed();
resetGame();
}
// ==================== 游戏控制方法 ====================
/**
* 计算网格参数
* 根据屏幕尺寸动态计算方块大小和偏移量,使网格横向铺满屏幕
*/
private void calculateGridParams() {
int width = getWidth();
int height = getHeight();
if (width == 0 || height == 0) return;
// 基于屏幕宽度计算方块大小,确保横向铺满屏幕
blockSize = width / GRID_COUNT_X;
// 确保方块大小不小于最小值
blockSize = Math.max(blockSize, 20);
// 计算网格区域尺寸
gridWidth = width; // 横向铺满屏幕
gridHeight = GRID_COUNT_Y * blockSize;
// 计算垂直偏移量,使网格在垂直方向居中显示
offsetX = 0; // 横向不偏移,铺满屏幕
offsetY = (height - gridHeight) / 2;
// 重新初始化食物生成器
if (foodSpawner != null) {
foodSpawner = new FoodSpawner(width, height, blockSize, 0, 0, width, height);
}
}
/**
* 重置游戏
* 清空蛇身、重置分数、生成新食物
*/
public void resetGame() {
// 重置游戏状态
resetGameState();
// 清空蛇身
snake.clear();
// 计算初始位置(网格中心)
int startX = offsetX + (GRID_COUNT_X / 2) * blockSize;
int startY = offsetY + (GRID_COUNT_Y / 2) * blockSize;
// 创建初始蛇身(5节)
for (int i = 0; i < 5; i++) {
snake.add(new Rect(startX - i * blockSize, startY,
startX - i * blockSize + blockSize, startY + blockSize));
}
// 重置游戏状态
direction = RIGHT;
score = 0;
foodEaten = 0;
goldenFoodEaten = 0;
isGameOver = false;
isGameStarted = true;
isPaused = false;
crashLocation = null;
crashTime = 0;
gameStartTime = System.currentTimeMillis();
gameDuration = 0;
// 初始化时间挑战模式
if (gameConfig.getGameMode() == GameConfig.GameMode.TIME_CHALLENGE) {
timeRemaining = gameConfig.getTimeLimit();
} else {
timeRemaining = 0;
}
// 重置核心组件
powerUpManager.reset();
foodSpawner.reset();
particleSystem.clear();
// 通知分数变化
if (gameListener != null) {
gameListener.onScoreChanged(score);
}
}
/**
* 重置游戏状态
*/
private void resetGameState() {
snake = new ArrayList<>();
direction = RIGHT;
score = 0;
isGameOver = false;
isGameStarted = false;
isPaused = false;
foodEaten = 0;
goldenFoodEaten = 0;
crashLocation = null;
crashTime = 0;
}
/**
* 调整游戏速度
* 根据蛇的身体长度逐渐增加速度,更加循序渐进
*/
private void adjustGameSpeed() {
// 初始蛇长为5,每增加3节身体减少一次速度
int speedDecrement = ((snake.size() - 5) / 3) * Constants.SPEED_DECREMENT;
gameSpeed = Math.max(Constants.SPEED_MIN, gameConfig.getDifficulty().getBaseSpeed() - speedDecrement);
}
/**
* 设置移动方向
* 防止蛇反向移动(例如:向右移动时不能直接向左)
*/
public void setDirection(int newDirection) {
if ((newDirection == UP && direction != DOWN) ||
(newDirection == DOWN && direction != UP) ||
(newDirection == LEFT && direction != RIGHT) ||
(newDirection == RIGHT && direction != LEFT)) {
direction = newDirection;
}
}
/**
* 开始游戏
* 启动游戏线程
*/
public void startGame() {
if (!isRunning) {
isRunning = true;
gameThread = new Thread(this);
gameThread.start();
}
}
/**
* 暂停游戏
*/
public void pauseGame() {
isRunning = false;
}
/**
* 切换暂停状态
*/
public void togglePause() {
if (isGameStarted && !isGameOver) {
isPaused = !isPaused;
}
}
// ==================== Getter方法 ====================
/**
* 获取游戏分数
*/
public int getScore() {
return score;
}
/**
* 游戏是否已开始
*/
public boolean isGameStarted() {
return isGameStarted;
}
/**
* 游戏是否暂停
*/
public boolean isPaused() {
return isPaused;
}
/**
* 游戏是否结束
*/
public boolean isGameOver() {
return isGameOver;
}
/**
* 获取游戏配置
*/
public GameConfig getGameConfig() {
return gameConfig;
}
/**
* 设置游戏监听器
*/
public void setGameListener(GameListener listener) {
this.gameListener = listener;
}
/**
* 设置音效管理器
*/
public void setSoundManager(SoundManager soundManager) {
this.soundManager = soundManager;
}
// ==================== SurfaceHolder回调方法 ====================
/**
* Surface创建时调用
* 初始化蛇的位置并开始游戏
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
calculateGridParams(); // 计算网格参数
resetGameState();
startGame();
}
/**
* Surface尺寸改变时调用
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
calculateGridParams(); // 重新计算网格参数
// 如果游戏已开始,重新调整蛇和食物的位置
if (isGameStarted && !snake.isEmpty()) {
resetGame();
}
}
/**
* Surface销毁时调用
* 暂停游戏
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
pauseGame();
}
// ==================== 游戏主循环 ====================
@Override
public void run() {
while (isRunning) {
// 开始时间
long startTime = System.currentTimeMillis();
// 更新游戏逻辑
update();
// 绘制游戏画面
draw();
// 计算需要睡眠的时间,控制游戏速度
long elapsedTime = System.currentTimeMillis() - startTime;
long sleepTime = (long) (gameSpeed * powerUpManager.getSpeedFactor()) - elapsedTime;
if (sleepTime > 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 更新游戏逻辑
* 移动蛇、检测碰撞、处理吃食物
*/
private void update() {
// 如果游戏未开始、已结束或暂停,不更新
if (!isGameStarted || isGameOver || isPaused || snake.isEmpty()) return;
// 更新游戏时长
gameDuration = (int) ((System.currentTimeMillis() - gameStartTime) / 1000);
// 更新时间挑战模式
if (gameConfig.getGameMode() == GameConfig.GameMode.TIME_CHALLENGE) {
timeRemaining = gameConfig.getTimeLimit() - gameDuration;
if (timeRemaining <= 0) {
timeRemaining = 0;
gameOver();
return;
}
}
// 更新核心组件
long currentTime = System.currentTimeMillis();
foodSpawner.update(currentTime, snake);
powerUpManager.update(currentTime);
particleSystem.update(currentTime);
// 调整游戏速度
adjustGameSpeed();
// 获取蛇头位置
Rect head = snake.get(0);
Rect newHead = new Rect(head);
// 根据方向计算新的蛇头位置
switch (direction) {
case UP:
newHead.top -= blockSize;
newHead.bottom -= blockSize;
break;
case DOWN:
newHead.top += blockSize;
newHead.bottom += blockSize;
break;
case LEFT:
newHead.left -= blockSize;
newHead.right -= blockSize;
break;
case RIGHT:
newHead.left += blockSize;
newHead.right += blockSize;
break;
}
// 检测碰撞(移除穿墙逻辑,直接检测边界)
if (checkCollision(newHead)) {
// 记录碰撞位置和时间,用于显示特效
crashLocation = new Rect(newHead);
crashTime = System.currentTimeMillis();
particleSystem.emitDeath(newHead.left + blockSize / 2f, newHead.top + blockSize / 2f);
gameOver();
return;
}
// 检测是否吃到食物
FoodItem eatenFood = foodSpawner.checkFoodCollision(newHead);
if (eatenFood != null) {
handleFoodEaten(eatenFood, newHead);
} else {
// 没吃到食物,移除尾巴,保持蛇的长度
snake.remove(snake.size() - 1);
}
// 将新头部添加到蛇身最前面
snake.add(0, newHead);
}
/**
* 检测碰撞
*/
private boolean checkCollision(Rect head) {
// 检测是否撞墙(基于整个屏幕边界,与绘制的墙体一致)
int minX = 0;
int maxX = getWidth();
int minY = 0;
int maxY = getHeight();
// 增强边界检测,确保任何超出边界的情况都能被检测到
if (head.left < minX || head.right > maxX || head.top < minY || head.bottom > maxY) {
return true;
}
// 检测是否撞到自己(从第2节开始检测)
if (!powerUpManager.isInvincible()) {
for (int i = 1; i < snake.size(); i++) {
if (Rect.intersects(head, snake.get(i))) {
return true;
}
}
}
return false;
}
/**
* 处理吃食物
*/
private void handleFoodEaten(FoodItem food, Rect head) {
// 增加分数
int foodScore = food.getScore();
if (powerUpManager.isDoubleScore()) {
foodScore *= 2;
}
score += foodScore;
foodEaten++;
// 播放吃食物音效
if (soundManager != null) {
if (food.getType() == FoodItem.FoodType.SLOW_DOWN ||
food.getType() == FoodItem.FoodType.INVINCIBLE ||
food.getType() == FoodItem.FoodType.DOUBLE_SCORE) {
soundManager.playSound(SoundManager.SOUND_POWER_UP);
} else {
soundManager.playSound(SoundManager.SOUND_EAT_FOOD);
}
}
// 处理金色食物
if (food.getType() == FoodItem.FoodType.GOLDEN) {
goldenFoodEaten++;
}
// 处理道具
if (food.getType() == FoodItem.FoodType.SLOW_DOWN ||
food.getType() == FoodItem.FoodType.INVINCIBLE ||
food.getType() == FoodItem.FoodType.DOUBLE_SCORE) {
powerUpManager.activatePowerUp(food.getType(), System.currentTimeMillis());
particleSystem.emitPowerUp(head.left + blockSize / 2f, head.top + blockSize / 2f, food.getColor());
} else {
particleSystem.emitFoodEaten(head.left + blockSize / 2f, head.top + blockSize / 2f, food.getColor());
}
particleSystem.emitScore(head.left + blockSize / 2f, head.top + blockSize / 2f, foodScore);
// 通知分数变化
if (gameListener != null) {
gameListener.onScoreChanged(score);
}
}
/**
* 游戏结束
*/
private void gameOver() {
isGameOver = true;
// 播放游戏结束音效
if (soundManager != null) {
soundManager.playSound(SoundManager.SOUND_GAME_OVER);
}
if (gameListener != null) {
gameListener.onGameOver(score, foodEaten, gameDuration);
}
}
/**
* 绘制游戏画面
*/
private void draw() {
if (holder == null) return;
Canvas canvas = null;
try {
canvas = holder.lockCanvas();
if (canvas == null) return;
// 绘制背景
canvas.drawColor(Color.parseColor("#000000"));
// 绘制网格
drawGrid(canvas);
// 绘制墙体
drawWall(canvas);
// 如果游戏已开始且未结束,绘制游戏元素
if (isGameStarted && !isGameOver) {
drawFood(canvas); // 绘制食物
drawSnake(canvas); // 绘制蛇
drawPowerUps(canvas); // 绘制道具效果
drawTimeRemaining(canvas); // 绘制剩余时间
}
// 绘制特效(无论游戏是否结束都绘制)
if (isGameStarted) {
drawCrashEffect(canvas); // 绘制碰撞特效
particleSystem.draw(canvas); // 绘制粒子效果
}
// 绘制暂停界面
if (isPaused) {
drawPauseScreen(canvas);
}
// 绘制游戏结束界面
if (isGameOver) {
drawGameOverScreen(canvas);
}
// 绘制初始界面
if (!isGameStarted) {
drawStartScreen(canvas);
}
} finally {
if (canvas != null) {
holder.unlockCanvasAndPost(canvas);
}
}
}
/**
* 绘制网格
*/
private void drawGrid(Canvas canvas) {
if (blockSize == 0) return;
// 绘制垂直线
for (int x = 0; x <= getWidth(); x += blockSize) {
canvas.drawLine(x, 0, x, getHeight(), gridPaint);
}
// 绘制水平线
for (int y = 0; y <= getHeight(); y += blockSize) {
canvas.drawLine(0, y, getWidth(), y, gridPaint);
}
}
/**
* 绘制墙体
*/
private void drawWall(Canvas canvas) {
if (gridWidth == 0 || gridHeight == 0) return;
// 绘制主边界,铺满整个屏幕
int padding = 2;
canvas.drawRect(padding, padding,
getWidth() - padding, getHeight() - padding, wallPaint);
// 绘制多层边界,从外到内逐渐透明
for (int i = 0; i < 3; i++) {
int alpha = 150 - i * 40; // 透明度逐渐降低
wallPaint.setAlpha(alpha);
int offset = i * 3;
canvas.drawRect(padding + offset, padding + offset,
getWidth() - padding - offset, getHeight() - padding - offset, wallPaint);
}
wallPaint.setAlpha(255); // 恢复透明度
}
/**
* 绘制蛇
*/
private void drawSnake(Canvas canvas) {
if (snake.isEmpty()) return;
// 绘制蛇身体
for (int i = 1; i < snake.size(); i++) {
Rect segment = snake.get(i);
drawSnakeSegment(canvas, segment, Color.parseColor("#00CC66"));
}
// 绘制蛇头(特殊处理,颜色不同)
Rect head = snake.get(0);
drawSnakeSegment(canvas, head, Color.parseColor("#00FF88"));
}
/**
* 绘制蛇的单个身体段
*/
private void drawSnakeSegment(Canvas canvas, Rect segment, int color) {
// 计算中心点和半径
float centerX = segment.left + blockSize / 2f;
float centerY = segment.top + blockSize / 2f;
float radius = blockSize / 2f - 2;
// 绘制圆形身体
snakeBodyPaint.setColor(color);
canvas.drawCircle(centerX, centerY, radius, snakeBodyPaint);
// 绘制高光效果,增加立体感
Paint highlightPaint = new Paint();
highlightPaint.setColor(Color.parseColor("#40FF40"));
highlightPaint.setStyle(Paint.Style.FILL);
highlightPaint.setAntiAlias(true);
highlightPaint.setAlpha(80);
canvas.drawCircle(centerX - radius * 0.3f, centerY - radius * 0.3f, radius * 0.3f, highlightPaint);
}
/**
* 绘制食物
*/
private void drawFood(Canvas canvas) {
for (FoodItem food : foodSpawner.getActiveFood()) {
drawFoodItem(canvas, food);
}
}
/**
* 绘制单个食物
*/
private void drawFoodItem(Canvas canvas, FoodItem food) {
// 计算中心点和半径
float centerX = food.getRect().left + blockSize / 2f;
float centerY = food.getRect().top + blockSize / 2f;
float radius = blockSize / 2f - 2;
// 计算脉冲缩放
float pulseScale = 0.85f + 0.15f * (float) Math.sin(food.getPulsePhase());
float scaledRadius = radius * pulseScale;
// 绘制食物主体
powerUpPaint.setColor(food.getColor());
canvas.drawCircle(centerX, centerY, scaledRadius, powerUpPaint);
// 绘制食物光芒效果
if (food.getType() == FoodItem.FoodType.GOLDEN) {
// 金色食物特殊效果
Paint glowPaint = new Paint();
glowPaint.setColor(Color.parseColor("#FFFF00"));
glowPaint.setStyle(Paint.Style.STROKE);
glowPaint.setStrokeWidth(2);
glowPaint.setAlpha(100);
for (int i = 0; i < 3; i++) {
float glowRadius = scaledRadius + i * 3;
canvas.drawCircle(centerX, centerY, glowRadius, glowPaint);
}
}
}
/**
* 绘制道具效果
*/
private void drawPowerUps(Canvas canvas) {
// 绘制减速效果
if (powerUpManager.isSlowDown()) {
Paint slowDownPaint = new Paint();
slowDownPaint.setColor(Color.parseColor("#00AAFF"));
slowDownPaint.setStyle(Paint.Style.STROKE);
slowDownPaint.setStrokeWidth(2);
slowDownPaint.setAlpha(100);
// 绘制波浪效果
long currentTime = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
float radius = 50 + i * 20 + (float) (Math.sin(currentTime / 100f) * 10);
canvas.drawCircle(getWidth() / 2, 50, radius, slowDownPaint);
}
}
// 绘制无敌效果
if (powerUpManager.isInvincible()) {
Paint invinciblePaint = new Paint();
invinciblePaint.setColor(Color.parseColor("#FF44FF"));
invinciblePaint.setStyle(Paint.Style.STROKE);
invinciblePaint.setStrokeWidth(2);
invinciblePaint.setAlpha(100);
// 绘制闪烁效果
long currentTime = System.currentTimeMillis();
if ((currentTime / 100) % 2 == 0) {
for (Rect segment : snake) {
float centerX = segment.left + blockSize / 2f;
float centerY = segment.top + blockSize / 2f;
float radius = blockSize / 2f + 5;
canvas.drawCircle(centerX, centerY, radius, invinciblePaint);
}
}
}
// 绘制双倍分数效果
if (powerUpManager.isDoubleScore()) {
Paint doubleScorePaint = new Paint();
doubleScorePaint.setColor(Color.parseColor("#FF4444"));
doubleScorePaint.setStyle(Paint.Style.FILL);
doubleScorePaint.setAlpha(100);
// 绘制分数翻倍图标
textPaint.setColor(Color.parseColor("#FF4444"));
textPaint.setTextSize(24);
canvas.drawText("2x", 50, 50, textPaint);
}
}
/**
* 绘制剩余时间
*/
private void drawTimeRemaining(Canvas canvas) {
if (gameConfig.getGameMode() == GameConfig.GameMode.TIME_CHALLENGE) {
textPaint.setColor(Color.parseColor("#FFAA00"));
textPaint.setTextSize(32);
canvas.drawText("时间: " + timeRemaining + "s", getWidth() / 2, 50, textPaint);
}
}
/**
* 绘制碰撞特效
* 在碰撞位置显示爆炸效果
*/
private void drawCrashEffect(Canvas canvas) {
if (crashLocation == null) return;
// 计算特效持续时间
long elapsed = System.currentTimeMillis() - crashTime;
if (elapsed > Constants.CRASH_EFFECT_DURATION) {
crashLocation = null;
return;
}
// 计算特效进度(0到1)
float progress = (float) elapsed / Constants.CRASH_EFFECT_DURATION;
int alpha = (int) (255 * (1 - progress)); // 透明度逐渐降低
// 计算爆炸半径(逐渐增大)
int radius = (int) (blockSize * (1 + progress * 4));
// 选择颜色(在黄色、红色、白色、洋红色、青色之间切换,更鲜艳)
int[] colors = {Color.YELLOW, Color.RED, Color.WHITE, Color.MAGENTA, Color.CYAN};
int colorIndex = (int) (progress * colors.length * 2) % colors.length;
int color = colors[colorIndex];
crashPaint.setColor(color);
crashPaint.setAlpha(alpha);
// 计算碰撞位置中心
int centerX = crashLocation.left + blockSize / 2;
int centerY = crashLocation.top + blockSize / 2;
// 绘制爆炸效果
canvas.drawCircle(centerX, centerY, radius, crashPaint);
// 绘制多重同心圆效果,增强层次感
for (int i = 1; i <= 3; i++) {
float innerRadius = radius * (0.3f * i);
crashPaint.setAlpha((int) (alpha * (1 - 0.3f * i)));
canvas.drawCircle(centerX, centerY, (int) innerRadius, crashPaint);
}
// 绘制内层发光圆
crashPaint.setColor(Color.WHITE);
crashPaint.setAlpha((int) (alpha * 0.5f));
canvas.drawCircle(centerX, centerY, radius / 3, crashPaint);
// 绘制外层冲击波效果
crashPaint.setColor(color);
crashPaint.setStyle(Paint.Style.STROKE);
crashPaint.setStrokeWidth(2);
crashPaint.setAlpha((int) (alpha * 0.3f));
for (int i = 0; i < 3; i++) {
float waveRadius = radius + i * 10;
canvas.drawCircle(centerX, centerY, (int) waveRadius, crashPaint);
}
}
/**
* 绘制初始界面
*/
private void drawStartScreen(Canvas canvas) {
// 绘制标题 "贪吃蛇"
textPaint.setColor(Color.parseColor("#00FF88"));
textPaint.setTextSize(56);
textPaint.setFakeBoldText(true);
canvas.drawText("贪吃蛇", getWidth() / 2, getHeight() / 2 - 80, textPaint);
// 绘制提示文字
textPaint.setColor(Color.parseColor("#FFAA00"));
textPaint.setTextSize(32);
textPaint.setFakeBoldText(false);
canvas.drawText("点击开始游戏", getWidth() / 2, getHeight() / 2 + 20, textPaint);
}
/**
* 绘制游戏结束界面
*/
private void drawGameOverScreen(Canvas canvas) {
// 绘制 "游戏结束" 文字
textPaint.setColor(Color.parseColor("#FF4444"));
textPaint.setTextSize(56);
textPaint.setFakeBoldText(true);
canvas.drawText("游戏结束", getWidth() / 2, getHeight() / 2 - 20, textPaint);
// 绘制分数
textPaint.setColor(Color.parseColor("#FFAA00"));
textPaint.setTextSize(36);
canvas.drawText("分数:" + score, getWidth() / 2, getHeight() / 2 + 40, textPaint);
// 绘制重新开始提示
textPaint.setColor(Color.parseColor("#00FF88"));
textPaint.setTextSize(28);
textPaint.setFakeBoldText(false);
canvas.drawText("点击开始重新游戏", getWidth() / 2, getHeight() / 2 + 90, textPaint);
}
/**
* 绘制暂停界面
*/
private void drawPauseScreen(Canvas canvas) {
// 绘制半透明背景
Paint overlayPaint = new Paint();
overlayPaint.setColor(Color.argb(150, 0, 0, 0));
canvas.drawRect(0, 0, getWidth(), getHeight(), overlayPaint);
// 绘制 "游戏暂停" 文字
textPaint.setColor(Color.parseColor("#00FF88"));
textPaint.setTextSize(56);
textPaint.setFakeBoldText(true);
canvas.drawText("游戏暂停", getWidth() / 2, getHeight() / 2 - 20, textPaint);
// 绘制提示文字
textPaint.setColor(Color.parseColor("#FFAA00"));
textPaint.setTextSize(28);
textPaint.setFakeBoldText(false);
canvas.drawText("点击继续游戏", getWidth() / 2, getHeight() / 2 + 30, textPaint);
}
// ==================== 游戏监听器接口 ====================
public interface GameListener {
void onScoreChanged(int score);
void onGameOver(int score, int foodEaten, int duration);
}
}
4.9 主活动
创建MainActivity.java文件,实现游戏的主界面和用户交互:
package com.example.snake;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.example.snake.managers.SoundManager;
import com.example.snake.models.GameConfig;
import com.tencent.mmkv.MMKV;
/**
* 主活动类
* 负责游戏的初始化、用户交互和界面管理
*/
public class MainActivity extends Activity {
// ==================== 成员变量 ====================
private SnakeView snakeView; // 游戏视图
private TextView scoreTextView; // 分数显示
private TextView highScoreTextView; // 历史最高分显示
private Button startButton; // 开始/暂停按钮
// ==================== 方向按钮 ====================
private Button upButton; // 向上按钮
private Button downButton; // 向下按钮
private Button leftButton; // 向左按钮
private Button rightButton; // 向右按钮
// ==================== 其他 ====================
private Handler handler; // 用于更新UI的Handler
private Runnable scoreUpdater; // 分数更新任务
// ==================== MMKV相关 ====================
private MMKV mmkv;
// ==================== 音效相关 ====================
private SoundManager soundManager;
// ==================== 生命周期方法 ====================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化MMKV
MMKV.initialize(this);
mmkv = MMKV.defaultMMKV();
// 初始化音效管理器
soundManager = new SoundManager(this);
// 初始化视图
initViews();
// 初始化Handler和分数更新任务
initHandler();
// 显示历史最高分
updateHighScore();
// 设置音效管理器
snakeView.setSoundManager(soundManager);
}
/**
* 初始化视图
*/
private void initViews() {
// 初始化游戏视图
snakeView = findViewById(R.id.snakeView);
// 初始化分数显示
scoreTextView = findViewById(R.id.scoreTextView);
highScoreTextView = findViewById(R.id.highScoreTextView);
// 初始化开始按钮
startButton = findViewById(R.id.startButton);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleStartButtonClick();
}
});
// 初始化方向按钮
upButton = findViewById(R.id.upButton);
downButton = findViewById(R.id.downButton);
leftButton = findViewById(R.id.leftButton);
rightButton = findViewById(R.id.rightButton);
// 设置方向按钮点击监听器
upButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
snakeView.setDirection(SnakeView.UP);
}
});
downButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
snakeView.setDirection(SnakeView.DOWN);
}
});
leftButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
snakeView.setDirection(SnakeView.LEFT);
}
});
rightButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
snakeView.setDirection(SnakeView.RIGHT);
}
});
// 设置游戏监听器
snakeView.setGameListener(new SnakeView.GameListener() {
@Override
public void onScoreChanged(int score) {
runOnUiThread(new Runnable() {
@Override
public void run() {
scoreTextView.setText("分数: " + score);
}
});
}
@Override
public void onGameOver(int score, int foodEaten, int duration) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// 更新最高分
updateHighScore();
}
});
}
});
}
/**
* 初始化Handler和分数更新任务
*/
private void initHandler() {
handler = new Handler();
scoreUpdater = new Runnable() {
@Override
public void run() {
// 更新分数显示
if (snakeView != null) {
int score = snakeView.getScore();
scoreTextView.setText("分数: " + score);
// 更新按钮状态
if (snakeView.isGameStarted() && !snakeView.isGameOver()) {
if (snakeView.isPaused()) {
startButton.setText("继续");
} else {
startButton.setText("暂停");
}
} else {
startButton.setText("开始游戏");
}
}
// 每100毫秒更新一次
handler.postDelayed(this, 100);
}
};
// 启动分数更新任务
handler.post(scoreUpdater);
}
/**
* 处理开始按钮点击
*/
private void handleStartButtonClick() {
if (snakeView.isGameStarted() && !snakeView.isGameOver()) {
// 游戏已开始且未结束,切换暂停状态
snakeView.togglePause();
} else {
// 游戏未开始或已结束,开始新游戏
snakeView.resetGame();
}
}
/**
* 更新历史最高分
*/
private void updateHighScore() {
int highScore = mmkv.getInt("high_score", 0);
int currentScore = snakeView.getScore();
// 如果当前分数高于历史最高分,更新最高分
if (currentScore > highScore) {
highScore = currentScore;
mmkv.putInt("high_score", highScore);
}
highScoreTextView.setText("最高分: " + highScore);
}
/**
* 处理返回按钮点击
*/
@Override
public void onBackPressed() {
// 显示退出确认对话框
new AlertDialog.Builder(this)
.setTitle("退出游戏")
.setMessage("确定要退出游戏吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.setNegativeButton("取消", null)
.show();
}
/**
* Activity销毁时调用
* 清理资源,移除所有回调
*/
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacks(scoreUpdater); // 移除分数更新任务
if (soundManager != null) {
soundManager.release();
}
}
}
4.10 布局文件
创建res/layout/activity_main.xml文件,定义游戏的界面布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
tools:context=".MainActivity">
<!-- 游戏视图 -->
<com.example.snake.SnakeView
android:id="@+id/snakeView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/buttonLayout" />
<!-- 分数显示 -->
<LinearLayout
android:id="@+id/scoreLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="horizontal"
android:padding="10dp"
android:background="#222222">
<TextView
android:id="@+id/scoreTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="分数: 0"
android:textColor="#00FF88"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/highScoreTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="最高分: 0"
android:textColor="#FFAA00"
android:textSize="18sp"
android:textStyle="bold"
android:gravity="right" />
</LinearLayout>
<!-- 按钮布局 -->
<LinearLayout
android:id="@+id/buttonLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="vertical"
android:padding="10dp"
android:background="#222222">
<!-- 开始按钮 -->
<Button
android:id="@+id/startButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始游戏"
android:textColor="#FFFFFF"
android:background="@drawable/start_button_bg"
android:layout_marginBottom="10dp"
android:padding="15dp"
android:textSize="18sp"
android:textStyle="bold" />
<!-- 方向按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<!-- 向上按钮 -->
<Button
android:id="@+id/upButton"
android:layout_width="80dp"
android:layout_height="80dp"
android:text="↑"
android:textColor="#FFFFFF"
android:background="@drawable/start_button_bg"
android:textSize="24sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 向左按钮 -->
<Button
android:id="@+id/leftButton"
android:layout_width="80dp"
android:layout_height="80dp"
android:text="←"
android:textColor="#FFFFFF"
android:background="@drawable/start_button_bg"
android:textSize="24sp" />
<!-- 向右按钮 -->
<Button
android:id="@+id/rightButton"
android:layout_width="80dp"
android:layout_height="80dp"
android:text="→"
android:textColor="#FFFFFF"
android:background="@drawable/start_button_bg"
android:textSize="24sp" />
</LinearLayout>
<!-- 向下按钮 -->
<Button
android:id="@+id/downButton"
android:layout_width="80dp"
android:layout_height="80dp"
android:text="↓"
android:textColor="#FFFFFF"
android:background="@drawable/start_button_bg"
android:textSize="24sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
4.11 按钮背景
创建res/drawable/start_button_bg.xml文件,定义按钮的背景样式:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#333333" />
<stroke android:width="2dp" android:color="#00FF88" />
<corners android:radius="8dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#222222" />
<stroke android:width="2dp" android:color="#00FF88" />
<corners android:radius="8dp" />
</shape>
</item>
</selector>
5. 游戏功能特性
5.1 核心功能
- 经典贪吃蛇玩法:控制蛇移动,吃食物增长身体
- 碰撞检测:撞到墙壁或自身会游戏结束
- 分数系统:根据食物类型获得不同分数
- 难度调整:根据蛇的长度逐渐增加游戏速度
5.2 特色功能
- 多种食物类型:普通食物、 bonus食物、金色食物
- 道具系统:减速、无敌、双倍分数
- 粒子特效:食物被吃、死亡、道具获得时的特效
- 音效系统:吃食物、游戏结束、获得道具的音效
- 历史最高分:使用MMKV存储历史最高分
- 退出确认:防止误操作退出游戏
5.3 视觉效果
- 霓虹风格:绿色霓虹效果,营造复古游戏机氛围
- 动态背景:网格背景和墙体效果
- 流畅动画:蛇的移动、食物的脉冲效果
- 碰撞特效:撞墙时的爆炸效果
6. 项目构建与运行
6.1 构建项目
在TRAE IDE中,点击"Build" -> "Make Project"构建项目。如果遇到构建错误,请检查以下几点:
- 依赖配置:确保
build.gradle中的依赖项正确配置 - 资源文件:确保所有需要的资源文件都已创建
- 代码语法:确保代码中没有语法错误
6.2 运行项目
点击"Run" -> "Run 'app'"运行项目。选择一个模拟器或连接的设备,即可在设备上运行游戏。
7. 项目优化与扩展
7.1 性能优化
- 线程管理:确保游戏线程正确启动和停止
- 内存管理:及时释放不需要的资源
- 绘制优化:减少不必要的绘制操作
7.2 功能扩展
- 游戏模式:添加更多游戏模式,如时间挑战、障碍模式等
- 皮肤系统:添加不同的蛇皮肤和游戏主题
- 成就系统:实现成就解锁和奖励机制
- 多人对战:添加本地多人对战功能
7.3 商业化升级
- 广告集成:添加适当的广告位置
- 内购系统:提供皮肤、道具等付费内容
- 排行榜:实现全球或本地排行榜
- 社交分享:添加游戏成绩分享功能
8. 总结
通过本文的详细介绍,我们成功使用TRAE工具实现了一个完整的Android原生贪吃蛇游戏。从项目初始化到功能实现,我们一步一步构建了一个具有现代感和良好用户体验的游戏应用。
这个项目不仅实现了经典贪吃蛇游戏的核心功能,还添加了许多现代化的特性,如道具系统、粒子特效、音效系统等。同时,我们采用了模块化的代码结构,使代码更加清晰和易于维护。
通过这个项目,我们学习了如何:
- 使用SurfaceView实现游戏渲染
- 实现游戏循环和碰撞检测
- 使用粒子系统实现视觉特效
- 使用MMKV实现数据持久化
- 构建模块化的Android应用
希望本文能够帮助你理解如何使用TRAE工具开发Android游戏应用,也希望你能够在此基础上进一步扩展和优化这个项目,创造出更加出色的游戏作品。