Flappy Bird 是一款风靡全球的休闲小游戏,玩家通过点击屏幕控制小鸟飞越不断出现的管道间隙。游戏规则看似简单,但要让 AI 自主学会飞行策略却涉及神经进化算法的核心思想。本文将用 Java 实现一个完整的 Flappy Bird AI 训练系统,基于遗传算法(Genetic Algorithm)进化神经网络权重,使小鸟能够在多代迭代中自适应地掌握跳越时机。
一、问题分析与算法设计
1.1 游戏状态建模
小鸟的决策需要感知当前环境状态。我们提取以下关键输入特征作为神经网络的输入层:
- 小鸟与上方管道的垂直距离
- 小鸟与下方管道的垂直距离
- 小鸟与前方管道的水平距离
- 小鸟当前的垂直速度
这四个特征足以描述小鸟当前面临的决策场景。
1.2 神经网络结构
采用简单的前馈神经网络(Feedforward Neural Network):
- 输入层:4 个神经元(对应上述 4 个特征)
- 隐藏层:6 个神经元,使用 Sigmoid 激活函数
- 输出层:1 个神经元,输出跳跃概率
当输出值大于 0.5 时,小鸟执行跳跃动作。
1.3 遗传算法流程
遗传算法通过模拟自然选择过程来优化神经网络权重,核心步骤包括:
- 种群初始化:随机生成 N 个个体的神经网络权重
- 适应度评估:每个个体控制小鸟飞行,存活时间越长、穿过管道越多,适应度越高
- 选择操作:根据适应度采用轮盘赌选择父代
- 交叉操作:以一定概率交换两个父代的神经网络权重
- 变异操作:以一定概率对权重添加随机扰动
- 迭代进化:重复评估-选择-交叉-变异,直到收敛
二、Java 实现
2.1 神经网络类
import java.util.Random;
/**
* 前馈神经网络:4输入 -> 6隐藏 -> 1输出
* 使用Sigmoid激活函数
*/
public class NeuralNetwork {
private static final Random RAND = new Random();
// 网络拓扑参数
private final int inputSize = 4;
private final int hiddenSize = 6;
private final int outputSize = 1;
// 权重矩阵
private double[][] weightsIH; // 输入到隐藏层 [hiddenSize][inputSize]
private double[][] weightsHO; // 隐藏到输出层 [outputSize][hiddenSize]
private double[] biasH; // 隐藏层偏置
private double[] biasO; // 输出层偏置
public NeuralNetwork() {
weightsIH = new double[hiddenSize][inputSize];
weightsHO = new double[outputSize][hiddenSize];
biasH = new double[hiddenSize];
biasO = new double[outputSize];
randomizeWeights();
}
/**
* 从权重数组构造网络(用于遗传算法的基因编码)
*/
public NeuralNetwork(double[] genes) {
this();
int idx = 0;
for (int i = 0; i < hiddenSize; i++) {
for (int j = 0; j < inputSize; j++) {
weightsIH[i][j] = genes[idx++];
}
biasH[i] = genes[idx++];
}
for (int i = 0; i < outputSize; i++) {
for (int j = 0; j < hiddenSize; j++) {
weightsHO[i][j] = genes[idx++];
}
biasO[i] = genes[idx++];
}
}
/**
* 随机初始化所有权重,使用Xavier初始化策略
*/
private void randomizeWeights() {
double limitIH = Math.sqrt(6.0 / (inputSize + hiddenSize));
double limitHO = Math.sqrt(6.0 / (hiddenSize + outputSize));
for (int i = 0; i < hiddenSize; i++) {
for (int j = 0; j < inputSize; j++) {
weightsIH[i][j] = (RAND.nextDouble() * 2 - 1) * limitIH;
}
biasH[i] = (RAND.nextDouble() * 2 - 1) * limitIH;
}
for (int i = 0; i < outputSize; i++) {
for (int j = 0; j < hiddenSize; j++) {
weightsHO[i][j] = (RAND.nextDouble() * 2 - 1) * limitHO;
}
biasO[i] = (RAND.nextDouble() * 2 - 1) * limitHO;
}
}
/**
* 前向传播:根据输入特征计算输出
* @param inputs 4维特征向量 [birdY-gapTop, birdY-gapBottom, distance, velocity]
* @return 输出层激活值,大于0.5建议跳跃
*/
public double feedForward(double[] inputs) {
// 隐藏层计算
double[] hidden = new double[hiddenSize];
for (int i = 0; i < hiddenSize; i++) {
double sum = biasH[i];
for (int j = 0; j < inputSize; j++) {
sum += inputs[j] * weightsIH[i][j];
}
hidden[i] = sigmoid(sum);
}
// 输出层计算
double output = biasO[0];
for (int j = 0; j < hiddenSize; j++) {
output += hidden[j] * weightsHO[0][j];
}
return sigmoid(output);
}
private double sigmoid(double x) {
return 1.0 / (1.0 + Math.exp(-x));
}
/**
* 将网络权重编码为一维基因数组
*/
public double[] toGenes() {
int geneLength = hiddenSize * (inputSize + 1) + outputSize * (hiddenSize + 1);
double[] genes = new double[geneLength];
int idx = 0;
for (int i = 0; i < hiddenSize; i++) {
System.arraycopy(weightsIH[i], 0, genes, idx, inputSize);
idx += inputSize;
genes[idx++] = biasH[i];
}
for (int i = 0; i < outputSize; i++) {
System.arraycopy(weightsHO[i], 0, genes, idx, hiddenSize);
idx += hiddenSize;
genes[idx++] = biasO[i];
}
return genes;
}
/**
* 基因长度:用于遗传算法初始化
*/
public static int getGeneLength() {
return 6 * (4 + 1) + 1 * (6 + 1); // 37
}
}
2.2 游戏环境与适应度评估
/**
* Flappy Bird 游戏模拟器
* 简化物理模型,用于快速评估神经网络表现
*/
public class GameEnvironment {
// 游戏参数
private static final double GRAVITY = 0.5;
private static final double JUMP_STRENGTH = -6.0;
private static final int PIPE_WIDTH = 60;
private static final int PIPE_GAP = 120;
private static final int PIPE_SPEED = 3;
private static final int SCREEN_WIDTH = 400;
private static final int SCREEN_HEIGHT = 600;
private static final int BIRD_RADIUS = 15;
// 状态变量
private double birdY, birdVelocity;
private double pipeX, pipeGapY;
private int score;
private boolean alive;
private Random rand;
public GameEnvironment() {
rand = new Random();
reset();
}
/**
* 重置游戏状态
*/
public void reset() {
birdY = SCREEN_HEIGHT / 2.0;
birdVelocity = 0;
pipeX = SCREEN_WIDTH;
pipeGapY = 150 + rand.nextInt(SCREEN_HEIGHT - 300);
score = 0;
alive = true;
}
/**
* 执行一帧游戏逻辑
* @param jump 是否跳跃
*/
public void step(boolean jump) {
if (!alive) return;
// 更新小鸟物理状态
if (jump) {
birdVelocity = JUMP_STRENGTH;
}
birdVelocity += GRAVITY;
birdY += birdVelocity;
// 更新管道位置
pipeX -= PIPE_SPEED;
if (pipeX < -PIPE_WIDTH) {
pipeX = SCREEN_WIDTH;
pipeGapY = 150 + rand.nextInt(SCREEN_HEIGHT - 300);
score++;
}
// 碰撞检测
if (birdY < BIRD_RADIUS || birdY > SCREEN_HEIGHT - BIRD_RADIUS) {
alive = false;
}
// 检测是否撞击管道
if (pipeX < 80 && pipeX + PIPE_WIDTH > 50) {
if (birdY - BIRD_RADIUS < pipeGapY - PIPE_GAP / 2.0 ||
birdY + BIRD_RADIUS > pipeGapY + PIPE_GAP / 2.0) {
alive = false;
}
}
}
/**
* 获取神经网络输入特征(已归一化)
*/
public double[] getInputs() {
double[] inputs = new double[4];
inputs[0] = (pipeGapY - PIPE_GAP / 2.0 - birdY) / SCREEN_HEIGHT; // 到上管道距离
inputs[1] = (birdY - (pipeGapY + PIPE_GAP / 2.0)) / SCREEN_HEIGHT; // 到下管道距离
inputs[2] = pipeX / SCREEN_WIDTH; // 水平距离
inputs[3] = birdVelocity / 10.0; // 垂直速度
return inputs;
}
/**
* 使用神经网络控制小鸟,运行完整一局并返回适应度
*/
public double evaluateFitness(NeuralNetwork nn) {
reset();
int frames = 0;
final int MAX_FRAMES = 10000; // 防止无限飞行
while (alive && frames < MAX_FRAMES) {
double[] inputs = getInputs();
double output = nn.feedForward(inputs);
step(output > 0.5);
frames++;
}
// 适应度 = 穿过的管道数 * 100 + 存活帧数(鼓励飞得更远)
return score * 100.0 + frames;
}
public boolean isAlive() { return alive; }
public int getScore() { return score; }
}
2.3 遗传算法引擎
import java.util.Arrays;
import java.util.Comparator;
/**
* 遗传算法引擎:管理种群进化过程
*/
public class GeneticEngine {
private static final Random RAND = new Random();
private final int populationSize;
private final double mutationRate;
private final double crossoverRate;
private final int eliteCount;
private final int geneLength;
private Individual[] population;
private GameEnvironment[] envs;
/**
* 个体封装:基因与适应度
*/
static class Individual {
double[] genes;
double fitness;
NeuralNetwork network;
Individual(int geneLength) {
genes = new double[geneLength];
for (int i = 0; i < geneLength; i++) {
genes[i] = RAND.nextDouble() * 2 - 1;
}
network = new NeuralNetwork(genes);
}
Individual(double[] genes) {
this.genes = genes.clone();
this.network = new NeuralNetwork(genes);
}
}
public GeneticEngine(int popSize, double mutRate, double crossRate, int elite) {
this.populationSize = popSize;
this.mutationRate = mutRate;
this.crossoverRate = crossRate;
this.eliteCount = elite;
this.geneLength = NeuralNetwork.getGeneLength();
population = new Individual[popSize];
envs = new GameEnvironment[popSize];
for (int i = 0; i < popSize; i++) {
population[i] = new Individual(geneLength);
envs[i] = new GameEnvironment();
}
}
/**
* 评估种群中所有个体的适应度
*/
public void evaluatePopulation() {
for (int i = 0; i < populationSize; i++) {
population[i].fitness = envs[i].evaluateFitness(population[i].network);
}
// 按适应度降序排序
Arrays.sort(population, Comparator.comparingDouble((Individual ind) -> ind.fitness).reversed());
}
/**
* 轮盘赌选择:按适应度比例选择父代
*/
private Individual rouletteSelect() {
double totalFitness = 0;
for (Individual ind : population) {
totalFitness += ind.fitness;
}
double pick = RAND.nextDouble() * totalFitness;
double current = 0;
for (Individual ind : population) {
current += ind.fitness;
if (current >= pick) {
return ind;
}
}
return population[populationSize - 1];
}
/**
* 单点交叉:交换两个父代的基因片段
*/
private Individual[] crossover(Individual p1, Individual p2) {
if (RAND.nextDouble() > crossoverRate) {
return new Individual[]{new Individual(p1.genes), new Individual(p2.genes)};
}
int point = RAND.nextInt(geneLength);
double[] child1 = new double[geneLength];
double[] child2 = new double[geneLength];
System.arraycopy(p1.genes, 0, child1, 0, point);
System.arraycopy(p2.genes, point, child1, point, geneLength - point);
System.arraycopy(p2.genes, 0, child2, 0, point);
System.arraycopy(p1.genes, point, child2, point, geneLength - point);
return new Individual[]{new Individual(child1), new Individual(child2)};
}
/**
* 高斯变异:对基因添加小幅随机扰动
*/
private void mutate(Individual ind) {
for (int i = 0; i < geneLength; i++) {
if (RAND.nextDouble() < mutationRate) {
ind.genes[i] += RAND.nextGaussian() * 0.3;
// 限制权重范围
ind.genes[i] = Math.max(-3, Math.min(3, ind.genes[i]));
}
}
ind.network = new NeuralNetwork(ind.genes);
}
/**
* 进化一代:选择 -> 交叉 -> 变异
*/
public void evolve() {
Individual[] newPop = new Individual[populationSize];
// 保留精英个体直接进入下一代
for (int i = 0; i < eliteCount; i++) {
newPop[i] = new Individual(population[i].genes);
}
// 生成新个体填充剩余位置
int idx = eliteCount;
while (idx < populationSize) {
Individual p1 = rouletteSelect();
Individual p2 = rouletteSelect();
Individual[] children = crossover(p1, p2);
for (Individual child : children) {
if (idx >= populationSize) break;
mutate(child);
newPop[idx++] = child;
}
}
population = newPop;
}
public Individual getBest() {
return population[0];
}
public double getAverageFitness() {
double sum = 0;
for (Individual ind : population) {
sum += ind.fitness;
}
return sum / populationSize;
}
}
2.4 主程序与训练入口
/**
* Flappy Bird AI 训练主程序
*/
public class FlappyBirdAI {
public static void main(String[] args) {
final int POPULATION_SIZE = 100;
final int GENERATIONS = 200;
final double MUTATION_RATE = 0.05;
final double CROSSOVER_RATE = 0.7;
final int ELITE_COUNT = 5;
GeneticEngine engine = new GeneticEngine(
POPULATION_SIZE, MUTATION_RATE, CROSSOVER_RATE, ELITE_COUNT
);
System.out.println("=== Flappy Bird 遗传算法训练开始 ===");
System.out.println("种群大小: " + POPULATION_SIZE);
System.out.println("进化代数: " + GENERATIONS);
System.out.println();
for (int gen = 1; gen <= GENERATIONS; gen++) {
engine.evaluatePopulation();
GeneticEngine.Individual best = engine.getBest();
if (gen % 10 == 0 || gen == 1) {
System.out.printf("第 %3d 代 | 最优适应度: %.1f | 平均适应度: %.1f | 最优得分: %d%n",
gen, best.fitness, engine.getAverageFitness(),
new GameEnvironment().evaluateFitness(best.network) / 100);
}
// 如果已经能稳定穿过50个管道,提前终止
if (best.fitness > 5000) {
System.out.println("\n>>> 训练收敛,提前终止!");
break;
}
engine.evolve();
}
// 最终评估
engine.evaluatePopulation();
GeneticEngine.Individual champion = engine.getBest();
System.out.println("\n=== 训练完成 ===");
System.out.println("冠军适应度: " + champion.fitness);
// 打印冠军网络的权重(可用于持久化保存)
System.out.println("冠军基因编码:");
double[] genes = champion.network.toGenes();
for (int i = 0; i < Math.min(genes.length, 10); i++) {
System.out.printf(" gene[%d] = %.4f%n", i, genes[i]);
}
System.out.println(" ... (共 " + genes.length + " 个基因)");
}
}
三、算法复杂度分析
| 阶段 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 神经网络前向传播 | O(I×H + H×O) = O(30) | O(I+H+O) | I=4, H=6, O=1,计算量极小 |
| 单局游戏模拟 | O(F) | O(1) | F 为存活帧数,每帧固定操作 |
| 种群适应度评估 | O(P × F × 30) | O(P) | P 为种群大小,可并行优化 |
| 单代进化(选择+交叉+变异) | O(P × G) | O(P × G) | G 为基因长度(37) |
其中 P 为种群规模,典型值 50~200。整体训练时间主要取决于每局游戏模拟的帧数,对于简单场景可在数秒内完成收敛。
四、关键优化技巧
- Xavier 权重初始化:避免初始权重过大导致 Sigmoid 饱和,加速早期进化
- 精英保留策略:每代保留最优个体不变异,防止优秀基因丢失
- 高斯变异:相比均匀变异,高斯扰动能更精细地搜索局部最优
- 适应度函数设计:同时考虑得分和存活帧数,避免局部最优(如悬停在安全区域不前进)
- 权重截断:将权重限制在 [-3, 3] 范围内,防止数值溢出
五、总结
本文通过 Java 实现了一套完整的 Flappy Bird AI 训练系统,核心在于将神经网络控制策略编码为基因序列,通过遗传算法的选择、交叉和变异操作在多代迭代中不断优化。整个系统不依赖梯度反向传播,完全基于进化策略搜索最优权重,对于非可导或奖励稀疏的问题尤为适用。读者可以在此基础上尝试调整网络拓扑(如增加隐藏层)、改变选择策略(如锦标赛选择)或引入共进化机制,进一步探索神经进化算法的潜力。