luv

Workshop wiki

critters.lisp

luvcraft/critters.lisp

system luvcraft/core · 74 definitions · on GitHub

The animals of the block world, and the first of them: a turtle.

#KTRMAO is the design; this is what it turned into.

A critter is one of the genuinely individuated entities of #F6R9QA rather than a reading of a dense field. It owns a position, a heading, a small mind, and an independently inspectable lifetime, and there are few enough of them that identity costs nothing: each animal is an ordinary CLOS instance and each species is a class. What stays dense is the per-frame render product -- one interleaved vertex array over the ordinary block atlas and block surface pipeline, exactly like the smash fragments.

Three protocols meet in an animal:

advance-critter is its own behaviour, the only part a new species must really think about; the body protocol in SIMULATION.LISP is its contact with terrain -- the same one-axis-at-a-time AABB sweep the player controller walks on; and map-critter-boxes is its body as seen, a handful of textured boxes in a local frame which the emitter here turns by the critter's yaw.

Nothing an animal decides comes from a random state. A critter's choices are hashed from its seed, so a turtle wanders the same way in the same world on every machine and in every replay, the same discipline the terrain source and the fragment bursts already keep.

in-package#:luvcraft
luv.arithmetic:define-quantity-constant+critter-gravity+24d0:typedouble-float:quantity
:quantity:gravity-magnitude:unit
:cell1
:second-2
luv.arithmetic:define-quantity-constant+critter-terminal-fall-speed+-32d0:typedouble-float:quantity
:quantity:world-velocity:unit
:cell1
:second-1
defconstant+maximum-critters+12"How many animals may live around the player at once."
defconstant+critter-model-box-limit+12"The most boxes any critter model may emit; the vertex buffer is sized for it."
defclasscritter
position:initarg:position:initform
make-vec30d00d00d0
:typevec3:quantity
:quantity:world-position:unit:cell:tensor-order1
:accessorcritter-position
velocity:initarg:velocity:initform
make-vec30d00d00d0
:typevec3:quantity
:quantity:world-velocity:unit
:cell1
:second-1
:tensor-order1
:accessorcritter-velocity
yaw:initarg:yaw:initform0d0:typedouble-float:quantity
:quantity:critter-yaw:unit:radian
:accessorcritter-yaw
half-width:initarg:half-width:initform0.30d0:typedouble-float:quantity
:quantity:critter-half-width:unit:cell
:readercritter-half-width
height:initarg:height:initform0.60d0:typedouble-float:quantity
:quantity:critter-height:unit:cell
:readercritter-height
grounded-p:initarg:grounded-p:initformnil:accessorcritter-grounded-p

The animal's own deterministic identity: every choice it makes is a hash of this seed and how many choices it has already made.

seed:initarg:seed:initform0:type
integer0
:readercritter-seed
decision-count:initform0:type
integer0
:accessorcritter-decision-count
:documentation

One individuated animal walking the block world.

defmethodbody-position
critter-positionbody
defmethodbody-velocity
critter-velocitybody
defmethodbody-grounded-p
critter-grounded-pbody
defmethod
grounded-p
setf
critter-grounded-pbody
grounded-p
defuncritter-x
vec3-x
critter-positioncritter
defuncritter-y
vec3-y
critter-positioncritter
defuncritter-z
vec3-z
critter-positioncritter
defuncritter-velocity-x
vec3-x
critter-velocitycritter
defuncritter-velocity-y
vec3-y
critter-velocitycritter
defuncritter-velocity-z
vec3-z
critter-velocitycritter
defun
valuecritter
setf
vec3-x
critter-velocitycritter
value
defun
valuecritter
setf
vec3-y
critter-velocitycritter
value
defun
valuecritter
setf
vec3-z
critter-velocitycritter
value
defuncritter-noise
seedindexsalt

Return a stable 0..1 reading for one small animal choice.

The same three integers always answer the same way, which is what lets a wandering animal be reproducible without carrying a random state around.

let
value
logand#xffffffff
+
*seed374761393
*index668265263
*salt2246822519
setfvalue
logand#xffffffff
*
logxorvalue
ashvalue-13
1274126177
/
logand
logxorvalue
ashvalue-16
#xffffff
coerce#xffffff'double-float
defuncritter-decision

Return a stable 0..1 reading for critter's current decision and salt.

