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_missingreports it. Give such entities a box or sphere occluder. - A dent makes the entity take its own copy of the mesh automatically.
vpyent_set_meshdrops 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,fleeandarrivemove towards or away from a point;arriveslows down insideslow_radius.separatekeeps 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.