-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rework monster decision code #109: continuous task
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
Showing
7 changed files
with
179 additions
and
164 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 77 additions & 47 deletions
124
game/src/main/kotlin/jake2/game/character/BehaviorTree.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.