Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/runner.c
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ static DrawKey drawableKey(const Drawable* d) {
case DRAWABLE_TILE: k.order = d->tileIndex; break;
case DRAWABLE_INSTANCE: k.order = (int32_t) d->instance->instanceId; break;
case DRAWABLE_LAYER: k.order = d->runtimeLayerId; break;
case DRAWABLE_PARTICLE_SYSTEM: k.order = d->particleSystemId; break;
}
return k;
}
Expand Down Expand Up @@ -726,6 +727,9 @@ static void refreshDrawableDepths(Runner* runner, Drawable* drawables, int32_t c
} else if (d->type == DRAWABLE_LAYER) {
RuntimeLayer* rl = Runner_findRuntimeLayerById(runner, d->runtimeLayerId);
if (rl != nullptr) d->depth = rl->depth;
} else if (d->type == DRAWABLE_PARTICLE_SYSTEM) {
ParticleSystem* ps = Particles_systemGet(runner, d->particleSystemId);
if (ps != nullptr) d->depth = ps->depth;
}
}
}
Expand Down Expand Up @@ -777,6 +781,19 @@ static void rebuildDrawableCacheIfDirty(Runner* runner) {
}
}

// Particle systems are not room-scoped: a system created in one room keeps running until the
// game destroys it, so they are re-added on every rebuild rather than tracked per room.
repeat((int32_t) arrlen(runner->particleSystemPool), i) {
ParticleSystem* particleSystem = &runner->particleSystemPool[i];
if (!particleSystem->used || !particleSystem->automaticDraw) continue;
Drawable d;
ZERO_STRUCT(d);
d.type = DRAWABLE_PARTICLE_SYSTEM;
d.depth = particleSystem->depth;
d.particleSystemId = (int32_t) i;
arrput(runner->cachedDrawables, d);
}

