Candy Cane Games

Animation

How animation works

Animations in Candy Cane Games are built out of ordinary objects in your world, the same way everything else is. There is no separate animation editor to learn: you create a few objects, point them at each other, and press play.

Three ideas do all the work:

  • A Skeleton full of SkJoint objects describes what can move.
  • A SkAnimation describes how the SkJoints move over time.
  • A SkAnimator plays a SkAnimation on a Skeleton.

The Skeleton itself is invisible. To see anything move, you attach visible objects (like the parts of a RigidGroup) to the SkJoints — that is the wiring step covered below.

The core object types

  • Skeleton — a container for SkJoint objects. It renders nothing itself; it only defines what can move.
  • SkJoint — one movable point. A SkJoint must live inside a Skeleton or inside another SkJoint, so you can build hierarchies like shoulder, elbow, wrist. Its bindPose is its resting location relative to its parent, and every movement in an animation is an offset from the bindPose.
  • SkAnimation — the definition of one animation clip. It has a duration and an interpolationStyle (Linear, EaseInOut, EaseIn, EaseOut, or Step for a stop-motion look).
  • SkKeyframe — a child of a SkAnimation that marks one moment in the clip with its time field.
  • SkKeyframePose — a child of a SkKeyframe that says where one SkJoint should be at that moment: jointName picks the SkJoint, and relativePose is the offset from that SkJoint's bindPose.
  • SkAnimator — the player. Point its skeletonId at the Skeleton and its skAnimationId at the animation. When several animators target the same Skeleton, they are blended in order of decreasing priority (0-10) until their weight values (0-100) reach a total of 100.
  • Clock — a small helper that tracks playback: paused or playing, shouldLoop, and speed. Each SkAnimator points at a clock through its clockId.

Create your first animation

Here is the full recipe, using a simple bobbing motion as an example:

  • Create a Skeleton, and add a SkJoint inside it named root. Set the SkJoint's bindPose to where the animated thing should rest.
  • Create a SkAnimation. Give it a duration of 2 and an interpolationStyle like EaseInOut.
  • Add SkKeyframe children to the animation with time set to 0, 1, and 2.
  • Inside each keyframe, add a SkKeyframePose with jointName set to root. Leave the poses at time 0 and time 2 at the resting position, and raise the one at time 1. Matching the first and last keyframes makes the loop seamless.
  • Create a Clock and set shouldLoop to true.
  • Create a SkAnimator. Set skeletonId to your Skeleton, skAnimationId to your animation, clockId to your clock, and weight to 100.

Once the clock is playing, the SkJoint bobs up and down forever. But SkJoints are invisible, so nothing appears to happen yet — the last step is to attach something visible.

Wiring it up to a rigid body

A RigidGroup is a bunch of objects that move together: every Ball, Brick, and Cylinder inside it is glued into one rigid body. It is the standard way to build characters and machines out of parts, and it is what you connect a Skeleton to.

Two fields do the wiring:

  • Set the rigid group's followSkeletonId to your Skeleton. This tells the rigid group which Skeleton drives it.
  • For each part that should move, add a SkAttachment. Set objectId to the part (a ball, brick, or cylinder in the rigid group), jointName to the name of the SkJoint it should follow, and offsetFromJoint to position the part relative to the SkJoint.

Attached parts move physically with their SkJoint, not just visually — they collide with and push other objects as the animation plays. There are AI tools in the editor that can automate this wiring for you.

If you only need visuals, a Mesh can also follow a Skeleton through its own followSkeletonId field, without any physics.

Controlling playback from a script

Everything above runs on its own once the clock is playing. Use ClockService to control playback and AnimationService to blend animations from a script:

  • ClockService.play(clockId, animationTimeSecs) starts a clock at the given time. Use 0 to play from the beginning.
  • ClockService.pause(clockId) pauses it.
  • ClockService.setSpeed(clockId, speed) changes the playback speed multiplier.
  • AnimationService.smoothlyChangeWeight(animatorId, targetWeight, durationMs, interpolationStyle) fades a SkAnimator in or out. This is the standard way to transition between two animations, like idle and walk.

Clocks can be changed by server or client scripts. Client-side changes stay local to that client, which is useful for animations that should not wait on the network.

Blending and transitions

There is no special "transition" object. Transitions fall out of how animator weights blend, so it's worth understanding that first.

Every SkAnimator has two numbers:

  • weight (0-100) — how much influence it wants,
  • priority (0-10) — who gets served first.

Each frame, the engine looks at every animator targeting a Skeleton, in order of decreasing priority, and hands out influence until the total reaches 100. A lower-priority animator only gets whatever the higher-priority animators left behind. And if the animators together use less than 100, the leftover blends the SkJoints back toward their bindPose — the skeleton's resting position.

This gives you cross-fades with a single number. Set it up like this:

  • walk animator — priority 1, weight 0
  • idle animator — priority 0, weight 100

The walk animator has higher priority, so it is served first. While its weight is 0, idle gets all 100 and the character idles. Fade walk's weight up to 100 and it consumes everything, squeezing idle down to nothing. Fade it back to 0 and idle returns. At walk weight 30, you get exactly 30% walk and 70% idle. One number drives the whole transition.

(No idle animation? Skip the idle animator entirely — the leftover weight blends toward the bind pose instead, so fading the walk animator still transitions smoothly between standing still and walking.)

The fading itself is built in. Every animator has a weightTransition field, and scripts set it with one call:

ts
game.animationService.smoothlyChangeWeight(animatorId, targetWeight, durationMs, interpolationStyle)

The engine interpolates the weight from its current value to the target over the duration, with an easing style like EaseInOut. That interpolation is the transition.

One more thing: when cross-fading two animations, both clocks should be playing and looping the whole time. You don't pause the idle clip while walking — you just take its weight away.

Every field on these objects is documented in the Object Types [blocked] API reference.