Implementing a Finite State Machine for Zombie Behavior in Plants vs. Zombies
Abstract State Class Implemantation
The foundation of zombie behavior relies on a abstract state class that defines core functionality shared across all states.
package step3.CharacterSystem.ZombieFSMSystem;
import step3.CharacterSystem.Character.ICharacter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class IZombieState {
protected Map<EnemyTransition, EnemyStateID> transitionMap = new HashMap<>();
protected EnemyStateID stateIdentifier;
protected ICharacter entity;
protected ZombieFSM stateMachine;
public IZombieState(ZombieFSM machine, ICharacter character) {
stateMachine = machine;
entity = character;
}
public void registerTransition(EnemyTransition transition, EnemyStateID targetState) {
if (transition == EnemyTransition.NullTansition || targetState == EnemyStateID.NullState || transitionMap.containsKey(transition)) {
return;
}
transitionMap.put(transition, targetState);
}
public void removeTransition(EnemyTransition transition) {
transitionMap.remove(transition);
}
public EnemyStateID getNextState(EnemyTransition transition) {
return transitionMap.getOrDefault(transition, EnemyStateID.NullState);
}
public abstract void onEnter();
public abstract void onExit();
public abstract void evaluateConditions(List<ICharacter> targets);
public abstract void executeActions(List<ICharacter> targets);
public EnemyStateID getStateId() {
return stateIdentifier;
}
}
Combat State Implementation
The attack state manages zombie combat behavior including damage calculation and target validation.
package step3.CharacterSystem.ZombieFSMSystem;
import step3.CharacterSystem.Character.ICharacter;
import java.util.List;
public class ZombieAttackState extends IZombieState {
private float combatInterval = 1;
private float combatTimer = 0;
public ZombieAttackState(ZombieFSM machine, ICharacter character) {
super(machine, character);
stateIdentifier = EnemyStateID.Attack;
combatTimer = combatInterval;
}
@Override
public void onEnter() {}
@Override
public void onExit() {}
@Override
public void executeActions(List<ICharacter> targets) {
if (targets == null || targets.isEmpty()) return;
combatTimer += 0.1f;
if (combatTimer >= combatInterval) {
processTargets(targets);
combatTimer = 0;
}
}
private void processTargets(List<ICharacter> targets) {
for(ICharacter target : targets) {
if (target.getPosRow() == entity.getPosRow()) {
int horizontalDistance = entity.getPosition().x - target.getPosition().x;
if (horizontalDistance <= entity.getAttr().getmBaseAttr().getAtkdistance() && horizontalDistance >= 0) {
entity.Attack(target);
}
}
}
}
@Override
public void evaluateConditions(List<ICharacter> targets) {
if (targets == null || targets.isEmpty()) {
stateMachine.PerformTransition(EnemyTransition.LostSoldier);
return;
}
if (!isTargetInRange(targets)) {
stateMachine.PerformTransition(EnemyTransition.LostSoldier);
}
}
private boolean isTargetInRange(List<ICharacter> targets) {
for (ICharacter target : targets) {
if (target.getPosRow() == entity.getPosRow()) {
int distance = entity.getPosition().x - target.getPosition().x;
if (distance >= 0 && distance <= entity.getAttr().getmBaseAttr().getAtkdistance()) {
return true;
}
}
}
return false;
}
}
Movement State Implementation
The chase state handles zombie movement toward targets and determines when to transition to attacking.
package step3.CharacterSystem.ZombieFSMSystem;
import step3.CharacterSystem.Character.ICharacter;
import java.awt.*;
import java.util.List;
public class ZombieChaseState extends IZombieState {
private Point destinationPoint;
public ZombieChaseState(ZombieFSM machine, ICharacter character) {
super(machine, character);
stateIdentifier = EnemyStateID.Chase;
}
@Override
public void onEnter() {
destinationPoint = new Point(50, entity.getPosition().y);
}
@Override
public void onExit() {}
public void executeActions(List<ICharacter> targets) {
if (entity.getPosition().x > destinationPoint.x) {
entity.MoveTo(destinationPoint.x);
}
}
@Override
public void evaluateConditions(List<ICharacter> targets) {
if (targets != null && !targets.isEmpty()) {
int closestTarget = findClosestTarget(targets);
if (closestTarget <= entity.getAttr().getmBaseAttr().getAtkdistance()) {
stateMachine.PerformTransition(EnemyTransition.CanAttack);
}
}
}
private int findClosestTarget(List<ICharacter> targets) {
int minimumDistance = Integer.MAX_VALUE;
for (ICharacter target : targets) {
if(target.getPosRow() == entity.getPosRow()) {
int currentDistance = entity.getPosition().x - target.getPosition().x;
if (minimumDistance > currentDistance && currentDistance >= 0) {
minimumDistance = currentDistance;
}
}
}
return minimumDistance;
}
}
State Machine Controller
The finite state machine orchestrates state transitions and manages the active state lifecycle.
package step3.CharacterSystem.ZombieFSMSystem;
import java.util.ArrayList;
import java.util.List;
public class ZombieFSM {
private List<IZombieState> availableStates = new ArrayList<>();
private IZombieState activeState;
public IZombieState getCurrentState() { return activeState; }
public void registerStates(IZombieState[] states) {
for(IZombieState state : states) {
registerState(state);
}
}
public void registerState(IZombieState state) {
if (state == null) return;
if (availableStates.isEmpty()) {
initializeFirstState(state);
return;
}
if (!stateExists(state.getStateId())) {
availableStates.add(state);
}
}
private void initializeFirstState(IZombieState state) {
availableStates.add(state);
activeState = state;
activeState.onEnter();
}
private boolean stateExists(EnemyStateID stateId) {
return availableStates.stream().anyMatch(s -> s.getStateId() == stateId);
}
public void removeState(EnemyStateID stateID) {
if (stateID == EnemyStateID.NullState) return;
availableStates.removeIf(state -> state.getStateId() == stateID);
}
public void PerformTransition(EnemyTransition transition) {
if (transition == EnemyTransition.NullTansition) return;
EnemyStateID nextIdentifier = activeState.getNextState(transition);
if (nextIdentifier == EnemyStateID.NullState) return;
switchToState(nextIdentifier);
}
private void switchToState(EnemyStateID targetStateId) {
for (IZombieState state : availableStates) {
if (state.getStateId() == targetStateId) {
activeState.onExit();
activeState = state;
activeState.onEnter();
return;
}
}
}
}
Zombie Entity Integration
The concrete zombie implementation integrates the state machine with character behavior.
package step3.CharacterSystem.Character;
import step3.CharacterSystem.Attr.CharacterAttr;
import step3.CharacterSystem.ZombieFSMSystem.*;
import java.awt.*;
import java.util.List;
public class ZombieNormal extends ICharacter {
private ZombieFSM behaviorController;
public ZombieNormal(CharacterAttr attributes, Point location, int rowPosition) {
super(attributes, location, rowPosition);
attackimg = "/data/workspace/myshixun/images/Zombies/Zombie/ZombieAttack.gif";
chaseimg = "/data/workspace/myshixun/images/Zombies/Zombie/Zombie.gif";
GetImageSize(chaseimg);
}
@Override
public void Killed() {}
@Override
public void MakeFSM() {
behaviorController = new ZombieFSM();
ZombieChaseState pursuitState = new ZombieChaseState(behaviorController, this);
pursuitState.registerTransition(EnemyTransition.CanAttack, EnemyStateID.Attack);
ZombieAttackState combatState = new ZombieAttackState(behaviorController, this);
combatState.registerTransition(EnemyTransition.LostSoldier, EnemyStateID.Chase);
behaviorController.registerState(pursuitState);
behaviorController.registerState(combatState);
}
@Override
public void UpdateFSMAI(List<ICharacter> targets) {
if (mIsKilled) return;
behaviorController.getCurrentState().evaluateConditions(targets);
behaviorController.getCurrentState().executeActions(targets);
}
@Override
public void Attack(ICharacter opponent) {
PlayAnim(attackimg);
opponent.UnderAttack(attr.getmBaseAttr().getDamage());
}
}