int32_t count = (int32_t) arrlen(runner->cachedDrawables);
if (count > 1) {
qsort(runner->cachedDrawables, count, sizeof(Drawable), compareDrawables);
Expand Down Expand Up @@ -909,6 +926,12 @@ void Runner_draw(Runner* runner) {
} else if (runner->renderer != nullptr) {
Renderer_drawSelf(runner->renderer, inst);
}
} else if (d->type == DRAWABLE_PARTICLE_SYSTEM) {
// Filtered at draw time, like instance visibility: part_system_automatic_draw can be
// toggled from a Draw event that already ran this frame.
ParticleSystem* particleSystem = Particles_systemGet(runner, d->particleSystemId);
if (particleSystem == nullptr || !particleSystem->automaticDraw) continue;
Particles_drawSystem(runner, d->particleSystemId);
} else if (d->type == DRAWABLE_LAYER) {
// Re-resolve every iteration: a previous instance's Draw event may have called layer_create/layer_destroy and reallocated runner->runtimeLayers.
RuntimeLayer* runtimeLayer = Runner_findRuntimeLayerById(runner, d->runtimeLayerId);
Expand Down Expand Up @@ -1806,6 +1829,8 @@ static void cleanupState(Runner* runner) {
}
runner->savedRoomStates = nullptr;

Particles_freeAll(runner);

// Drain ds_map/ds_list pools BEFORE bulk-freeing struct instances. Their RValue entries may hold RVALUE_STRUCT refs to structs in runner->structInstances, and RValue_free would deref freed memory if the structs are gone.
{
repeat((int32_t) arrlen(runner->dsMapPool), i) {
Expand Down Expand Up @@ -3982,6 +4007,10 @@ void Runner_step(Runner* runner) {
// Execute End Step for all instances
Runner_executeEventForAll(runner, EVENT_STEP, STEP_END);

// Step particle systems left on automatic update. After End Step, so a system whose emitters were
// just reconfigured streams with this frame's settings, and before the draw pass that shows them.
Particles_updateAutomatic(runner);

// Update view following
updateViews(runner);

Expand Down
124 changes: 123 additions & 1 deletion src/runner.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ typedef struct {

// A single entry in the depth-sorted draw list. Cached on Runner and rebuilt lazily based on Runner.drawableListStructureDirty / drawableListSortDirty.
// Filtering on instance->active/visible and runtimeLayer->visible happens at draw time so toggling those does not require invalidating the cache.
typedef enum { DRAWABLE_TILE, DRAWABLE_INSTANCE, DRAWABLE_LAYER } DrawableType;
typedef enum { DRAWABLE_TILE, DRAWABLE_INSTANCE, DRAWABLE_LAYER, DRAWABLE_PARTICLE_SYSTEM } DrawableType;

typedef struct {
DrawableType type;
Expand All @@ -266,6 +266,8 @@ typedef struct {
int32_t tileIndex;
// Stored as an ID (resolved via Runner_findRuntimeLayerById) instead of a pointer, because layer_create can call arrput on runner->runtimeLayers mid-draw and realloc the array, invalidating any cached pointers.
int32_t runtimeLayerId;
// Same reasoning as runtimeLayerId: part_system_create during a draw event can realloc the pool.
int32_t particleSystemId;
};
} Drawable;

Expand Down Expand Up @@ -378,6 +380,122 @@ typedef struct {
uint8_t* cells;
} MpGrid;

// ===[ Particle System ]===
// Backs the part_* builtins, which are implemented in vm_builtins.c. GameMaker splits particles into
// three resources:
// * a SYSTEM owns the live particles and the emitters that spawn them, and decides when they are drawn
// * a TYPE describes how a particle looks and moves; types are global, so any system can stream any type
// * an EMITTER is owned by one system and spawns particles of a given type inside a region
//
// Ids are indices into the pools below, with a "used" tombstone so a destroyed id can be handed out
// again. Same convention as the ds_* pools, and games do rely on it.

// part_emitter_region() shape constants
#define PS_SHAPE_RECTANGLE 0
#define PS_SHAPE_ELLIPSE 1
#define PS_SHAPE_DIAMOND 2
#define PS_SHAPE_LINE 3

// part_emitter_region() distribution constants
#define PS_DISTR_LINEAR 0
#define PS_DISTR_GAUSSIAN 1
#define PS_DISTR_INVGAUSSIAN 2

// Upper bound on live particles per system. GameMaker itself has no such limit, but an emitter left
// streaming in a room the player never leaves will grow without bound, and the consoles this runner
// targets cannot absorb that. Spawns past the cap are dropped (warned about once per system).
#define PARTICLE_SYSTEM_MAX_PARTICLES 8192

typedef struct {
bool used;

int32_t sprite; // sprite asset index, -1 when the type has no sprite (draws nothing)
bool spriteAnimate; // advance the subimage as the particle ages
bool spriteStretch; // stretch one full animation cycle across the particle's whole life
bool spriteRandom; // start from a random subimage

// Every "min/max/incr/wiggle" quadruple works the same way: the initial value is picked uniformly
// in [min, max], "incr" is added every step, and "wiggle" oscillates the value used for motion and
// drawing without accumulating into the base.
GMLReal sizeMin, sizeMax, sizeIncr, sizeWiggle;
GMLReal scaleX, scaleY;
GMLReal speedMin, speedMax, speedIncr, speedWiggle;
GMLReal dirMin, dirMax, dirIncr, dirWiggle;

GMLReal gravityAmount;
GMLReal gravityDirection;

// Drawn orientation. Independent of the direction of travel unless angRelative is set, in which
// case the angle is measured from it.
GMLReal angMin, angMax, angIncr, angWiggle;
bool angRelative;

int32_t lifeMin, lifeMax;

// Alpha and colour both run through three stops across the particle's life: start, middle at the
// halfway mark, end. part_type_alpha1/alpha2 and part_type_colour1/colour2 collapse the stops.
GMLReal alphaStart, alphaMiddle, alphaEnd;
uint32_t colourStart, colourMiddle, colourEnd; // GML packed BGR, as the drawing functions take it
bool additive;

int32_t deathType; // type id spawned when a particle of this type dies, -1 when none
int32_t deathNumber; // how many to spawn; negative means a 1-in-|n| chance
int32_t stepType; // type id spawned every step a particle of this type lives, -1 when none
int32_t stepNumber; // same "negative means a chance" convention as deathNumber

// Particles alive that were born from this type. A destroyed type keeps its slot until the
// count reaches zero: in GameMaker a particle outlives the type it came from, and holding the
// slot is far cheaper than copying every type field into every particle.
int32_t refCount;
} ParticleType;

typedef struct {
int32_t typeId;
GMLReal x, y;
GMLReal speed; // base speed, before wiggle
GMLReal direction; // base direction in degrees, before wiggle
GMLReal size; // base size, before wiggle
GMLReal angle; // drawn orientation in degrees, before wiggle
int32_t life; // steps remaining
int32_t lifeTotal; // steps this particle started with, for the alpha/animation curves
int32_t subimgBase; // starting subimage
uint32_t colour; // set by part_particles_create_colour; overrides the type's colour curve
bool colourFixed; // true when "colour" above is in force
int32_t seed; // per-particle random; phase-shifts this particle's wiggle oscillations
} Particle;

typedef struct {
bool used;
GMLReal xmin, xmax, ymin, ymax;
int32_t shape;
int32_t distribution;
int32_t streamType; // type id streamed every step, -1 when the emitter is idle
// Particles per step. Negative means a 1-in-|n| chance; a fraction is a chance of one more, so
// an emitter streaming 0.5 spawns on roughly every other step.
GMLReal streamNumber;
} ParticleEmitter;

typedef struct {
bool used;
bool automaticUpdate; // step the system at the end of every frame (on by default, as in GML)
bool automaticDraw; // draw the system from the depth list (on by default, as in GML)
int32_t depth;
GMLReal originX, originY; // part_system_position: added to every particle when drawing
bool warnedFull; // the "hit PARTICLE_SYSTEM_MAX_PARTICLES" warning fires once per system
Particle* particles; // stb_ds array
ParticleEmitter* emitters; // stb_ds array, index = emitter id within this system
} ParticleSystem;

// Implemented in vm_builtins.c next to the part_* builtins; these four are the only entry points the
// runner itself needs (depth list, frame step, teardown).
ParticleSystem* Particles_systemGet(Runner* runner, int32_t systemId);
// Steps every system with automaticUpdate set. Called once at the end of Runner_step.
void Particles_updateAutomatic(Runner* runner);
// Draws one system at the current draw state. Backs part_system_drawit and the depth-list entry.
void Particles_drawSystem(Runner* runner, int32_t systemId);
// Frees both pools. Called from the Runner's cleanup path.
void Particles_freeAll(Runner* runner);

// Open text file handle for GML file_text_* functions
#define MAX_OPEN_TEXT_FILES 32
typedef struct {
Expand Down Expand Up @@ -550,6 +668,10 @@ struct Runner {
DsGrid* dsGridPool; // stb_ds array of DsGrid
GmlBuffer* gmlBufferPool; // stb_ds array of GmlBuffer
MpGrid* mpGridPool; // stb_ds array of motion-planning grids
// Particle systems own their emitters and live particles; types are global and can be streamed by
// any system's emitters. Both pools reuse destroyed slots, matching the ds_* id behaviour.
ParticleSystem* particleSystemPool; // stb_ds array of ParticleSystem
ParticleType* particleTypePool; // stb_ds array of ParticleType

// Motion planning potential field settings
GMLReal mpPotMaxrot;
Expand Down
Loading
Loading