Search code examples
c#monogame

Setting target field values


I'm making a game, in which I have various fields that I'd like to set target values for. For example, my Camera class has:

public double zoomLevel

Currently, if the zoomLevel is (say) 1.0 and I'd like to increase it gradually to (say) 2.0, I have the following other fields to support this:

private double targetZoomLevel
private double zoomIncrement

I then have a Camera.SetZoom(double target, double increment) method that sets a desired furure zoom level, and then a Camera.Update() method that moves the current zoom level towards the target level, using the increment.

This all works well enough, but I'd really like to implement the same behaviour for other fields (e.g. camera world position, player size, player position, etc.). Using my current method, I'd need to add 2 additional 'support' fields for each field.

I'm pretty sure that my current solution is a sub-optimal, but not sure how to go about improving this. I was thinking about implementing a Property<T> class that encapulates this behaviour for a value, but not sure how to generalise an Update() method to move the current value towards its target.

Thanks!


Solution

  • What you're describing sounds very much like an animation. Animations are formal concepts in a couple of frameworks (CSS and WPF come to mind).

    The goal of an animation is to transition something from one value to the next.

    There are a variety of ways to make that transition. Sometimes you want a 2D point to follow a Bézier curve as the curve's t variable linearly changes from 0 to 1 over some period of time. Other times you want a color to transition smoothly from red to blue by going the long way around a color wheel.

    Here's an interface that can abstract over that concept:

    public interface IAnimation<T>
    {
      T Current { get; }
    
      void Update(double progress); // progress is a number between 0 and 1
    }
    

    For example, if you want a 2D point to animate over a sine wave 800 units wide and 2 units tall:

    public sealed class Point2DAnimation : IAnimation<Point2D>
    {
      public Point2D Current { get; private set; }
    
      public void Update(double progress)
      {
        Current = new Point2D(progress * 800, Math.Sin(progress * Math.PI * 2));
      }
    }
    

    There are also a variety of ways to drive the transition. A common way to drive it is to say "I want this animation to happen as smoothly as possible over X seconds". But sometimes you might want the animation to repeat from the beginning a few more times. Or perhaps you want the animation to run forward then backward then forward and so on forever.

    We could define an interface to abstract over the different ways that an IAnimation<T> can be driven. And one implementation might internally have a timer that ticks frequently enough to give the illusion of smoothness as progress goes from 0 to 1 in proportion to the amount of time that has passed at the moment of each tick.

    But I want to pause here for a moment and ask a question.

    What does the consumer of the abstraction need from it?

    If all your code needs is read access to a T and a method called Update(), then Property<T> sounds like your ticket:

    public sealed class Property<T>
    {
      readonly Func<T, T> _update;
    
      public Property(T initial, Func<T, T> update)
      {
        Value = initial;
        _update = update;
      }
    
      public T Value { get; private set; }
    
      public void Update()
      {
        Value = _update(Value);
      }
    }
    

    That class encapsulates a readable value and gives you an Update() method to call. No need for an IAnimation<T> interface or any other fancy trappings. Of course you'll have to instantiate instances of Property<T> with an update delegate that does what you want. But you can add some static methods somewhere to set it up in the most common ways. For example:

    public static class PropertyHelpers
    {
      public Property<double> CreateZoomLevelProperty(double initial, double increment, double target)
      {
        return new Property<double>(initial, old =>
        {
          var @new = old + increment;
          if (increment > 0 && @new > target || increment < 0 && @new < target)
            return target;
          else
            return @new;
        });
      }
    }
    

    On the other hand if you want to think of the value as a stream of values controlled by something else in the code and you only need to get read access to the values when they arrive, then perhaps IObservable<T> (and the Reactive NuGet package) is what you're after. For example:

    public class Camera
    {
      double _zoomLevel;
    
      public IDisposable UpdateZoomLevel(IObservable<double> zoomLevels)
      {
        return zoomLevels.Subscribe(zoomLevel => _zoomLevel = zoomLevel);
      }
    }
    

    Of course then it becomes the responsibility of other code somewhere to come up with an IObservable<double> that publishes zoom levels in the fashion that you desire.

    So I suppose my answer can be summed up as this:

    It depends.

    Disclaimer: Perhaps none of the code above will compile