2
\$\begingroup\$

I'm trying to create some interesting variations on timescales in my game. In essence, I'd like at least two, probably three or four separate timescales.

  • The player - the player timescale is likely to be the game/scene timescale always.

  • Mobs - mob timescales can be altered (slower primarily, but also faster possibly) depending on cast spells or other effects in the game.

  • Environment - like mobs, the environment may be slowed or sped up depending on some game effects that occur. This would control things like traps, torches, etc.

I'm thinking about things such as a Slow Time spell that might slow most mobs, but maybe not others (bool flag), something like a time elemental or something of that nature. I'd also like to possibly slow environmental time so the player may have a single-use item that would allow them to move easily navigate traps (but not have guaranteed success if they still don't see the pattern or such).

Having controller scripts that are added to all objects that listen for calls to change a multiplier on Time.deltaTime could be an option, but it's clunky, would have to be in all of the scripts, would have to be accounted for in many areas (movement, combat, possibly related effects), and is just not an ideal option.

Inversely, the same effect (a lich for example casting slow on a player) could require opposing things. If I had global settings that contained a multiplier for player/mob/object that were impacted by an effect, I could still always use a multiplier with Time.deltaTime, but I still think it would be clunky.

Has anyone done this? Are there any good ideas? I've searched a bit, but didn't find anything terribly useful.

I've considered custom Update() classes, though I'm not entirely sure how to implement them. Maybe extend the FixedUpdate() class that's built in to take other things into account? I know that Time.timeScale is inherently for the current scene, but could I make instances of Time that are used by different scripts FixedUpdate()? Am I overthinking this? I feel like this shouldn't be quite so difficult.

\$\endgroup\$
1
\$\begingroup\$

I'd solve this by introducing a global TimeManager (probably a Singleton or a static class) whose job is to keep track of the appropriate time scale values for each subset of content, centralising your time-manipulation logic.

It would expose a method something like:

public float GetTimeScale(EntityType entityType)

…where EntityType is an enumeration containing all the different categories of stuff in your game which might need custom time scales.

Internally it can use an array or dictionary to map this type to a timescale coefficient. If you need something more nuanced than a table lookup (eg. time speed varies from place to place around the level, and characters wearing purple are immune on Tuesdays...) you could pass more information in and apply whatever game logic you need in this method to decide what time scale to use.

Each script whose behaviour can be affected will need to call this GetTimeScale() method when making any time- or physics-related updates, passing in its identifying information and using the result to scale any changes it's making.

Anything that needs to modify the time scale rules can talk directly to the TimeScaleManager, rather than broadcasting out to all the objects that might use the modified rule.

If you want to modify the behaviour of any built-in components like ParticleSystems or Rigidbodies, you'll need to write a little adapter script, like...

[RequireComponent(typeof(ParticleSystem))]
public class ParticleTimeScaler : MonoBehaviour {

    public EntityType entityType;
    ParticleSystem _system;

    void Start() {
        _system = GetComponent<ParticleSystem>();
    }

    void Update() {
        _system.playbackSpeed = TimeManager.GetTimeScale(entityType);
    }
}

...so you can adjust its behaviour to respect the appropriate speed for its category.

\$\endgroup\$
  • \$\begingroup\$ This is interesting. I think this makes sense, but I may have to play around with it a bit. I was hoping to create an extension of Update() - something like ScaledUpdate() that would take some value associated (whether it's entityType, or more likely some enum value assigned to the object) and use that value for updates rather than the system values. Is that not reasonable/feasible? \$\endgroup\$ – Jesse Williams Aug 9 '16 at 16:05
  • 1
    \$\begingroup\$ It's certainly a style you could use, but I'm not sure if it makes things any simpler. You'd still want a central manager to call all the ScaledUpdate methods, but now it also needs to handle registering lists of objects that need this method called. If you need something other than a baked Time.deltaTime value (eg. for adjusting physics velocities or particle/animation play speeds) then you still need a way to access the raw coefficient, and either a wrapper or registration of these components to make sure they get adjusted as the scale changes. So, similar work, different style. \$\endgroup\$ – DMGregory Aug 9 '16 at 16:17
1
\$\begingroup\$

I'd have every class extending MonoBehaviour instead extend TimeBehaviour, where TimeBehaviour extends MonoBehaviour. It has a layer, such as environment, enemies, player, etc, that can be set from the inspector. It also has a map of layers to timescales. In the update method, get the timescale associated with the layer, and set it.

Now in your subclasses, just remember to call base.Update(), and you can forget about everything else. Alternatively, have TimeBehaviour be abstract, and call ScaledUpdate() from the Update method. Then in your subclasses, call ScaledUpdate instead of update.

This is an interesting question to me, and I am actually going to implement this feature for fun tomorrow. I'll let you know if anything from my suggestion changes.

\$\endgroup\$
  • 1
    \$\begingroup\$ When you say "get the timescale associated with the layer, and set it," do you mean set Time.timeScale = thisLayerNetTimeScale? This may correctly scale anything that's multiplied by Time.deltaTime in this update method, but we need to remember to set it back to 1 at the end of our method to avoid messing up any non-timewarp-aware script that updates immediately after this one. We also still need special handling for slowing physics velocities, particles, or other systems that do their own time stepping outside of our scripts. \$\endgroup\$ – DMGregory Aug 9 '16 at 12:11
0
\$\begingroup\$

My approach:

  • Make a singleton class that contains 3 Dictionary<float, float>. One for player, mob, and environment. The two floats would represent a timeScale multiplier and the other a lifeTime value.

  • On update, remove Time.deltaTime from each lifeTime values in the 3 Dictionaries. If an entry has negative time remaining then remove it from the map. Store three public floats representing the total multiplied scale values from the three maps.

  • Add a method to the singleton class that can add a new value pair to one of the maps. Something like ChangeMobTimeScale(float percent, float duration)

  • Now: in all update scripts, just multiply Time.deltaTime by the relevant singleton class combined float.

This is more complicated than a single multiplier but would allow for compound effects and would "clean" itself automatically. You will need to access Time.deltaTime frequently so adding that multiplier shouldn't change any greater structure of the gameloop.

Your other option would be a class that checks all GameObject tags and handles calling a custom update function in all objects found with that tag, passing a custom scaled deltaTime value. This method would be slower because of the overhead in constantly checking every tag in the hierarchy.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.