Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Best practices concerning time handling  (Read 4038 times)

0 Members and 1 Guest are viewing this topic.

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Best practices concerning time handling
« on: May 25, 2011, 03:06:50 pm »
The recent SFML change from float seconds to sf::Uint32 milliseconds leads to some trouble in my projects, especially Thor. The library must be more or less consistent with SFML, however at the same time it should be user-friendly and abstract from some basic concepts. For me, the old system was actually perfectly fine ;)

I have now changed thor::StopWatch, thor::Timer and thor::TriggeringTimer to use sf::Uint32 like sf::Clock, and I'm not sure whether this decision was a good one. While it is consistent with SFML itself, the price for this is high. On the other side, those classes are somehow higher-level than sf::Clock, hence a change towards user-friendliness (float seconds) might be appropriate.

The other place where I use times is for mechanisms that need steady updating like thor::ParticleSystem or thor::Animator. They take a parameter float dt which specifies the frame time difference. I don't want to change them to sf::Uint32 because:
  • Seconds are easier to imagine.
  • Floats make computations easier, because most other types are floats as well, and because you can store and calculate times "between" integral numbers (e.g. 0.8f * lastTime).
  • If I used milliseconds, all the dependent units like acceleration (particle affectors) should also be measured in milliseconds for consistency. This leads to very small, hard imaginable numbers.
However, the status quo is not ideal either, since conversions are omnipresent:
Code: [Select]
particleSystem.Update(window.GetFrameTime() / 1000.f);
stopWatch.Reset(static_cast<sf::Uint32>(seconds * 1000));

What do you think? Should I write wrappers that return floats? Should my time classes work with floats, although sf::Clock uses sf::Uint32? Other ideas?
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

Laurent

  • Administrator
  • Hero Member
  • *****
  • Posts: 32498
    • View Profile
    • SFML's website
    • Email
Best practices concerning time handling
« Reply #1 on: May 25, 2011, 03:22:31 pm »
Quote
Seconds are easier to imagine.

If you already changed the rest of the API to milliseconds, it shouldn't be a problem here.

Quote
Floats make computations easier, because most other types are floats as well, and because you can store and calculate times "between" integral numbers (e.g. 0.8f * lastTime).

Receiving the time as integer milliseconds doesn't prevent from using floats internally to store the accumulated time. You can limit the modification to the public API.

Quote
If I used milliseconds, all the dependent units like acceleration (particle affectors) should also be measured in milliseconds for consistency. This leads to very small, hard imaginable numbers.

Can you show an example for this? That would make things clearer for those who are not familiar with your classes :)

Whatever you choose, be consistent. I think the weirdest thing would be to keep two different conventions in the same API.
Laurent Gomila - SFML developer

Laurent

  • Administrator
  • Hero Member
  • *****
  • Posts: 32498
    • View Profile
    • SFML's website
    • Email
Best practices concerning time handling
« Reply #2 on: May 25, 2011, 03:25:18 pm »
There's maybe another solution: write an abstraction for time too. Like a thor::Time class, with methods to get seconds or milliseconds, as integer or float ;)
Laurent Gomila - SFML developer

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Best practices concerning time handling
« Reply #3 on: May 25, 2011, 05:43:23 pm »
Quote from: "Laurent"
If you already changed the rest of the API to milliseconds, it shouldn't be a problem here.
I only changed the direct wrappers around sf::Clock (i.e. the pausable clocks and timers), and I haven't thought of all the implications in advance :)

Quote from: "Laurent"
Receiving the time as integer milliseconds doesn't prevent from using floats internally to store the accumulated time. You can limit the modification to the public API.
Yes, but this is still more complicated because for every parameter and return value, a conversion is necessary. Unfortunately, this is not the kind of "change-the-implementation-once-and-it-will-work-forever" situations, since users are confronted with different types as soon as they customize behavior (e.g. they write a custom particle emitter that inherits the interface).

Quote from: "Laurent"
Can you show an example for this? That would make things clearer for those who are not familiar with your classes :)
Sure:
Code: [Select]
// Constructor for affector that applies an
// acceleration to each particle
ForceAffector::ForceAffector(sf::Vector2f acceleration)
: mAcceleration(acceleration)
{
}

// Virtual method, overrides Affector::Affect()
// mAcceleration is a member of type sf::Vector2f
void ForceAffector::Affect(Particle& particle, float dt)
{
    particle.Velocity += dt * mAcceleration;
}

With sf::Uint32 dt, I could either use dt / 1000.f * mAcceleration, but then acceleration is measured per second, while all time units are milliseconds. The other option is static_cast<float>(dt) * mAcceleration, but then the acceleration has small values that are difficult to choose ("how strong is the acceleration per millisecond?").  In both ways, the implementation is not straightforward anymore, which is relevant when users write similar functions.