critter-noise
critter-seedcritter
critter-decision-countcritter
salt
defuncritter-decide-again

Move critter to its next decision, so its readings are all new ones.

incf
critter-decision-countcritter

The critter protocol proper.

defgenericadvance-critter
critterworldseconds
:documentation

Advance critter's behaviour and body by seconds in world.

This is the only part of an animal a new species really has to think about; terrain contact is the shared body protocol and appearance is map-critter-boxes.

defgenericmap-critter-boxes
:documentation

Call function for each textured box of critter's model.

Each call passes the box centre and half extents in the animal's own frame -- X to its right, Y up from the ground it stands on, Z ahead of it -- and the atlas tile its faces wear: (X Y Z HALF-X HALF-Y HALF-Z TILE). The emitter turns the boxes by the critter's yaw; a model never sees world coordinates.

defgenericcritter-model-box-count
:documentation

How many boxes map-critter-boxes emits for critter, at most CRITTER-MODEL-BOX-LIMIT.

defgenericcritter-species
:documentation

The keyword naming critter's species.

defgenericurge-critter
critterforwardturnseconds
:documentation

Ask critter to walk forward and turn turn over seconds, as a rider does.

forward and turn are wishes in -1..1, not velocities: what the animal makes of them is the animal's business, and one which ignores its rider simply has no method here.

defmethodurge-critter
critterforwardturnseconds
declare
ignorecritterforwardturnseconds
nil
defgenericcritter-sway
:documentation

Return the lift and yaw a rider is swayed by, as critter moves under them.

An animal walking is not a platform gliding: whoever is sitting on it rides its gait. (VALUES LIFT YAW), both small and both zero for an animal which does not say otherwise.

defmethodcritter-sway
declare
ignorecritter
values0d00d0
defgenericspawn-critter-at
speciesworldxyzseed
:documentation

Make a species animal standing on the ground at cell X,Y,Z, or NIL.

species is a keyword, so a new animal joins the world's population by adding one EQL method here rather than by editing a spawn table. A method returns NIL when the site does not suit it: this is where an animal says what ground it lives on and how much room it needs.

defmethodspawn-critter-at
speciesworldxyzseed
declare
ignoreworldxyzseed
error"No critter species is named ~S."species

The turtle: slow, deliberate, and unbothered. It walks a while, stops a while, turns away from what it bumps into, and never jumps.

luv.arithmetic:define-quantity-constant+turtle-walk-speed+0.62d0:typedouble-float:quantity
:quantity:critter-walk-speed:unit
:cell1
:second-1
luv.arithmetic:define-quantity-constant+turtle-half-width+0.34d0:typedouble-float:quantity
:quantity:critter-half-width:unit:cell
luv.arithmetic:define-quantity-constant+turtle-height+0.46d0:typedouble-float:quantity
:quantity:critter-height:unit:cell
defconstant+turtle-turn-rate+1.1d0"How fast a turtle swings toward the heading it wants, in radians a second."
defconstant+turtle-step-length+0.42d0"How far a turtle walks per full stride, which sets its leg cadence."
defparameter*turtle-leg-offsets*'
-0.20d00.23d0
0.20d00.23d0
-0.20d0-0.23d0
0.20d0-0.23d0

Where a turtle's four legs stand: right of, and ahead of, its centre.

A walking turtle moves its legs in diagonal couplets, so the two legs of each diagonal share a gait phase and the other pair is half a stride behind.

defclassturtle
heading:initarg:heading:initform0d0:typedouble-float:quantity
:quantity:critter-yaw:unit:radian
:accessorturtle-heading
resting-p:initformnil:accessorturtle-resting-p
mood-seconds:initform0d0:typedouble-float:quantity
:quantity:critter-behavior-duration:unit:second
:accessorturtle-mood-seconds

How far this turtle has walked, in strides: the phase its legs swing on, kept as distance rather than time so a stopped turtle stands still.

gait-phase:initform0d0:typedouble-float:accessorturtle-gait-phase
:default-initargs:half-width+turtle-half-width+:height+turtle-height+
:documentation

A small, slow, deliberate animal with a domed shell.

defunturtle-choose-mood

Choose turtle's next spell of walking or resting, and where it heads.

