New

dot-player-char

What a player looks like and how it animates — body metrics five systems share, a customisation document validated from ids alone, a 3D rig with named mounts, a 2D sheet, a locomotion state machine, animated people built from a look, and ragdolls for the dead.

dot-player-char covers the character definition (body metrics and the customisation document), a 3D rig with named mounts, a 2D sprite sheet with layered overlays, a locomotion state machine that drives either, and two things built on top: ready-made animated people and ragdolls for when they die.

Requires dot-core and dot-player. Both drawing halves ship in one addon and neither loads anything until a catalogue points it at content, so a 3D game pays a handful of unused scripts for the 2D half and no assets.

Usage

One definition carries every body measurement, and the node applies it:

var ch := DotPlayerChar.new()
ch.catalogue = DotPlayerCharCatalogue.three_builds()
player.add_child(ch)

ch.set_char(&"heavy")     # the capsule resizes with the model
ch.set_crouched(true)     # so does the crouched one
ch.eye_height()           # asked by the camera, the muzzle and the trace

Height decides the collision capsule, eye height decides where the camera and the muzzle are, crouch height decides what it fits under, radius decides what it fits through, and hitbox scale decides how hard it is to shoot. Those are five numbers in five files in most projects, and one DotPlayerCharDef here.

What is in it

DotPlayerCharDef / DotPlayerCharCatalogue Body metrics, slots and options. Three preset builds ship.
DotPlayerCharLook The customisation document: slot choices and tint colours.
DotPlayerChar The node. Applies a definition to the body and holds the look.
DotPlayerModelBuilder / DotPlayerModelVisual / DotPlayerModelCatalogue The 3D half: a plan built from ids, applied to a rig.
DotPlayerSpriteVisual / DotPlayerSpriteFacing The 2D half: layered overlays on one grid, and the facing lookup.
DotPlayerAnimDriver / DotPlayerAnimState / DotPlayerAnimSet Locomotion, and the clip table both halves share.
DotPlayerAnimPlayerSink / DotPlayerAnimSpriteSink Where the animation decision is applied: an AnimationPlayer, or a sheet.
DotHumanoidLook / DotHumanoidFigure / DotHumanoidClips Animated people: four skinned characters on one rig, coloured by a look.
DotPlayerRagdolls / DotPlayerDeath Ragdolls, and the death reaction the server picks.

Validating a look

var res := look.validate(def)   # does the slot exist, is the option real,
                                # are there no more colours than channels

Nothing in validating a look loads art, so a server can check one without holding the cosmetics — the same rule as dot-user-avatar.

conform() is the other half, and the split is deliberate:

  • validate is for the trust boundary. A bad document is refused.
  • conform is for loading a saved character after an option was retired. Refusing there would lock a player out of their own character, so it drops what is illegal and keeps the rest.

fill_defaults() completes a partial look, so a player who never opened the customisation screen still has a valid one.

Drawing it in 3D

var plan := DotPlayerModelBuilder.plan(model_def, look)
DotPlayerModelBuilder.apply(plan, rig, placeholder)

Planning never touches the filesystem. missing means the definition names no path for this part, not the file is absent — a dedicated server plans from ids and holds none of the content, so a plan that consulted ResourceLoader.exists would answer differently on each machine.

DotPlayerModelVisual keeps a signature of slot→scene, so a colour or stance change writes to the instances that already exist instead of rebuilding the figure. Tints are per-instance shader parameters, never a material swap: two players wearing the same part must not recolour each other.

DotPlayerModelCatalogue.humanoid() ships nine mount names — head, face, chest, back, waist, left_hand, right_hand, left_foot, right_foot — and no scene paths. Agreeing on the spelling is what lets a weapon written for one game attach in another.

Drawing it in 2D

A sheet is a grid: a row is a state, a direction offsets that row, and a column is a frame. def.frame_index(row, direction, frame) resolves one, and overlays are cut on the base’s grid so a single index serves every layer.

DotPlayerSpriteFacing.resolve(angle, count, mirror) returns the row offset and the flip, because a mirrored eight-way sheet is five drawn rows and three derived.

Animating it

DotPlayerAnimDriver           anim_set = DotPlayerAnimSet.locomotion()
  DotPlayerAnimPlayerSink     3D: drives an AnimationPlayer
  DotPlayerAnimSpriteSink     2D: calls set_row / set_frame

DotPlayerAnimState.advance(motion, delta) is pure — no node, no clock, no input device — so a server can run the same state machine as a client and check what the client claims to be doing.

Animation events come from the clip’s own timeline rather than a Timer, as fractions, so 0.25 is the same place in a rig’s clip and a sheet’s.

People

var look := DotHumanoidLook.from_site(site_avatar)   # or DotHumanoidLook.stock(hash(player_id))
var person := DotHumanoidFigure.new()
world.add_child(person)
person.build(look.with_side(team_colour, 0.6), 1.8)

# every frame:
person.set_motion({"on_floor": on_floor, "vertical": velocity.y, "crouched": crouched})
person.set_hold(DotHumanoidFigure.Hold.GUN)
person.pose(drawn_position, yaw, ground_speed)

Players, bots and NPCs can be four animated people on one rig, coloured by a look: a body and its skin, hair, top and bottom. A member’s site avatar becomes a look, a team colour goes on the top, and everybody dressed alike shares one material. The figure chooses the clip from how the player moves (idle, walk, run, crouch, jump, fall, land, crawl when down, swim, climb, sit, drive, death), holds a gun with the gun clips, bends the upper body to aim up and down (aim), and plays swings, flinches and gestures over the legs. The sit and driving motion keys seat it, with driving falling back to the sit clip if the driving one is missing, and a hat, crest or similar attaches at the head_top mount.

Two settings matter for a body that moves in steps, as one carried on physics ticks or network snapshots does. Its per-frame speed alternates between zero and double and the clip flips between idle, walk and run, so set auto_smooth_time to average the measured speed over a short time. A game whose walking pace is near the default raises set_run_speeds(run_speed, run_exit_speed) so a walk does not play the hunched run clip.

When somebody dies

var yard := DotPlayerRagdolls.new()     # one per world, on a client
yard.rules.follow_cvars(link)           # sv_ragdoll and friends
world.add_child(yard)

# the server, on a kill:
var death := DotPlayerDeath.decide(reactions, headshot, damage, direction, seed, weapon_def)

# every client, on that message:
yard.spawn(victim_figure, DotPlayerDeath.from_dict(kill_message), victim_velocity)

The dead figure becomes a ragdoll: its parts on rigid bodies joined at the hips, shoulders and neck, pushed the way the shot went, then settling, freezing and fading, with the oldest making room when there are too many. A skinned figure falls by its bones. Death reactions are data (a plain fall, a headshot that throws the body, a powerful weapon that carries it along the shot, a face plant, a crumple), and the server picks and sends the pick with the kill, so every screen shows the same fall. A server can also make ragdolls one stiff piece, a scripted fall with no physics, or turn them off.

A pack that draws a ragdoll or a person needs a shell with the addon API level that has it (level 2 for ragdolls, 3 for people, 4 for driving), and says so in its requires.json.