Skip to content

C++: Behavior Trees + CharacterController + Timelines

Choose a tag to compare

@coinbump coinbump released this 03 Nov 05:12
· 64 commits to main since this release

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, TimeTrack for animating objects across a timeline using keyframes (interpolated or discrete)
  • AnimatedSpriteRenderer improvements (uses timeline for frames now for more complex animations)
  • New: CharacterController, BehaviorNode, for building complex character behaviors
  • New: SimpleAnimationController for linking states to frame animations (2D)
  • New: ComponentsController for 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;