每日算法 — 使用java实现饥荒:生存资源调度与有限状态机季节循环

《饥荒》是一款以生存为核心的开放世界游戏,玩家需要在昼夜更替、季节变换和饥饿威胁中收集资源并维持生命。本文用 Java 实现饥荒的核心生存模拟系统,重点讲解有限状态机(FSM)驱动的季节循环、基于优先级的资源调度算法,以及饥饿-理智-血量三属性衰减模型。读者将掌握如何用状态机管理复杂游戏逻辑,并理解生存游戏中资源平衡的设计思路。

一、饥荒生存系统概述

饥荒的生存机制可以抽象为以下核心循环:

  1. 时间推进:游戏世界以固定速率流逝,分为白天、黄昏、夜晚三个阶段。
  2. 季节循环:春夏秋冬四季轮转,每季持续固定天数,带来不同环境效果(如冬季降温、夏季自燃)。
  3. 属性衰减:玩家拥有饥饿值(Hunger)、理智值(Sanity)、血量(Health),三者随时间和行为不断变化。
  4. 资源采集与消耗:玩家通过采集、狩猎、种植获取食物和材料,用于制作工具、建造营地和维持属性。
  5. 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 决策。关键算法在于状态转换条件的分层设计和多属性权衡的评分函数。在此基础上,读者可以扩展建筑系统、农场种植周期、以及更复杂的生物群落生态模拟。