let
resting-p
not
turtle-resting-pturtle
setf
turtle-resting-pturtle
resting-p
turtle-mood-secondsturtle
ifresting-p
unlessresting-p
setf
turtle-headingturtle
turtle
defunturtle-turn-away

Send turtle off on a new heading after it has walked into something.

turtle
defmethodadvance-critter
worldseconds
let
seconds
coerceseconds'double-float
decf
turtle-mood-secondsturtle
seconds
when
minusp
turtle-mood-secondsturtle

Turning is what a turtle does slowly and visibly, so the heading is a wish and the yaw follows it at a bounded rate.

let
incf
max
-maximum-turn
minmaximum-turndifference
turtle
defmethodmap-critter-boxes

A blocky animal is a stack of slabs: a stepped carapace over a pale plastron, a head on the front, a stub of tail behind, and four legs which swing and lift on the gait phase the walk has accumulated.

let*
phase
turtle-gait-phaseturtle
walking-p
not
turtle-resting-pturtle
flet
box
xyzhalf-xhalf-yhalf-ztile
funcallfunctionxyzhalf-xhalf-yhalf-ztile

The belly plate, then a carapace of three steps: the dome is made of slabs because this is a world of blocks, but each step is deep enough to read as a shell rather than as a stack of planks.

box0.0d00.190d00.0d00.250d00.050d00.320d0plastron
box0.0d00.265d00.0d00.285d00.060d00.365d0shell
box0.0d00.350d0-0.015d00.225d00.045d00.290d0shell
box0.0d00.415d0-0.030d00.130d00.035d00.175d0shell

A resting turtle draws its head back toward the shell; a walking one cranes it a little further out on every stride.

box0.0d00.240d0
+0.425d0
ifwalking-p
*0.035d0
sin
*0.5d0phase
-0.075d0
0.095d00.080d00.130d0skin
box0.0d00.245d0-0.435d00.050d00.040d00.075d0skin
loopfor
offset-xoffset-z
in*turtle-leg-offsets*forcouplet=
if
plusp
*offset-xoffset-z
0d0pi
forleg-phase=
+phasecouplet
forswing=
ifwalking-p
*0.075d0
sinleg-phase
0d0
forlift=
ifwalking-p
*0.035d0
max0d0
sinleg-phase
0d0
do
boxoffset-x
+0.090d0lift
+offset-zswing
0.080d00.090d00.090d0skin
defconstant+turtle-rider-turn-rate+1.3d0"How fast a rider may swing a turtle's heading, in radians a second."
defconstant+turtle-rider-lead+0.7d0

How far ahead of its own yaw a rider may point a turtle, in radians.

Bounded, because a turtle turns at its own rate: without this, a held rein would run the wanted heading far past what the animal can follow and leave it turning long after the rider let go.

defmethodurge-critter
forwardturnseconds

A ridden turtle sets its own mind aside and goes where it is pointed.

Urging renews its walking spell rather than replacing its gait, so a ridden turtle walks and stops like a turtle: slowly, and in its own time.

let
seconds
coerceseconds'double-float
setf
turtle-resting-pturtle
not
pluspforward
turtle-mood-secondsturtle
max0.75d0
turtle-mood-secondsturtle
turtle
defmethodcritter-sway

A walking turtle rocks its rider once a stride, and rolls a little with it.

The roll has to be read as a small yaw, because a camera pose here carries no roll: what is left of a waddle is the shell's nose swinging across the direction of travel.

let
phase
turtle-gait-phaseturtle
if
turtle-resting-pturtle
values0d00d0
values
*0.022d0
sin
*2d0phase
*0.030d0
sinphase
defmethodspawn-critter-at
species
eql:turtle
worldxyzseed

Stand a turtle on grass or sand with clear air over it.

let
ground
world-block-atworldx
1-y
z
when
and
memberground
list*grass-block**sand-block**moss-block*
:test#'eq
make-instance'turtle:seedseed:position
make-vec3
+x0.5d0
coercey'double-float
+z0.5d0
:yaw
*2d0pi
:heading
*2d0pi

The population: the session-owned semantic object which holds the animals alive around the player, advances them, and keeps them company.

