-
Notifications
You must be signed in to change notification settings - Fork 6
en Basic PRT System
Documentation moved: This page is archived. See the up-to-date guide at innovault.wiki.
PRT (Particle Render Type) is InnoVault's particle system, named to distinguish it from vanilla Dust.
Compared to Dust, PRT supports custom blend modes, shaders, layered draw, and object pooling. The workflow mirrors projectile classes.
Features:
- Multiple draw blend modes (AlphaBlend, AdditiveBlend, NonPremultiplied)
-
Shader support (attachable
ArmorShaderData) - Historical position/rotation caches (for trail effects)
- Lifetime management (automatic lifecycle tracking)
-
Global hooks (modify all particle behavior via
GlobalPRT) -
Local particle collections (manage particles in non-global contexts via
PRTGroup) -
Layered rendering (control draw order in the game world via
PRTRenderLayer) -
Object pool recycling (reduce GC pressure for high-frequency particles via
CanPoolandReset())
BasePRT is the PRT base class. Each instance has its own fields; the workflow mirrors projectile classes.
| Property/Field | Type | Default | Description |
|---|---|---|---|
Texture |
string |
"" |
Texture path; auto-loads a same-name .png from the same directory |
TexValue |
Texture2D |
— | The loaded texture (read-only) |
ID |
int |
— | Globally unique ID |
active |
bool |
false |
Whether this particle is alive; set to false to remove it next frame |
Time |
int |
0 |
Frames alive (auto-incremented) |
Lifetime |
int |
-1 |
Max lifetime in frames; -1 or any negative value disables lifetime tracking |
LifetimeCompletion |
float |
— | Lifetime ratio 0–1 (read-only) |
Position |
Vector2 |
— | World coordinate |
Velocity |
Vector2 |
— | Movement velocity |
Origin |
Vector2 |
— | Draw origin |
Color |
Color |
— | General-purpose color |
Rotation |
float |
0 |
Rotation angle (radians) |
Scale |
float |
1 |
Scale multiplier |
Opacity |
float |
0 |
Fade/interpolation value |
ai |
float[3] |
[0,0,0] |
AI scratch data for custom behavior |
Frame |
Rectangle |
default |
General-purpose frame rectangle |
PRTDrawMode |
PRTDrawModeEnum |
AlphaBlend |
Blend mode |
PRTLayersMode |
PRTLayersModeEnum |
InWorld |
Update and draw layer |
RenderLayer |
PRTRenderLayer |
BeforeInfernoRings |
Render timing layer (orthogonal to PRTDrawMode) |
shader |
ArmorShaderData |
null |
Attached shader |
ShouldKillWhenOffScreen |
bool |
true |
Auto-destroy when off-screen |
InGame_World_MaxCount |
int |
4000 |
Max simultaneous instances of this type |
oldPositions |
Vector2[] |
null |
Historical position cache (manual init required) |
oldRotations |
float[] |
null |
Historical rotation cache (manual init required) |
CanPool |
bool |
false |
Enable object pool recycling (see Object Pool section) |
A simple example from InnoVaultExample:
internal class ExamplePRT : BasePRT
{
// The Texture property does not need to be overridden.
// BasePRT auto-loads a same-name .png from the same directory.
public override void SetProperty() {
PRTDrawMode = PRTDrawModeEnum.AdditiveBlend; // glow effect
Lifetime = Main.rand.Next(220, 360);
}
public override void AI() {
if (--ai[0] <= 0) {
Velocity = Velocity.RotatedByRandom(0.3f);
ai[0] = Main.rand.Next(10);
}
Rotation += Velocity.X * 0.02f;
Color.A = (byte)((1 - LifetimeCompletion) * 255f);
if (LifetimeCompletion > 0.8f) {
Color *= 0.9f;
}
}
public override bool PreDraw(SpriteBatch spriteBatch) => true;
}| Method | When Called | Description |
|---|---|---|
SetProperty() |
Once on spawn | Initialize instance data, similar to ModProjectile.SetDefaults
|
AI() |
Every frame | Logic update; position has already been moved by Velocity at this point |
ShouldUpdatePosition() |
Before AI each frame | Return false to disable automatic position updates |
PreDraw(SpriteBatch) |
Before drawing | Return true for the default draw; false to handle drawing yourself |
PostDraw(SpriteBatch) |
After default drawing | Runs regardless of what PreDraw returns |
DrawInUI(SpriteBatch) |
Manual call only | Not called automatically; use for drawing in UI contexts |
Kill() |
Manual call | Sets active to false; particle is removed on the next update |
Reset() |
Before pool recycle | Clears all fields; subclasses with CanPool must override to reset custom fields |
| Mode | Description |
|---|---|
AlphaBlend |
Alpha blending (default), for standard particles |
NonPremultiplied |
Non-premultiplied blending |
AdditiveBlend |
Additive blending, for glow effects |
Particles are dynamically assigned to the appropriate render batch each frame based on PRTDrawMode, so it can be changed at runtime.
Particles with a shader assigned are placed into a separate shader batch, which is internally grouped by (shader, drawMode) to minimize GPU state switches.
| Mode | Description |
|---|---|
InWorld |
Update and draw in the game world (default) |
NoDraw |
Update logic only, no drawing |
None |
No automatic update or draw; only loaded into the list |
RenderLayer controls when the particle is drawn relative to other world elements. It is orthogonal to PRTDrawMode and can be set independently.
| Layer | Description | Typical Use |
|---|---|---|
BeforeTiles |
Before tiles are drawn (after walls and black background) | Background particles such as distant dust or underground fog |
AfterTiles |
After tiles finish drawing | Particles floating above tiles that should be occluded by entities |
BeforePlayers |
Before players are drawn | Particles that should be occluded by the player |
AfterPlayers |
After players are drawn | Flames, trails, and effects overlaid on the player |
BeforeInfernoRings |
Before DrawInfernoRings (default) |
Default render timing; matches legacy PRT behavior |
Note: Changes to
RenderLayermade insideAI()or later hooks take effect on the next frame, because render buckets are dirtied at the end of the update and rebuilt on the next render hook. Configure it once inSetProperty()to avoid visual glitches.
Initialize in SetProperty():
public override void SetProperty() {
InitializeCaches(15); // 15 position and rotation cache points
}
public override void AI() {
UpdatePositionCache(oldPositions.Length);
UpdateRotationCache(oldRotations.Length);
}Available methods:
-
InitializePositionCache(int length)— Initializes the position cache, filling all slots with the current position -
InitializeRotationCache(int length)— Initializes the rotation cache, filling all slots with the current rotation -
InitializeCaches(int length)— Initializes both caches at once -
UpdatePositionCache(int length)— Shifts entries forward, recording the current position at the end -
UpdateRotationCache(int length)— Same as above, for rotation
The PRT system includes a built-in object pool to reduce GC pressure from high-frequency particles. Pooling is disabled by default and must be explicitly opted into.
Override CanPool to return true, and override Reset() to clear all custom fields:
internal class BulletSpark : BasePRT
{
private float _customValue;
public override bool CanPool => true;
public override void SetProperty() {
// Cache arrays must be initialized here, not in the constructor.
// Pooled instances never run through the constructor again.
InitializeCaches(8);
PRTDrawMode = PRTDrawModeEnum.AdditiveBlend;
Lifetime = Main.rand.Next(15, 30);
_customValue = Main.rand.NextFloat();
}
public override void Reset() {
base.Reset(); // clear base class fields first
_customValue = 0f; // then clear subclass fields
}
}Two rules must be followed when CanPool is true:
-
Cache arrays must be initialized in
SetProperty(), not the constructor.Reset()setsoldPositions/oldRotationstonull, and pooled instances skip the constructor entirely. Any trail logic that depends on these arrays must callInitializeCachesagain inSetProperty(). -
All custom fields must be reset in an overridden
Reset(). The baseReset()only coversBasePRT's own fields.
The per-type pool capacity is PRTLoader.MaxPoolPerType (default 4096). Instances beyond that cap are discarded to the GC.
Rough memory ceiling: number of pooled types × 4096 × bytes per instance.
PRTLoader is the core of the PRT system, managing registration, loading, updates, and lifecycle.
- Global particle cap:
InGame_World_MaxPRTCount = 32767(short.MaxValue) - Per-type cap: override
InGame_World_MaxCount(default 4000) - Per-type pool capacity:
MaxPoolPerType = 4096
// Generic (recommended)
PRTLoader.NewParticle<ExamplePRT>(
player.Center,
Main.rand.NextFloat(MathHelper.TwoPi).ToRotationVector2() * 6,
Color.White,
Main.rand.NextFloat(1, 2)
);
// By ID
PRTLoader.NewParticle(
PRTLoader.GetParticleID<ExamplePRT>(),
player.Center,
velocity,
Color.White,
1f
);
// With initial AI values (saves manual assignment in SetProperty)
PRTLoader.NewParticle(
PRTLoader.GetParticleID<ExamplePRT>(),
player.Center,
velocity,
Color.White,
1f,
ai0: 10, ai1: 5, ai2: 0
);To configure an instance manually before submitting it:
ExamplePRT prt = PRTLoader.GetPRTInstance<ExamplePRT>();
prt.Position = player.Center;
prt.Velocity = velocity;
prt.Color = Color.Red;
PRTLoader.AddParticle(prt);Note: Instances created with
newand submitted viaAddParticleare never returned to the pool, even if the type hasCanPool = true. Only instances created through the internalSpawnpath (used byNewParticle) participate in pooling.
| Method | Description |
|---|---|
NewParticle<T>(pos, vel, color, scale) |
Spawn a particle via generic type |
NewParticle(id, pos, vel, color, scale, ai0, ai1, ai2) |
Spawn via ID, with optional initial AI values |
NewParticle(entity, pos, vel, color, scale, ai0, ai1, ai2) |
Initialize an existing instance and submit it |
AddParticle(BasePRT) |
Submit a particle instance directly to the global system |
AddParticle(BasePRT, bool setProperty) |
Submit a particle; optionally skip SetProperty
|
GetParticleID<T>() |
Get the internal ID of a particle type |
GetPRTInstance<T>() |
Get a fresh clone of the specified particle type |
GetPRTInstance(int id) |
Get a fresh clone by ID |
CreateAndInitializePRT<T>(pos, vel, color, scale) |
Clone and fully initialize a particle without submitting it |
NumberUsablePRT() |
Returns the number of available particle slots |
The PRT system is client-side only. On a dedicated server (
Main.dedServ),NewParticlereturnsnullimmediately.
PRTRender is the PRT system's renderer. It inherits from RenderHandle and is triggered automatically at multiple rendering stages by RenderHandleLoader — no manual registration required. For pipeline details see RenderHandle Pipeline.
Most developers will not need to call PRTRender directly, but understanding how it works helps you use RenderLayer correctly.
PRTRender maintains a flat 2D bucket array indexed by (layer, blendMode). At the end of each frame, PRTLoader.PostUpdateEverything marks the buckets as dirty, and they are rebuilt on the next render hook call. This means:
- Changes to
RenderLayertake effect on the next frame - Empty layers are detected upfront and skipped, avoiding unnecessary
SpriteBatchstate switches
// SpriteBatch must be active on entry; it remains active on exit
PRTRender.DrawAll(spriteBatch);// SpriteBatch must be inactive on entry; it remains inactive on exit
PRTRender.DrawLayer(spriteBatch, PRTRenderLayer.AfterTiles);PRTGroup manages a set of local particles independent of the global system. It is well-suited for particles attached to a specific entity, such as an NPC or projectile.
public class MyProjectile : ModProjectile
{
private PRTGroup _particles = new();
public override void AI() {
_particles.NewParticle<ExamplePRT>(
Projectile.Center,
Vector2.UnitY * -2f,
Color.Cyan
);
_particles.Update();
}
public override bool PreDraw(ref Color lightColor) {
_particles.Draw(Main.spriteBatch);
return true;
}
}| Method | Description |
|---|---|
NewParticle<T>(center, velocity, color, scale) |
Create and add a particle via generic type |
NewParticle(center, velocity, type, color, scale) |
Create and add a particle by ID |
Add(BasePRT particle) |
Add an existing instance directly |
Update() |
Update all particles in the collection |
Draw(SpriteBatch) |
Draw all particles in world space |
DrawInUI(SpriteBatch) |
Draw all particles in UI space |
Clear() |
Remove all particles |
Any() / Any(int id)
|
Check if any particles exist / if particles of a given ID exist |
PRTGroup implements IEnumerable<BasePRT>, so you can iterate it directly with foreach.
Inherit from GlobalPRT to modify the behavior of all particles, analogous to GlobalItem or GlobalNPC in tModLoader.
public class MyGlobalPRT : GlobalPRT
{
// Called when a particle is spawned; the particle is already in the world list
public override void OnSpawn(BasePRT prt) { }
// Called before all particles update; return false to block all updates this frame
public override bool PreUpdatePRTAll() => true;
// Called after all particles update
public override void PostUpdatePRTAll() { }
// Called before a single particle draws; return false to block its default draw
public override bool PreDrawPRT(SpriteBatch spriteBatch, BasePRT prt) => true;
// Called after a single particle draws
public override void PostDrawPRT(SpriteBatch spriteBatch, BasePRT prt) { }
}BasePRT prt = Mod.FindPRT<ExamplePRT>();
BasePRT prt2 = Mod.FindPRT("ExamplePRT");| Previous | Home | Next |
|---|---|---|
| Basic VaultLoaden | Home | RenderHandle Pipeline |