Projectiles with TimeMod

The TimeMod class from Game Modules 4 enables global and local time scales to be used, meaning individual objects can have time scales unique to themselves while also being affected by a global time scale.

Projectiles can also make use of this class.

In the example below, I will demonstrate how to add a custom script to the projectiles which will track their unique TimeMod, and also set the Particle System simulation speed.

I will also demonstrate a new Movement Behavior derived from an existing one which takes into account the GlobalTimeScale on the projecitles.

In the example below, I have a "TimeScaleZone" layer, which is used for areas on the map which may change the TimeScale of any object within it.

Projectile Setup

Add a Collider

First, add a collider to your projectile, and set the "Include" and "Exclude" layers so that only the TimeScaleZone is affected.

Note: Depending on your setup, you may need to create this logic in a new child object, so that there is only one collider on each object in your Projectile. If you don't have a collder, you can set this on the parent object.

Add the Custom Script

You'll want to create your own script, but you can copy this to start. You may have to change some values to align with the way you're setting up your global TimeMod object.

public class ProjectileTimeScale : MonoBehaviour
{
    public TimeMod timeMod = new TimeMod();
    public float LocalTimeScale => timeMod.TimeScale;
    public float GlobalTimeScale => timeMod.CombinedTimeScale(GameState.Instance.timeMod.TimeScale);
    public float DeltaTime => timeMod.UnscaledDeltaTime(GameState.Instance.timeMod.TimeScale);

    private float _lastTimeScale = 1f;
    
    public virtual void SetTimeScale(float timeScale)
    {
        _lastTimeScale = LocalTimeScale;
        timeMod.SetTimeScale(timeScale);
    }
    
    public virtual void ResetTimeScale() => SetTimeScale(_lastTimeScale);
    
    private void OnEnable()
    {
        _lastTimeScale = 1f;
        timeMod.SetTimeScale(1f);
    }
}

Create a new Movement Behavior

In this example, I'm going to create a new behavior that derives from MovementBehaviorForward, which comes with Projectile Factory. The same logic will apply to any movement behavior that moves the object via a deltaTime value.

Again, you can copy this, but you may need to modify things to better fit your project.

namespace MagicPigGames.ProjectileFactory
{
    [CreateAssetMenu(fileName = "New Basic Forward Movement Behavior Battle Ages",
        menuName = "Projectile Factory/Moving Behavior/Battle Ages - Basic Forward Movement Behavior")]
    [Serializable]
    public class MovementBehaviorForwardBattleAges : MovementBehaviorForward
    {
        private ProjectileTimeScale _projectileTimeScale;
        private ProjectileTimeScale ProjectileTimeScale
        {
            get
            {
                if (_isInitialized) return _projectileTimeScale; // Only check for this value once
                
                // Set the value with GetComponent
                if (_projectileTimeScale == null)
                    _projectileTimeScale = Projectile.GetComponent<ProjectileTimeScale>();
                
                // Set isInitialized to true and return the value
                _isInitialized = true;
                return _projectileTimeScale;
            }
        }
        
        private bool _isInitialized;
        
        protected override void Move()
        {
            var deltaTime = ProjectileTimeScale == null ? Time.deltaTime : ProjectileTimeScale.DeltaTime;
            Projectile.transform.Translate(LocalDirection * (ProjectileSpeed * deltaTime), Space.Self);
        }
    }
}

We compute the value of deltaTime in the Move() method, using the ProjectileTimeScale class if it's attached to the projectile, otherwise the standard Time.deltaTime.

Optional: Set the Simulation Speed of Particle Systems

You can create a new class which derives from ProjectileTimeScale to add additional methods, or create other classes that reference ProjectileTimeScale, whichever you prefer. Here we have a new child class called ProjectileParticleSystemTimeScale.

public class ProjectileParticleSystemTimeScale : MonoBehaviour
{
    public ProjectileTimeScale projectileTimeScale;
    public ParticleSystem[] particleSystems;

    private float _timeScale;

    public float TimeScale => projectileTimeScale.GlobalTimeScale;

    public void Update()
    {
        var timeScale = TimeScale;
        if (Mathf.Approximately(timeScale, _timeScale))
            return;
        
        _timeScale = timeScale;
        foreach (var ps in particleSystems)
            SetSimulationSpeed(ps, timeScale);
    }

    private void SetSimulationSpeed(ParticleSystem ps, float timeScale)
    {
        var main = ps.main;
        main.simulationSpeed = timeScale;
    }

    // Automatically populate particleSystems with all ParticleSystems in children
    public void OnValidate()
    {
        if (particleSystems == null || particleSystems.Length == 0)
            particleSystems = GetComponentsInChildren<ParticleSystem>();
        
        if (projectileTimeScale == null)
            projectileTimeScale = GetComponent<ProjectileTimeScale>();
    }
}

The OnValidate() method will automatically populate the list of Particle Systems. Set the list count to 0 to force the script to recompute the list if you add additional systems later.

You can use these scripts (and the collider) on other particles that aren't "Projectiles" as well, to ensure that muzzle flashes and impacts also react to the Time Scale.

Last updated