defconstant+critter-spawn-radius+22"How far from the player a new animal may appear, in cells."
defconstant+critter-spawn-clearance+7"How near the player an animal may never simply appear, in cells."
defconstant+critter-despawn-distance+46d0"How far an animal may wander from the player before it is forgotten."
defclasscritter-population
critters:initform
make-array+maximum-critters+:adjustablet:fill-pointer0
:readercritter-population-critters
species:initarg:species:initform
list:turtle
:readercritter-population-species
target-count:initarg:target-count:initform5:accessorcritter-population-target-count
seed:initarg:seed:initform8675309:readercritter-population-seed
attempt-count:initform0:accessorcritter-population-attempt-count
:documentation

The living animals near the player, and how to find more.

defuncritter-count
population

Return the number of animals alive in population.

length
critter-population-critterspopulation
defunadd-critter
populationcritter

Add critter to population unless it is already full, returning it or NIL.

check-typecrittercritter
when
vector-push-extendcritter
critter-population-critterspopulation
critter
defunadvance-critters
populationworldseconds

Advance every animal in population by seconds in world.

check-typepopulationcritter-population
when
pluspseconds
loopforcritteracross
critter-population-critterspopulation
do
population
defunforget-distant-critters
populationxz

Drop the animals of population which have left the neighbourhood of X,Z.

let
critters
critter-population-critterspopulation
write-index0
dotimes
read-index
lengthcritters
let*
critter
arefcrittersread-index
when
setf
arefcritterswrite-index
critter
incfwrite-index
setf
fill-pointercritters
write-index
population
defuncritter-spawn-site
worldxz

Return the standing height on world's resident column at X,Z, or NIL.

A column only offers a site when it is resident: an animal must not be conjured onto terrain which has not been generated yet.

let
height
chunk-shape-height
voxel-space-chunk-shape
block-world-spaceworld
loopforyfromdownto1do
multiple-value-bind
blockstatus
unless
eqstatus:resident
defunmaintain-critter-population
populationworldxz

Keep population's animals living on resident terrain near X,Z.

Called once a frame beside residency: animals which have wandered out of the neighbourhood are forgotten, and a couple of deterministic candidate sites are offered to the population's species until it is as full as it wants to be.

check-typepopulationcritter-population
let
center-x
center-z
dotimes
declare
ignorableattempt
when
>=
critter-countpopulation
critter-population-target-countpopulation
let*
index
incf
critter-population-attempt-countpopulation
seed
critter-population-seedpopulation
offset-x
offset-z
species
nth
modindex
length
critter-population-speciespopulation
critter-population-speciespopulation
when
let*
site-x
+center-xoffset-x
site-z
+center-zoffset-z
whensite-y
let
critter
spawn-critter-atspeciesworldsite-xsite-ysite-z
logand
*index2654435761
#xffffff
population

Looking at an animal. The block world answers a ray with a lattice traversal; an animal is not on the lattice, so its body answers for itself. Its body for this purpose is the same upright box its feet walk on, not its model: what the player aims at is the animal, not its left hind leg.

defuncritter-ray-distance
critterorigindirectionmax-distance

Return how far along a ray it enters critter's body, or NIL if it misses.

origin and direction are vec3 values in continuous cell coordinates, and direction is assumed to be a unit vector, as a camera basis gives it.

let
near0d0
far
coercemax-distance'double-float
flet
slab
origindirectionminimummaximum
let
origin
coerceorigin'double-float
direction
coercedirection'double-float
if
zeropdirection
<=minimumoriginmaximum
let
entering
/
-minimumorigin
direction
leaving
/
-maximumorigin
direction
when
>enteringleaving
rotatefenteringleaving
setfnear
maxnearentering
far
minfarleaving
<=nearfar
and
slab
vec3-xorigin
vec3-xdirection
slab
vec3-zorigin
vec3-zdirection
near
defuncritter-along-ray
populationorigindirectionmax-distance

Return the nearest animal of population on the ray, and its distance.

check-typepopulationcritter-population
let
nearestnil
nearest-distancenil
loopforcritteracross
critter-population-critterspopulation
fordistance=
critter-ray-distancecritterorigindirectionmax-distance
when
anddistance
or
nullnearest-distance
<distancenearest-distance
do
setfnearestcritternearest-distancedistance
valuesnearestnearest-distance

The render product: one dense vertex array over the block surface pipeline. A critter's boxes are not voxel faces, so they classify none of their edges: the fragment stage leaves their silhouette alone and shapes their surface from the material's own relief.

defuncritter-face-shade
normal-y

