Skip to content

en Basic PRT System

HoCha113 edited this page May 19, 2026 · 9 revisions

Chinese version

What is PRT?

PRT is short for 'particle', used to distinguish it from other particle systems.

Compared to the vanilla Dust system, the PRT system provided by InnoVault offers far greater customization, letting developers implement more complex particle effects with less boilerplate.

Features of the PRT system:

  • 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 CanPool and Reset())

BasePRT

BasePRT is the base class for PRT entities. Inherit from it to create custom particle effects.

The development workflow for PRT entities mirrors that of projectiles. Each instance has its own independent field space and does not interfere with other particles.

Key Properties

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)

Quick Example

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;
}

Lifecycle Callbacks

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

Draw Blend Modes (PRTDrawModeEnum)

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.

Update and Draw Layers (PRTLayersModeEnum)

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

Render Timing Layer (PRTRenderLayer)

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 RenderLayer made inside AI() 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 in SetProperty() to avoid visual glitches.

History Caches (Trail Effects)

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

Object Pool

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.

Enabling Pooling

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
    }
}

Pool Constraints

Two rules must be followed when CanPool is true:

  1. Cache arrays must be initialized in SetProperty(), not the constructor. Reset() sets oldPositions/oldRotations to null, and pooled instances skip the constructor entirely. Any trail logic that depends on these arrays must call InitializeCaches again in SetProperty().

  2. All custom fields must be reset in an overridden Reset(). The base Reset() only covers BasePRT's own fields.

Pool Memory Ceiling

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

PRTLoader is the core of the PRT system, managing registration, loading, updates, and lifecycle.

Global Limits

  • Global particle cap: InGame_World_MaxPRTCount = 32767 (short.MaxValue)
  • Per-type cap: override InGame_World_MaxCount (default 4000)
  • Per-type pool capacity: MaxPoolPerType = 4096

Spawning Particles

// 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 new and submitted via AddParticle are never returned to the pool, even if the type has CanPool = true. Only instances created through the internal Spawn path (used by NewParticle) participate in pooling.

Common Static Methods

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), NewParticle returns null immediately.


PRTRender

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.

Most developers will not need to call PRTRender directly, but understanding how it works helps you use RenderLayer correctly.

Bucket Mechanism

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 RenderLayer take effect on the next frame
  • Empty layers are detected upfront and skipped, avoiding unnecessary SpriteBatch state switches

Drawing All Layers at Once

// SpriteBatch must be active on entry; it remains active on exit
PRTRender.DrawAll(spriteBatch);

Drawing a Specific Layer

// SpriteBatch must be inactive on entry; it remains inactive on exit
PRTRender.DrawLayer(spriteBatch, PRTRenderLayer.AfterTiles);

PRTGroup (Local Particle Collection)

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;
    }
}

PRTGroup API

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.


GlobalPRT (Global Particle Hooks)

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) { }
}

Finding Particle Instances via VaultContent

BasePRT prt = Mod.FindPRT<ExamplePRT>();
BasePRT prt2 = Mod.FindPRT("ExamplePRT");

Navigation

Previous Next
Basic VaultLoaden Basic TP Entity

Clone this wiki locally