Skip to content

Commit

Permalink
Rework monster decision code #109: continuous task
Browse files Browse the repository at this point in the history
Leaf nodes now can return a continuous task: `BtActionTask`(for a certain number of frames) and `BtWaitingTask` (until an event is received).
 This is a way of saving a state of the BT which was completely stateless before (previously each execution of BT could result in a different decision every frame, even though the character's state machine is unlikely to change so quickly).

 BT is now wrapped into a `BtContext`, which is a combination of a BT and a current task (if exists).

 As an example, when the BT decides to initiate an attack, it would keep this decision until the attack is finished.
  • Loading branch information
demoth committed Jan 16, 2024
1 parent 0c21986 commit 51a330c
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 164 deletions.
4 changes: 2 additions & 2 deletions game/src/main/java/jake2/game/SubgameEntity.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package jake2.game;

import jake2.game.adapters.*;
import jake2.game.character.BehaviorTree;
import jake2.game.character.BtContext;
import jake2.game.character.GameCharacter;
import jake2.game.components.ThinkComponent;
import jake2.game.items.GameItem;
Expand Down Expand Up @@ -232,7 +232,7 @@ public SubgameEntity(int i) {
public Map<String, Object> components = new HashMap<>();

public GameCharacter character; // todo: move to a component
public BehaviorTree controller; // todo: move to a component?
public BtContext btContext; // todo: move to a component?

// todo: replace with a constructor call?
void G_InitEdict(int i) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ open class AnimationSequenceState(
eventProcessor.process(listOf(zeroEvent))
}

override fun update(time: Float): String? {
override fun update(time: Float): Pair<String?, Collection<String>> {
val events = animationSequence.update(time)
eventProcessor.process(events)
if (animationSequence.finished)
return nextState
return nextState to events

return null
return null to emptySet()
}

override val currentFrame: Int
Expand Down
124 changes: 77 additions & 47 deletions game/src/main/kotlin/jake2/game/character/BehaviorTree.kt
Original file line number Diff line number Diff line change
@@ -1,86 +1,116 @@
package jake2.game.character


enum class BtNodeState {
Success,
Failure,
Running
sealed class BtNodeResult

/**
* Represents a logical outcome of an internal behaviour tree node. Used to control the execution flow (see sequence & selector).
*/
internal class BtFinalResult(val success: Boolean) : BtNodeResult()

/**
* Represents a decision that the behavior tree has made - an action that can be executed by the character.
* Usually the last node of a sequence (leaf).
*/
abstract class BtTask<T>(val action: T): BtNodeResult()
class BtActionTask<T>(action: T, var duration: Int = 1): BtTask<T>(action)

class BtWaitingTask<T>(action: T, val waitingFor: String): BtTask<T>(action)

class BtContext(
private val tree: BehaviorTree
) {
private var currentTask: BtNodeResult? = null

fun reset() {
currentTask = null
}

fun update(events: Collection<String>): BtNodeResult? {
println("Bt events: $events")
val task = currentTask
currentTask = if (task != null) {
when (task) {
is BtActionTask<*> -> {
if (task.duration > 0) {
task.duration--
task
} else {
tree.run()
}
}
is BtWaitingTask<*> -> {
if (task.waitingFor in events) {
tree.run()
} else {
task
}
}
// fixme: these outcomes are errors
is BtFinalResult -> null
is BtTask<*> -> null
}
} else {
tree.run()
}
return currentTask
}
}


interface BehaviorTree {
fun run(): BtNodeState
fun run(): BtNodeResult
}
abstract class BhAbstractNode(protected val nodes: List<BhAbstractNode>): BehaviorTree {
abstract override fun run(): BtNodeState
abstract override fun run(): BtNodeResult
}

/**
* Executes all nodes until a failure, then returns failure. Returns success otherwise
* Executes all nodes until a failure or a task outcome, then returns. Returns success otherwise.
* Used to represent a series of nodes related to a certain task.
* Usually consists of checks returning a [BtFinalResult] and a terminal node with a [BtActionTask] decision.
*/
class BhSequence(vararg nodes: BhAbstractNode) : BhAbstractNode(nodes.asList()) {
override fun run(): BtNodeState {
override fun run(): BtNodeResult {
nodes.forEach {
var result = it.run()
while (result == BtNodeState.Running) {
result = it.run()
when (val result = it.run()) {
is BtFinalResult -> if (!result.success) return result
is BtTask<*> -> return result
}
if (result == BtNodeState.Failure) return BtNodeState.Failure
}
return BtNodeState.Success
return BtFinalResult(true)
}
}

/**
* Executes all nodes until a success, then returns success. Returns failure otherwise
* Executes all nodes until a success or a task outcome, then returns. Returns failure otherwise.
* Usually aggregates a group of sequences and defines priorities between them.
*/
class BhSelector(vararg nodes: BhAbstractNode) : BhAbstractNode(nodes.asList()) {
override fun run(): BtNodeState {
override fun run(): BtNodeResult {
nodes.forEach {
var result = it.run()
while (result == BtNodeState.Running) {
result = it.run()
when (val result = it.run()) {
is BtFinalResult -> if (result.success) return result
is BtTask<*> -> return result
}
if (result == BtNodeState.Success) return BtNodeState.Success
}
return BtNodeState.Failure
return BtFinalResult(false)
}
}

/**
* Leaf node, Can be condition or action (side effect)
*/
class BtNode(private val condition: () -> Boolean) : BhAbstractNode(emptyList()) {
override fun run(): BtNodeState {
return if (condition.invoke())
BtNodeState.Success
else
BtNodeState.Failure
}
class BtConditionNode(private val condition: () -> Boolean) : BhAbstractNode(emptyList()) {
override fun run(): BtNodeResult = BtFinalResult(condition.invoke())
}

class BtEventNode(private val event: String, private val condition: () -> Boolean): BhAbstractNode(emptyList()) {
val events: Set<String> = TODO()

override fun run(): BtNodeState {
return if (!events.contains(event))
BtNodeState.Running
else if (condition.invoke())
BtNodeState.Success
else
BtNodeState.Failure
}
class BtLeafNode(private val action: () -> BtTask<*>) : BhAbstractNode(emptyList()) {
override fun run(): BtNodeResult = action.invoke()
}

// short names (come up with better names?)
fun selector(vararg nodes: BhAbstractNode) = BhSelector(*nodes)

fun sequence(vararg nodes: BhAbstractNode) = BhSequence(*nodes)

fun check(condition: () -> Boolean) = BtNode(condition)

fun run(condition: () -> Unit) = BtNode { condition.invoke(); true }
fun check(condition: () -> Boolean) = BtConditionNode(condition)

fun runUntil(event: String, condition: () -> Unit) = BtEventNode(event) {
condition.invoke(); true
}
fun run(action: () -> BtTask<*>) = BtLeafNode(action)
Loading

0 comments on commit 51a330c

Please sign in to comment.