All SDK docs

Entities, AI & replays

vpyent ties meshes, bodies, occluders, marks and sounds into one table; vpyai steers and finds paths; vpyreplay records input and plays it back exactly.

vpyent — entities

vpyent does, once, the bookkeeping that a 3D game with physics otherwise writes by hand. An entity groups:

  • a transform: a vpyphys body's, or one the game sets;
  • a mesh: shared, or its own copy once dented;
  • an occluder shape;
  • its shot marks;
  • a vpyimpact material;
  • a kind and a user pointer.

The table is fixed at VPYENT_MAX (64, the same as vpyphys's body table: one entity per body), and every refusal is counted.

To use it, add $(VPY_C_SDK)/vpyent.c together with vpy3d, vpyphys, vpyimpact, vpyfx and vpycam.

int  vpyent_create(const vpy_mesh *mesh,int body);       /* body VPYP_NONE: use set_place */
void vpyent_destroy(int e);                               /* and its body */
void vpyent_reset(void);
int  vpyent_alive(int e);   int vpyent_next(int e);       /* iterate */
int  vpyent_of_body(int body);
void vpyent_set_place(int e,const vpy_xf *place);         /* for an entity with no body */
void vpyent_place(int e,vpy_xf *out);                     /* where it is now, either way */
 
void vpyent_set_mesh(int e,const vpy_mesh *mesh);
void vpyent_set_brightness(int e,int br);                 /* default 100 */
void vpyent_set_occluder(int e,int kind,int32_t hx,int32_t hy,int32_t hz);
                                          /* VPYENT_OCC_MESH / _BOX / _SPHERE / _NONE */
void vpyent_set_material(int e,int material);             /* VPYI_* */
void vpyent_set_kind(int e,int kind);     void vpyent_set_user(int e,void *user);
 
int  vpyent_dent(int e,int32_t px,int32_t py,int32_t pz,int32_t dx,int32_t dy,int32_t dz,
                 int32_t depth,int32_t r);
void vpyent_mark(int e,int32_t x,int32_t y,int32_t z,int32_t nx,int32_t ny,int32_t nz);
void vpyent_set_marks(int e,int32_t radius,int br,int style);
 
int  vpyent_draw(void);                                   /* near to far, through the occluder */
int  vpyent_draw_shadows(int32_t lx,int32_t ly,int32_t lz,int32_t floor_y,int32_t lift,int br);
int  vpyent_step(int floor_material);                     /* camera, impacts, physics, fx */
const vpyent_stats_t *vpyent_stats(void);

It removes the ordering trap

The occluder only works if you get the order right: draw near to far, draw each solid and then add it, and draw marks before the occluder that would cut them. vpyent_draw does all of that every frame, whatever order the entities were created in.

vpyent_step also runs the subsystems in the right order. For example, it reads the hit-stop before vpycam_step counts it down, so vpycam_hitstop(n) holds for exactly n frames.

Two rules still apply:

  • A mesh with more than 8 vertices and no occluder shape hides nothing, and occl_missing reports it. Give such entities a box or sphere occluder.
  • A dent makes the entity take its own copy of the mesh automatically. vpyent_set_mesh drops that copy again.

scene_demo is a whole scene built this way. sdk/vpy-c/tools/ent_check.c checks it.


vpyai — steering and paths

void vpyai_seek(const int32_t pos[3],const int32_t target[3],int32_t speed,int32_t out[3]);
void vpyai_flee(const int32_t pos[3],const int32_t threat[3],int32_t speed,int32_t out[3]);
void vpyai_arrive(const int32_t pos[3],const int32_t target[3],int32_t speed,
                  int32_t slow_radius,int32_t out[3]);
void vpyai_separate(const int32_t pos[3],const int32_t (*others)[3],int n,
                    int32_t radius,int32_t strength,int32_t out[3]);
void vpyai_steer(int32_t v[3],const int32_t desired[3],int32_t max_change);   /* a turn rate */
 
int  vpyai_path(const uint8_t *grid,int w,int h,int sx,int sy,int gx,int gy,
                int diagonal,int16_t *out_xy,int max_cells);
int32_t vpyai_path_cost(void);

Steering

The steering calls each return a desired velocity:

  • seek, flee and arrive move towards or away from a point; arrive slows down inside slow_radius.
  • separate keeps a group apart.

Add the results together, then use vpyai_steer to turn the current velocity towards the sum at a limited rate.

Paths

vpyai_path is A* on a grid:

  • A cell of 0 is free, 255 is a wall, and anything in between costs more to cross.
  • It returns the cheapest path, the same one every time, using fixed memory.

sdk/vpy-c/tools/ai_check.c compares it with Dijkstra on 196 random grids.


vpyreplay — record and play back

typedef struct { uint8_t buttons; int8_t jx, jy; } vpyreplay_frame;
 
void vpyreplay_record(vpyreplay_frame *buf,int capacity,uint32_t seed);
void vpyreplay_play(const vpyreplay_frame *buf,int frames,uint32_t seed);
void vpyreplay_stop(void);
int  vpyreplay_mode(void);          /* VPYREPLAY_OFF / _RECORDING / _PLAYING */
void vpyreplay_input(uint8_t *buttons,int8_t *jx,int8_t *jy);   /* once a frame, BEFORE reading input */
int  vpyreplay_frames(void);        /* recorded, or played, so far */
int  vpyreplay_done(void);          /* a playback that has run out */
uint32_t vpyreplay_seed(void);      /* seed every generator from this */

Everything that moves in libvpy is deterministic: physics, effects, ropes, camera. The same input and the same seed give the same frame, bit for bit. So a replay only needs the input (three bytes a frame) and a seed. That's enough for:

  • an attract mode;
  • a ghost to race against;
  • a bug report that can be watched again.

Two rules:

  • Seed every random generator from vpyreplay_seed() when a recording or playback starts.
  • A recording that outgrows its buffer stops recording and counts what it lost. A partial recording would not reproduce the game.

sdk/vpy-c/tools/replay_check.c plays 500 frames of physics back exactly.