Quote from: "Laurent"
Whatever you choose, be consistent. I think the weirdest thing would be to keep two different conventions in the same API.
I actually tend to use float throughout the library, even though SFML uses sf::Uint32, since it is most probably more convenient for the end user. One possibility is to keep the time-measuring entities (StopWatch etc.) taking sf::Uint32 like sf::Clock and the update-mechanisms (ParticleSystem etc.) taking float, but then we have the inconsistence again. Which is worse?

Quote from: "Laurent"
There's maybe another solution: write an abstraction for time too. Like a thor::Time class, with methods to get seconds or milliseconds, as integer or float ;)
That is also a possibility, but without implicit conversions from/to float, the code is rather inconvenient (a little less than writing *1000 or /1000 each time)...
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

Laurent

  • Administrator
  • Hero Member
  • *****
  • Posts: 32498
    • View Profile
    • SFML's website
    • Email
Best practices concerning time handling
« Reply #4 on: May 25, 2011, 06:09:32 pm »
Quote
One possibility is to keep the time-measuring entities (StopWatch etc.) taking sf::Uint32 like sf::Clock and the update-mechanisms (ParticleSystem etc.) taking float, but then we have the inconsistence again. Which is worse?

The problem is that if you keep float for clock-like classes, you'll end up with the same problems that SFML had: loss of precision after a few hours. Floats are ok only for delta times.

Quote
That is also a possibility, but without implicit conversions from/to float, the code is rather inconvenient (a little less than writing *1000 or /1000 each time)...

Actually, I find this solution rather convenient. It's a little longer to type but you get exactly what you want.
Code: [Select]
void ForceAffector::Affect(Particle& particle, Time dt)
{
    particle.Velocity += dt.seconds() * mAcceleration;
}
Laurent Gomila - SFML developer

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Best practices concerning time handling
« Reply #5 on: May 25, 2011, 06:44:21 pm »
Quote from: "Laurent"
The problem is that if you keep float for clock-like classes, you'll end up with the same problems that SFML had: loss of precision after a few hours. Floats are ok only for delta times.
That is true. Would you find it very inconsistent if I kept sf::Uint32 for the clock-like classes and float for the delta-time functions?


Quote from: "Laurent"
Actually, I find this solution rather convenient. It's a little longer to type but you get exactly what you want.
Yes, I just see some problems with this approach, even apart from the need to type a lot.

Should I overload two constructors for float and sf::Uint32?
Code: [Select]
thor::Time(4)
thor::Time(4.f)

Probably not, because the above lines mean something completely different. Maybe use the named constructor idiom? This is clearer, but again a lot to type (often redundantly).
Code: [Select]
thor::Time t = thor::Seconds(4.f);
thor::Time u = thor::Milliseconds(4000);

The Time class ought to overload the following operators:
  • Between two Time objects: + - += -= < <= > >= == !=

For exact comparisons, I should use integral types internally. What about subtractions that lead to negative numbers?
  • Between Time and factor: * *= / /=

Of which type should the factors be? float? What about negative factors?
  • Unary - and +

Negative times can be meaningful when talking about time differences, but raise confusion in other cases. Then I also need a signed internal type.[/list]What should happen in the following example? No one expects a loss of precision, but this is exactly what occurs when integers are used internally.
Code: [Select]
Time t = ...;
assert(t == t / 50 * 50);

But when I use floating point types, I may lose precision as well. Therefore, it might happen that the assertion fails:
Code: [Select]
thor::Time t = thor::Milliseconds(x);
assert(t.ToMilliseconds() == x);

Such a time class must provide a lot of guarantees to fulfill the expectations, of which some are hard to achieve.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

Laurent

  • Administrator
  • Hero Member
  • *****
  • Posts: 32498
    • View Profile
    • SFML's website
    • Email
Best practices concerning time handling
« Reply #6 on: May 25, 2011, 08:16:46 pm »
Quote
Would you find it very inconsistent if I kept sf::Uint32 for the clock-like classes and float for the delta-time functions?

No, I think this is a valid solution.

I think that the thor::Time class can work with a Int32 (or Int64), divisions and multiplications don't need to be exact. If millisecond is the indivisible base unit, everything's fine.

But ok... I admit that it can bring more problems than it solves ;)
Laurent Gomila - SFML developer

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Best practices concerning time handling
« Reply #7 on: May 25, 2011, 08:49:30 pm »
Okay, then I will probably leave sf::Uint32 for clocks and float for delta-time updates for a while, and keep the thor::Time in mind. The idea is really good, but such a class brings more issues than one might first think. If more people begin to use Thor, they will probably give feed-back for designs they don't like. And if they don't, I can't do anything ;)

Anyway, thank you very much for your advice! Also your contributions in the Thor project threads are very useful, it's really nice that you take your time! :)
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

Laurent

  • Administrator
  • Hero Member
  • *****
  • Posts: 32498
    • View Profile
    • SFML's website
    • Email
Best practices concerning time handling
« Reply #8 on: May 25, 2011, 09:56:32 pm »
It's always a pleasure to discuss design issues. Actually, it helps me too; I already thought about a Time class (but it's definitely too high level for SFML).

And you also helped me a lot in the past for SFML ;)
Laurent Gomila - SFML developer