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
Skeletonfull ofSkJointobjects describes what can move. - A
SkAnimationdescribes how the SkJoints move over time. - A
SkAnimatorplays aSkAnimationon aSkeleton.
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 forSkJointobjects. It renders nothing itself; it only defines what can move.SkJoint— one movable point. A SkJoint must live inside aSkeletonor inside anotherSkJoint, so you can build hierarchies like shoulder, elbow, wrist. ItsbindPoseis 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 adurationand aninterpolationStyle(Linear,EaseInOut,EaseIn,EaseOut, orStepfor a stop-motion look).SkKeyframe— a child of aSkAnimationthat marks one moment in the clip with itstimefield.SkKeyframePose— a child of aSkKeyframethat says where one SkJoint should be at that moment:jointNamepicks the SkJoint, andrelativePoseis the offset from that SkJoint's bindPose.SkAnimator— the player. Point itsskeletonIdat the Skeleton and itsskAnimationIdat the animation. When several animators target the same Skeleton, they are blended in order of decreasingpriority(0-10) until theirweightvalues (0-100) reach a total of 100.Clock— a small helper that tracks playback: paused or playing,shouldLoop, andspeed. EachSkAnimatorpoints at a clock through itsclockId.
Create your first animation
Here is the full recipe, using a simple bobbing motion as an example:
- Create a
Skeleton, and add aSkJointinside it namedroot. Set the SkJoint'sbindPoseto where the animated thing should rest. - Create a
SkAnimation. Give it adurationof 2 and aninterpolationStylelikeEaseInOut. - Add
SkKeyframechildren to the animation withtimeset to 0, 1, and 2. - Inside each keyframe, add a
SkKeyframePosewithjointNameset toroot. 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
Clockand setshouldLoopto true. - Create a
SkAnimator. SetskeletonIdto your Skeleton,skAnimationIdto your animation,clockIdto your clock, andweightto 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
followSkeletonIdto yourSkeleton. This tells the rigid group which Skeleton drives it. - For each part that should move, add a
SkAttachment. SetobjectIdto the part (a ball, brick, or cylinder in the rigid group),jointNameto the name of the SkJoint it should follow, andoffsetFromJointto 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 aSkAnimatorin 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 —
priority1,weight0 - idle animator —
priority0,weight100
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:
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.