《饥荒》是一款以生存为核心的开放世界游戏,玩家需要在昼夜更替、季节变换和饥饿威胁中收集资源并维持生命。本文用 Java 实现饥荒的核心生存模拟系统,重点讲解有限状态机(FSM)驱动的季节循环、基于优先级的资源调度算法,以及饥饿-理智-血量三属性衰减模型。读者将掌握如何用状态机管理复杂游戏逻辑,并理解生存游戏中资源平衡的设计思路。
一、饥荒生存系统概述
饥荒的生存机制可以抽象为以下核心循环:
- 时间推进:游戏世界以固定速率流逝,分为白天、黄昏、夜晚三个阶段。
- 季节循环:春夏秋冬四季轮转,每季持续固定天数,带来不同环境效果(如冬季降温、夏季自燃)。
- 属性衰减:玩家拥有饥饿值(Hunger)、理智值(Sanity)、血量(Health),三者随时间和行为不断变化。
- 资源采集与消耗:玩家通过采集、狩猎、种植获取食物和材料,用于制作工具、建造营地和维持属性。
- AI 行为树:生物(如猪人、蜘蛛、野牛)根据昼夜和季节切换行为模式。
二、数据模型
import java.util.*;
/**
* 季节枚举
*/
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
/**
* 时间段
*/
enum DayPhase {
DAY, DUSK, NIGHT
}
/**
* 可采集资源
*/
class Resource {
String name;
int amount;
ResourceType type;
public Resource(String name, int amount, ResourceType type) {
this.name = name;
this.amount = amount;
this.type = type;
}
}
enum ResourceType {
FOOD, FUEL, MATERIAL, TOOL
}
/**
* 玩家属性
*/
class Survivor {
double hunger = 100; // 饥饿值 0-150
double sanity = 100; // 理智值 0-200
double health = 100; // 血量 0-100
List<Resource> inventory = new ArrayList<>();
public boolean isAlive() {
return health > 0;
}
}
/**
* 生物 AI
*/
class Creature {
String name;
double health;
double damage;
boolean isHostile;
CreatureState state = CreatureState.IDLE;
enum CreatureState {
IDLE, WANDER, CHASE, FLEE, SLEEP
}
public Creature(String name, double health, double damage, boolean isHostile) {
this.name = name;
this.health = health;
this.damage = damage;
this.isHostile = isHostile;
}
}
三、有限状态机:季节与昼夜循环
季节和昼夜是饥荒世界运转的核心。我们用有限状态机管理季节转换,并在每个状态中定义不同的环境参数:
class WorldClock {
private int day = 1;
private int dayInSeason = 1;
private Season season = Season.AUTUMN; // 饥荒默认从秋季开始
private DayPhase phase = DayPhase.DAY;
private int phaseTicks = 0;
// 每季节天数
private static final int SEASON_LENGTH = 15;
// 每阶段刻数(简化:一天 30 刻,每阶段 10 刻)
private static final int PHASE_LENGTH = 10;
/**
* 推进一帧(一刻)
*/
public void tick() {
phaseTicks++;
if (phaseTicks >= PHASE_LENGTH) {
phaseTicks = 0;
switch (phase) {
case DAY -> phase = DayPhase.DUSK;
case DUSK -> phase = DayPhase.NIGHT;
case NIGHT -> {
phase = DayPhase.DAY;
day++;
dayInSeason++;
if (dayInSeason > SEASON_LENGTH) {
dayInSeason = 1;
transitionSeason();
}
}
}
}
}
private void transitionSeason() {
season = switch (season) {
case AUTUMN -> Season.WINTER;
case WINTER -> Season.SPRING;
case SPRING -> Season.SUMMER;
case SUMMER -> Season.AUTUMN;
};
}
public Season getSeason() { return season; }
public DayPhase getPhase() { return phase; }
public int getDay() { return day; }
public int getDayInSeason() { return dayInSeason; }
/**
* 获取当前环境温度(影响玩家体温)
*/
public double getAmbientTemperature() {
return switch (season) {
case SPRING -> 15 + 5 * Math.sin(dayInSeason * Math.PI / SEASON_LENGTH);
case SUMMER -> 35 + 10 * Math.sin(dayInSeason * Math.PI / SEASON_LENGTH);
case AUTUMN -> 10 + 8 * Math.sin(dayInSeason * Math.PI / SEASON_LENGTH);
case WINTER -> -10 - 15 * Math.sin(dayInSeason * Math.PI / SEASON_LENGTH);
};
}
@Override
public String toString() {
return String.format("Day %d (%s, day %d of season) - %s", day, season, dayInSeason, phase);
}
}
四、三属性衰减与恢复模型
玩家的饥饿、理智、血量每帧根据环境状态和行为进行更新:
class SurvivalEngine {
/**
* 更新玩家属性
* @param player 玩家
* @param clock 世界时钟
* @param isNearFire 玩家是否在火源旁(影响理智和体温)
*/
public void updateAttributes(Survivor player, WorldClock clock, boolean isNearFire) {
double dt = 1.0; // 每刻为单位时间
// 1. 饥饿衰减:基础 1.0/刻,冬季额外 0.2
double hungerDecay = 1.0;
if (clock.getSeason() == Season.WINTER) hungerDecay += 0.2;
player.hunger = Math.max(0, player.hunger - hungerDecay * dt);
// 2. 理智衰减:夜晚 -1.5/刻,雨天(春季概率)-0.5,靠近火源 +0.5
double sanityDecay = 0;
if (clock.getPhase() == DayPhase.NIGHT) sanityDecay -= 1.5;
if (clock.getSeason() == Season.SPRING && Math.random() < 0.3) sanityDecay -= 0.5;
if (isNearFire) sanityDecay += 0.5;
player.sanity = Math.max(0, Math.min(200, player.sanity + sanityDecay * dt));
// 3. 血量恢复/衰减
if (player.hunger <= 0) {
player.health -= 1.0 * dt; // 饥饿时掉血
} else if (player.hunger > 80 && player.sanity > 50) {
player.health = Math.min(100, player.health + 0.2 * dt); // 状态良好时缓慢回血
}
// 4. 体温伤害(简化版)
double temp = clock.getAmbientTemperature();
if (!isNearFire && temp < 0) {
player.health -= 0.5 * dt; // 寒冷伤害
}
}
/**
* 进食恢复
*/
public void eat(Survivor player, Resource food) {
if (food.type != ResourceType.FOOD || food.amount <= 0) return;
double hungerRestore = food.amount * 1.5;
double healthRestore = food.amount * 0.3;
double sanityRestore = food.amount * 0.1;
player.hunger = Math.min(150, player.hunger + hungerRestore);
player.health = Math.min(100, player.health + healthRestore);
player.sanity = Math.min(200, player.sanity + sanityRestore);
food.amount = 0;
}
}
五、基于优先级的资源调度
当玩家拥有多种资源时,需要按优先级决定先消耗哪种食物、先制作哪种工具。我们实现一个简单的贪心优先级调度器:
class ResourceScheduler {
/**
* 根据当前状态选择最优食物
* 优先级:血量低时优先回血食物 > 理智低时优先回理智 > 饥饿低时优先高饱食度
*/
public Resource selectBestFood(Survivor player, List<Resource> foods) {
Resource best = null;
double bestScore = Double.NEGATIVE_INFINITY;
for (Resource f : foods) {
if (f.amount <= 0 || f.type != ResourceType.FOOD) continue;
double score = 0;
// 基础饱食度价值
score += f.amount * 1.0;
// 紧急需求加成
if (player.health < 30) score += f.amount * 2.0; // 假设高血量恢复
if (player.sanity < 30) score += f.amount * 1.5;
if (player.hunger < 20) score += f.amount * 1.8;
if (score > bestScore) {
bestScore = score;
best = f;
}
}
return best;
}
/**
* 判断当前应该执行什么行动(简化决策树)
*/
public Action decideAction(Survivor player, WorldClock clock, List<Resource> nearbyResources) {
// 1. 血量危急:寻找治疗
if (player.health < 20) {
return Action.FIND_HEALING;
}
// 2. 夜晚且无火:必须生火
if (clock.getPhase() == DayPhase.NIGHT) {
boolean hasFire = nearbyResources.stream().anyMatch(r -> r.name.equals("Fire"));
if (!hasFire) return Action.BUILD_FIRE;
}
// 3. 饥饿低:寻找食物
if (player.hunger < 40) {
return Action.GATHER_FOOD;
}
// 4. 理智低:靠近火源或采集花朵
if (player.sanity < 40) {
return Action.RESTORE_SANITY;
}
// 5. 默认:探索/采集资源
return Action.EXPLORE;
}
enum Action {
FIND_HEALING, BUILD_FIRE, GATHER_FOOD, RESTORE_SANITY, EXPLORE
}
}
六、生物 AI:基于昼夜的状态机
class CreatureAI {
/**
* 根据世界状态更新生物行为
*/
public void updateCreature(Creature creature, WorldClock clock, Survivor player) {
double distToPlayer = Math.random() * 50; // 简化距离计算
switch (creature.state) {
case IDLE -> {
if (clock.getPhase() == DayPhase.NIGHT && !creature.isHostile) {
creature.state = Creature.CreatureState.SLEEP;
} else if (distToPlayer < 10 && creature.isHostile) {
creature.state = Creature.CreatureState.CHASE;
} else if (Math.random() < 0.3) {
creature.state = Creature.CreatureState.WANDER;
}
}
case WANDER -> {
if (distToPlayer < 10 && creature.isHostile) {
creature.state = Creature.CreatureState.CHASE;
} else if (Math.random() < 0.2) {
creature.state = Creature.CreatureState.IDLE;
}
}
case CHASE -> {
if (distToPlayer > 20) {
creature.state = Creature.CreatureState.IDLE;
} else if (creature.health < 20) {
creature.state = Creature.CreatureState.FLEE;
}
}
case FLEE -> {
if (distToPlayer > 30 || creature.health > 50) {
creature.state = Creature.CreatureState.IDLE;
}
}
case SLEEP -> {
if (clock.getPhase() == DayPhase.DAY) {
creature.state = Creature.CreatureState.IDLE;
}
}
}
}
}
七、完整运行示例
public class DontStarveDemo {
public static void main(String[] args) {
WorldClock clock = new WorldClock();
Survivor player = new Survivor();
SurvivalEngine engine = new SurvivalEngine();
ResourceScheduler scheduler = new ResourceScheduler();
// 初始化 inventory
player.inventory.add(new Resource("Berries", 3, ResourceType.FOOD));
player.inventory.add(new Resource("Carrot", 2, ResourceType.FOOD));
player.inventory.add(new Resource("Meat", 1, ResourceType.FOOD));
System.out.println("=== Don't Starve Simulation ===");
for (int tick = 0; tick < 120 && player.isAlive(); tick++) {
clock.tick();
boolean nearFire = (clock.getPhase() == DayPhase.NIGHT && tick > 50);
engine.updateAttributes(player, clock, nearFire);
// 自动进食决策
if (player.hunger < 50) {
Resource food = scheduler.selectBestFood(player, player.inventory);
if (food != null) {
engine.eat(player, food);
}
}
if (tick % 10 == 0) {
System.out.printf("Tick %3d | %s | Hunger=%.1f Sanity=%.1f Health=%.1f | Action=%s%n",
tick, clock, player.hunger, player.sanity, player.health,
scheduler.decideAction(player, clock, player.inventory));
}
}
System.out.println("Survived? " + player.isAlive());
}
}
八、复杂度分析
- 世界时钟更新:$O(1)$ 每刻。
- 属性更新:$O(1)$ 每刻,仅涉及固定公式计算。
- 资源调度:$O(F)$,$F$ 为食物种类数。可用堆优化至 $O(\log F)$。
- 生物 AI:$O(C)$,$C$ 为生物数量。每个生物独立执行状态机切换。
九、总结
本文用 Java 构建了饥荒的核心生存模拟系统,通过有限状态机管理季节与昼夜,通过属性衰减模型营造生存压力,通过贪心优先级调度辅助 AI 决策。关键算法在于状态转换条件的分层设计和多属性权衡的评分函数。在此基础上,读者可以扩展建筑系统、农场种植周期、以及更复杂的生物群落生态模拟。