Return the ambient shade of a critter face with world normal lane normal-y.

An animal carries no baked neighbourhood, but its underside is genuinely more occluded than its shell, and saying so is what keeps it from reading as a paper cut-out standing on the grass.

cond
pluspnormal-y
1.0
minuspnormal-y
0.62
t0.86
defuncritter-light-levels
critterworld

Return critter's normalized sky and block light, sampled where it stands.

multiple-value-bind
skyblockstatus
if
eqstatus:resident
values
/sky15.0
/block15.0

An animal standing in terrain which is not resident is lit as if it stood under open sky rather than blacked out.

values1.00.0
defconstant+critter-texture-cell-span+0.75d0

How much of the world one whole critter atlas tile covers, in cells.

A terrain material tiles once per cell, so a block face can simply take the whole tile. A critter's faces are all different fractions of a cell, and stretching one tile across each of them would print scutes the size of the shell on its top and smear the same scutes into bands around its rim. Each face instead takes the centred fraction of its tile which keeps texels roughly the same size everywhere on the animal.

defuncritter-face-texture-scales
facehalf-xhalf-yhalf-z

Return how much of face's tile the face's two in-plane extents deserve.

The in-plane axes are the ones block-face-local-uv projects onto, read from the same face normal it reads.

let
normal
block-face-neighborface
multiple-value-bind
extent-uextent-v
cond
not
zerop
voxel-direction-dynormal
values
*2d0half-x
*2d0half-z
not
zerop
voxel-direction-dznormal
values
*2d0half-x
*2d0half-y
t
values
*2d0half-z
*2d0half-y
defunemit-critter-box
verticescritterbox-xbox-ybox-zhalf-xhalf-yhalf-ztilesky-levelblock-level

Append one of critter's model boxes, turned by its yaw, to vertices.

The box arrives in the animal's own frame; the yaw rotation here is the only place a critter model meets world coordinates.

let*
position
critter-positioncritter
cos-yaw
cosyaw
sin-yaw
sinyaw
origin-x
vec3-xposition
origin-y
vec3-yposition
origin-z
vec3-zposition
dolist
let*
normal
block-face-neighborface
nx
voxel-direction-dxnormal
ny
voxel-direction-dynormal
nz
voxel-direction-dznormal
world-nx
+
*nxcos-yaw
*nzsin-yaw
world-nz
-
*nzcos-yaw
*nxsin-yaw
multiple-value-bind
scale-uscale-v
critter-face-texture-scalesfacehalf-xhalf-yhalf-z
let
origin-u
*0.5d0
-1d0scale-u
origin-v
*0.5d0
-1d0scale-v
dolist
index'
012023
let*
corner
nthindex
block-face-cornersface
local-x
+box-x
*
-
firstcorner
0.5d0
2d0half-x
local-y
+box-y
*
-
secondcorner
0.5d0
2d0half-y
local-z
+box-z
*
-
thirdcorner
0.5d0
2d0half-z
multiple-value-bind
uv
let
tile-u
+origin-u
*uscale-u
tile-v
+origin-v
*vscale-v
push-block-vertex-componentsvertices
+origin-x
*local-xcos-yaw
*local-zsin-yaw
+origin-ylocal-y
+origin-z
-
*local-zcos-yaw
*local-xsin-yaw
tile-utile-vshadeworld-nxnyworld-nzsky-levelblock-level0.0tile+block-face-edge-flush++block-face-edge-flush++block-face-edge-flush++block-face-edge-flush+
vertices
defunemit-critter
verticescritterworld

Append critter's whole model to vertices, lit where the animal stands.

let
unless
error"~S emits ~D model boxes, more than the ~D the buffer holds."boxes+critter-model-box-limit+
multiple-value-bind
sky-levelblock-level
map-critter-boxescritter
lambda
xyzhalf-xhalf-yhalf-ztile
emit-critter-boxverticescritterxyzhalf-xhalf-yhalf-ztilesky-levelblock-level
vertices
defuncritter-vertices
populationworld

Build the block-pipeline vertex stream for population's animals.

check-typepopulationcritter-population
let*
critters
critter-population-critterspopulation
vertices
make-array:element-type'single-float:adjustablet:fill-pointer0
loopforcritteracrosscrittersdo
emit-critterverticescritterworld
vertices