Repository navigation
C++: Behavior Trees + CharacterController + Timelines
What's New in v2.41
C++:
- Lots of QOL improvements: Moved SDL out of the Xcode project, and we’re now using the framework from the frameworks folder (results in faster build times)
- New:
Timeline,TimeTrackfor animating objects across a timeline using keyframes (interpolated or discrete) AnimatedSpriteRendererimprovements (uses timeline for frames now for more complex animations)- New:
CharacterController,BehaviorNode, for building complex character behaviors - New:
SimpleAnimationControllerfor linking states to frame animations (2D) - New:
ComponentsControllerfor turning effects on and off based on states - QuickBuilder, QuickTimeline improvements for creating complex characters with effects and animations
- Better imGui painters
- EditorWorldSystem + editor imGui painters for adding editor UI to your game/app
How CharacterController/Behavior Trees Work
You can now build complex characters using a combination of behavior trees and controllers.
Example:
auto bossPrefab = MAKE<Prefab>([=](auto& node) {
QuickBuild(node)
.Named("boss")
.CycleHueEffect("effect.cycle", SwitchState::Off)
.SetFrameRate(24)
.SetCycleType(AnimationCycleType::Once)
.With<VelocityKSteering>()
.With<SimpleAnimationController>()
.With<Emitter>(bossBulletSpawnFunc, 0)
.AnimationStateFrames("default", {{"boss-1"}})
.AnimationStateFrames("launch", {
{.textureId = "boss-6"},
{.textureId = "boss-7", .deltaFrame = 2}
})
.AnimationStateFrames("jump", {
{.textureId = "boss-7"},
{.textureId = "boss-8", .deltaFrame = 2},
})
.AnimationStateFrames("land", {
{.textureId = "boss-5"},
{.textureId = "boss-6", .deltaFrame = 2},
{.textureId = "boss-9", .deltaFrame = 3},
{.textureId = "boss-9", .deltaFrame = 11},
})
.AnimationStateFrames("fire", {
{.textureId = "boss-3"},
{.textureId = "boss-4", .deltaFrame = 3},
{.textureId = "boss-4", .deltaTime = 0.5},
})
.AnimationStateTransitions({
{ .input = "launch", .toState = "launch"},
{ .state = "launch", .input = "next", .toState = "jump"},
{ .state = "jump", .input = "next", .toState = "land"},
{ .state = "land", .input = "next", .toState = "default"},
})
.SetAnimationState("default")
.WithOnEnable("effect.default", [](WorldComponent<>& component) {
component.Target()->TypeComponent<SomeRenderer>()->SetColor(Color::white);
})
.ComponentsState("default", {"effect.default"})
.ComponentsState("rainbow", {"effect.cycle"})
.SetComponentsState("default")
.CharacterStateBehavior("demo", [&](auto& controller) {
UP<BehaviorNode> root = NEW<BehaviorNode>("Root", RunType::Repeat);
root->_diagnose.EnableIfDebug();
QuickBehavior(*root, *controller.owner)
.DiagnoseIfDebug()
.AndSequence()
.AnimationState("default")
.Delay("", 2)
.AnimationState("launch")
.Delay("", 2)
.AnimationState("jump")
.Delay("", 2)
.AnimationState("land")
.Delay("", 2);
return root;
})
.CharacterStateBehavior("start", [&](CharacterController& controller) {
UP<BehaviorNode> root = NEW<BehaviorNode>("Root", RunType::Repeat);
root->_diagnose.EnableIfDebug();
QuickBehavior(*root, *controller.owner)
.DiagnoseIfDebug()
.AndRandom()
// Random choice
.AndSequence("Sequence: Fire-Wait")
.AndParallel("Parallel: Fire")
.AnimationState("fire")
.Action("Fire emit", [&](auto& behavior) {
auto _emitFunc = EmitFuncs::MakeVaryAngle(10, Angle::DegreesAngle(90), Angle::DegreesAngle(30));
auto emitFunc = EmitFuncs::AddVaryDirectionVelocity(_emitFunc, 300, 30);
auto emitter = controller.owner->TypeComponent<Emitter>();
emitter->emitFunc = emitFunc;
emitter->Fire();
})
.Timeline("Shake", [&]() {
Timeline::Config config{.duration = 0.5f, .cycleType = AnimationCycleType::Once};
auto result = NEW<Timeline>(config);
QuickTimeline(*result, *controller.Target())
.Shake2D(20, 3);
return result;
})
.Pop()
.AnimationState("default")
.Delay("Delay: Short Jump", 2)
.Pop()
// Random choice
.AndRepeat("Repeat: Short Jump", 3)
.AndSequence("Sequence: Short jump")
.AnimationState("launch")
.AnimationInput("next")
.ComponentsState("rainbow")
.AndParallel("Parallel: Short jump", 1)
.Timeline("Short Jump", [&]() {
Timeline::Config config{.duration = 0.7f};
auto result = NEW<Timeline>(config);
StandardRandom random;
bool moveRight = random.Choice(2) == 1;
float xFactor = moveRight ? vecRight : vecLeft;
auto destX = 100 * xFactor;
auto destY = random.Delta(100);
QuickTimeline(*result, *controller.owner)
.MoveX({0, destX / 2.0f})
.MoveY({0, destY + 100 * vecUp, destY });
return result;
})
.Timeline("Wobble", [&]() {
Timeline::Config config{.duration = 1, .cycleType = AnimationCycleType::PingPong};
auto result = NEW<Timeline>(config);
QuickTimeline(*result, *controller.Target())
.Wobble2D(1.0f, 0.2f);
return result;
})
.Pop()
.ComponentsState("default")
.AnimationInput("next")
.AnimationState("default")
.Delay("", 1);
return root;
})
.SetCharacterState("start")
.Drag();
});
worldPtr->prefabs["boss"] = bossPrefab;