Players with a Strut body
Draw a player as a Strut model, with no second entity beside them.
A body is a Strut model, and the controllers that animate it, that a player can be given in place of the player model. The player stays the player: they move, collide, take damage and are tracked as before. What changes is how every client draws them. Bodies are for any mod: a morph, a disguise, a race, a shapeshift.
Included: bodies, assignment, the renderer, bone overrides, server-driven clips and locomotion, first-person modes, held items, the scene anchor, menu previews and hitbox sizes. Armour is drawn by the armour system (PlayerBody.Builder#armour, see armour), and capes and elytras are not drawn on a body.
Register a body
Register the same body on the server and on every client, from your mod initialiser (the common entrypoint runs on both):
PlayerBodies.register(PlayerBody.builder(new Identifier("mymod", "fox"))
.controllers(registrar -> registrar.add(new AnimationController<>(PlayerBody.class, "main", 5, state -> state.setAndContinue(RawAnimation.begin().thenLoop("idle")))))
.build());
The model, texture and animations come from the same folders as an entity of that name: strut/models/entity/fox.strutmodel, textures/entity/fox.png, strut/animations/entity/fox.strutanim, in the id's namespace. To choose differently, give the body a model on the client:
StrutPlayersClient.model(new Identifier("mymod", "fox"), myModel);
A model can read StrutPlayersClient.renderedPlayer() while it is asked for its texture or animation file, to pick them by StrutPlayers.variant(player).
Walking, sneaking, swimming and the rest
Add .locomotion() to the builder and the body moves by itself, from what the player is doing, choosing among the clips its animation file has. It looks for them in this order, taking the first that exists, so a body with only idle and walk still works everywhere:
| Doing | Clips |
|---|---|
| dead | death (played once, held) |
| sleeping | sleep, idle |
| riding | ride_boat, ride_minecart or ride_mount (for that vehicle), ride, sit |
| gliding with an elytra | gliding, glide, elytra, fly, fall |
| swimming | swim when moving, else swim_idle, tread; then walk or idle |
| crawling | crawl or crawl_idle, then the sneak clips |
| climbing | climb, then walk or idle |
| flying (creative, yourself only) | fly, fall |
| falling | fall, idle |
| sneaking | sneak_walk or sneak_idle, sneak, then walk or idle |
| sprinting | run, sprint, walk |
| walking, standing | walk, idle |
A second controller, actions, plays over those: swing_main or swing_off (then swing) once for each hand swing, and use_eat, use_drink, use_block, use_bow, use_spear, use_crossbow, use_spyglass, use_horn, use_brush (then use) while an item is used. While the server is playing a clip on the body, both stand aside, so a scene is not walked over.
The same facts are on every body's animation state as PlayerTickets.MOTION (a PlayerMotion), for controllers of your own. The head bone (head by default, builder.headBone(name) to change or null for none) turns to where the player looks, in the conventional model. A file whose clips are named animation.base.walk is read with builder.clipPrefix("animation.base."), crouch is understood for sneak, and builder.clipAlias("death", "downed") adds another name for a role. builder.hideBones(...) hides bones whenever the body is drawn (the server can show one with an override).
First person
By default (FirstPersonMode.VANILLA) nothing changes in first person: the camera is at the player's eyes and the game draws the vanilla arm and held item. A body can instead make the player look out of it:
PlayerBody.builder(id).locomotion()
.firstPerson(FirstPersonMode.IMMERSIVE)
.eyeBone("eyes") // the camera sits here; a bone with no geometry, placed at the eyes
.hideInFirstPerson("head") // hidden for the player themself only; others still see it
.build();
In IMMERSIVE the whole body is drawn and the camera is put at the eye bone of the animated pose, so the player sees their own arms and chest and the camera moves with a walk or a crouch. Only the camera moves: the eye position that aiming, hits and projectiles use stays the player's own, so nothing disagrees with the server. The camera is placed from where the bone was drawn in the frame before, and is left alone (the vanilla camera) if the bone is missing or more than four blocks from the player. The vanilla arm and held item are not drawn in this mode, so give the body hand items (above) to see what is held.
Drawing the body somewhere else
StrutPlayers.setAnchor(player, bedPosition, 90f); // server: everyone sees the body there, facing 90 degrees
StrutPlayers.clearAnchor(player);
For a scene on a bed or at a mark: the body is drawn at the anchor and faces the way it says, while the player stays where they are (their position, hitbox and what hits them do not change). In first person with an immersive body the camera goes with the body. The anchor lasts until it is cleared, or the player respawns or leaves.
One body, many characters: variants
A body is registered once and a player is given a variant of it (the string given to StrutPlayers.setBody). Anything that differs for a variant is given by a function, which is asked often (a hitbox every tick), so it should look something up:
PlayerBody.builder(id).locomotion().dimensions(0.6f, 1.8f, 1.62f).armour(creatureProfile)
.variants(variant -> BodyVariant.builder().dimensions(0.5f, 1.2f, 1.05f).armour(profileOf(variant)).eyeBone("kobolCam").build())
.animationFiles(variant -> List.of(fileOf(variant), baseFile)) // what the server finds a clip's events and length by
.eventBindings(variant -> List.of(bindingsOf(variant))) // the sounds and lines of a character
.onServerEvent(context -> ...).onClientEvent(context -> ...) // as StrutEntity#onAnimationEvent
.build();
A BodyVariant can change the hitbox, the armour profile, the eye bone, the bones always hidden, the bones hidden in first person, the hand item bones and the first person mode; what it leaves out is the body's. The model, texture and animation files of a variant are chosen by the body's model (StrutPlayersClient.model), which reads the variant of StrutPlayersClient.renderedPlayer().
On the server, StrutPlayers.clipLengthTicks(player, clip) is a clip's length as the server finds it and StrutPlayers.hasEnded(player, controller) says whether what StrutPlayers.play started has run to its end (a clip held on its last frame has ended too), so a scene can step on when a clip is over.
Showing a body in a menu, turned by you
StrutPlayersClient.drawBody(context, bodyId, variant, x1, y1, x2, y2, 30, yawDegrees, pitchDegrees, player -> ClientFlags.preview(true));
With a yaw (0 faces the viewer, 180 shows the back) and a pitch in place of the mouse, a preview the player drags round. The last argument, which may be null, runs on the preview player each time it is drawn, before it is: the preview player is one for every call, so a model that reads a client-only flag from it (the stripped form) is told it there.
The camera and bones, chosen at run time
StrutPlayersClient.eyeBone(player -> inScene(player) ? "boyCam" : null); // null: the body's
StrutPlayersClient.firstPerson(player -> option ? FirstPersonMode.IMMERSIVE : FirstPersonMode.VANILLA);
Vec3d at = StrutPlayersClient.bonePosition(player, "boyCam"); // relative to the player, in world axes, as last drawn
bonePosition answers null the first time it is asked for a bone (it starts keeping track of it), then the place as of the last frame; a bone not asked for for two seconds is let go of. It is how a rider is put at a bone.
Showing a body in a menu
StrutPlayersClient.drawBody(context, bodyId, "red", x1, y1, x2, y2, 30, mouseX, mouseY);
Draws a body the way the inventory draws the player: in a box, turned toward the mouse, standing still, with the variant given. It is for a screen that lets a player see what a body would look like before choosing it. It needs a world, so nothing is drawn in the title screen (it returns false). The body is drawn on a player that is not in the world, so it has no name tag and its animation state is its own.
Hitbox and eye height
PlayerBody.builder(id).dimensions(0.5f, 1.5f, 1.35f).build(); // width, height, eye height, in blocks
A body can give the player a different size when standing: the hitbox and the height they aim from, on both the server and the client, so they agree about what fits where. The game's other poses (sneaking, swimming, sleeping) keep their shape in proportion. This changes what a player can do, so a server has to be happy with it; nothing is changed unless the body sets it.
Sleeping, swimming and gliding
PlayerBody.builder(id).sleepRotation(false).swimTilt(false).glideTilt(false)
The renderer does what the vanilla player does around the clips: lays the body down in a bed, leans it along the water while swimming or crawling, and flies it forward and banks it while gliding. Each is on by default and can be turned off for a body whose clip does that itself. With sleepRotation(false) the body is only turned to face along the bed.
Swimming plays the swim clip only in the swimming pose; wading and treading water use the walk and idle clips. While swimming or gliding the head does not turn to the camera. For a body with an immersive camera, what the crosshair aims at and what is shot start at the eye bone, where the camera is . The client tells the server where the eye bone is (a few times a second while it moves), and the server aims from there too, for a player with an immersive body; an offset of more than four blocks is ignored.
Hand swings play swing_main / swing_off or swing if the file has them, and otherwise the combo attack0, attack1, ... one for each swing in turn; a swing's clip plays out in full. Using an item plays <item>_charge once and then holds <item>_idle (bow, crossbow, shield, and so on), from the start again each time the item is used, then blends out through the null clip when the use ends. A loaded crossbow in hand holds crossbow_idle. Without them, use_<item> and then use are held. Used in the off hand, each of these is looked for with _off on the end first (shield_idle_off, bow_charge_off, use_off), as swing_off is for swing, and the main hand's clip is played if the body has no clip of its own for the off hand.
Held items
The item a player holds is drawn on the body when the body says which bone holds it. Nothing is drawn unless it does:
PlayerBody.builder(id)
.mainHandItem("hand_r") // the bone's pivot is the hand
.offHandItem(HandItem.at("hand_l").offset(0, -0.1f, 0).rotation(-90f, 180f, 0f))
.build();
The item is drawn as the game draws one held in a vanilla player's third-person hand (swords, tools, blocks, shields), starting from the bone's pivot and moving with the hand's animation. The defaults turn it as a vanilla arm holds it; offsets are in blocks and rotations in degrees, in the bone's own space, to fit a rig whose hand bone is placed or turned differently.
The item is drawn as the game draws it, so it changes with what the player does: a shield held up becomes the blocking model, a bow being drawn shows its pull. A body whose clip shows the use already can keep the model an item has at rest with .stableHeldModels("minecraft:shield") on the builder: a list of items (or tags, "#minecraft:swords"), so a shield stays a shield while a bow still shows its draw.
Give a player a body
StrutPlayers.setBody(player, new Identifier("mymod", "fox"), "red", StrutPlayers.Persistence.SAVED); // server
StrutPlayers.clearBody(player); // server
PlayerBody body = StrutPlayers.body(player); // either side: null if they have none
String variant = StrutPlayers.variant(player); // "red"
The assignment is stored on the player and synced to every client that can see them, including one that comes into range later, so there is nothing to send. SAVED (the default) is written with the player, so it is there after a relog and a respawn; SESSION lasts until the player leaves, and a death keeps it. A body id that is not registered (a mod was removed) draws the ordinary player until it is.
Overrides, attachments and clips
StrutPlayers.overrides(player).setHidden("tail", true); // server: everyone sees it; client: only you
StrutPlayers.attachments(player).setItem("hand", "hand_r", itemId);
StrutPlayers.play(player, "scene", "hug", LoopMode.LOOP); // server: everyone sees it ("scene" is a controller every body has, over the others)
StrutPlayers.stop(player, "scene");
Several players and entities animated together (a paired animation, a cutscene) are made with scenes. These are the same overrides, attachments and server-driven animations an entity has: see Bones and Playing animations. A player's own client receives the state of their own player when they join, respawn or change dimension.
What to know
- What the hands hold is not drawn while the player is in a scene (
builder.showHeldItemsInScenes()keeps it), andStrutPlayers.setHeldItemsHidden(player, true)hides it on the server for everyone, until it is shown again. - While one hand uses an item (a bow drawn, a shield raised, food eaten), or while a loaded crossbow is held, the item in the other hand is not drawn until the use is over, so it is not in the way of the aim;
builder.showOtherHandWhileUsing()keeps it drawn. Items a character holds throughattachments().setHeldItemfollow the same rule. - A player with a body is drawn by Strut instead of the vanilla player renderer, so another mod that adds layers to the vanilla player model (capes, cosmetics) does not draw on a body.
- Armour is drawn on a body when it names an armour profile (see armour); capes and elytras are not drawn on a body. In first person the vanilla arm and held item are drawn as before, except in the immersive mode, where neither is.
- The hitbox, camera and eye position are the player's own.