SimTimes returned by GameLoop and GameSystemManager are different (SiO2)

Hi

Depending on when we are calling this method on GameLoop for the first time

it will return a different time (always smaller) than SimTime returned by GameSystemManager.

It is because it will be taken as “baseTime” here.

So if the first call to it is 10 seconds after the system manager has started, the step time returned by GameLoop will be 10 seconds behind the one returned by GameSystemManager.

To prevent this I must call gameLoop.getStepTime().update(0);
before starting GameLoop to make sure “baseTime” will be set to 0.

There is something I still don’t understand because baseTime should never be 0. SimTime is supposed to be the “time since the first frame” ie: the simulation time.

If you force baseTime to 0 then the SimTime will be “random time since the CPU nano counter was reset”.

Even though your explanation is clear it’s still somehow not clicking to me what the problem is.

1 Like

There are two SimTime instances. One is maintained by GameSystemManager and one is maintained by GameLoop.

The one maintained by GameLoop claims to be thread safe and its value is get from GameSystemManager.getStepTime().getTime(). (i.e not the System.nanoTime())

I only force “baseTime” to 0 on the GameLoop step time, not the GameSystemManager step time.

Edit:

The time represented by these two methods is not the same. Their difference is arbitrary (could be 10s, 20m, 1h,…) depends on when we first call to GameLoop.getStepTime() after the game system manager is started.

Aren’t they supposed to represent the same time?

1 Like

I think so. I will need to dig deeper to see why I never hit this issue.

1 Like

Note: I just checked in some changes to SimTime that lets the application force the game time to be a certain value. This is useful for restoring the game time from a save game or even just making sure there isn’t a huge step time after a pause/resume.

I don’t remember if this fixes your original issue.

As to the other questions, now that my head is back into this…

GameSystemManager’s SimTime is the authoritative time. If you need to force time to a certain place then that’s the one you modify. Modifying GameLoop’s SimTime is ‘bad’ because it’s only a frame synched copy of what GameSystemManager’s time will be.

…on a given frame, they will always be the same.

In general, the only things that should be using GameLoop’s SimTime are other threads that are not already managed by GameSystemManager… ie: pretty rare in general.

That could be the source of your “these times are different” since you were modifying GameLoop’s time… which is backwards. At least that’s what today’s brain is saying to me.

1 Like

Thanks for the update.

I do not think it will fix the original issue.

From what I remember it is not a copy as it modifies the GameSystemManager’s time.
Hint, this is the problematic line with GameLoop’s SimTime. Note, here we pass sim time we grabbed from GameSystemManager to the method argument, not the real-time, so here baseTime should be 0 and should not change.

From a quick glance at the code, it seems to me that for the first update call it will still return the specified value ignoring the time has passed. Also, it sounds like tpf is going to be negative if timeOffset is a positive value!?

I understand now. GameLoop’s getStepTime() is a weird method that is guaranteed to produce strange results. I think that it’s either misnamed or we need to think about its contract a little better.

As it stands, even if the base time problem were fixed, there are potentially a whole bunch of cases where tpf would be nonsense. So I begin to wonder about the use-case for this value.

How are you using GameLoop’s step time in your case? Are things like tpf important or is it just a matter of getting the nice time conversion on a ‘safe’ game time?

It might be that GameLoop should now just call setCurrentTime() and never call update() at all.

I think the original idea was that tpf would be based on when the other-thread-caller accessed GameLoop’s step time but if that other-thread did it twice in its own update() then time could move… and it completely ignores the fact that other threads might also access it.

Yes, because I’m an idiot. I’ve now refactored all of this code to change the logic of how time is advanced to make it a bit more flexible. (I’ve had to duplicate some logic which is unfortunate but for now I think the code is clearer otherwise.)

The old code was trying to do too much with a single value.

If you get a chance then please make sure I haven’t done something else dumb.

Yes, just to access the thread-safe game time from HostedConnectionServices.

Curious… what kind of services?

For example, the services that I need to get the BodyPosition component, which requires sim time to return entity position.

Edit:
Another example can be ActionBarService which I would need the game time to check the action cooldown time before triggering action. (although we can delegate this into its own dedicated system)

It turns out that I haven’t needed this anywhere myself and that’s why I was curious… because I don’t have any use-cases to draw from.

Can you elaborate on the BodyPosition use-cases where time-accurate tweening is necessary (versus just grabbing the top frame which some of my newer BodyPosition implementations provide)?

Because the time can jump between one GameLoop.getTimeStep() call to the next, I’m trying to make sure that use-cases are actually covered by this method or if it’s just hiding problems for callers that will be surprised later.

I’m using it in “ObjectPickupHostedService”. When the player clicks on a pickup item, the client will delegate it to server-side ObjectPickupHostedService, there I check the player distance from the item and if it is in pickup radius then trigger the “Take” action.

It looks like my copy of the BodyPosition component does not provide a method to grab the top frame. It is backed by TransitionBuffer (from sim-math) which seems to have no method to get the last frame.

Ah. In my case, game state modifying actions are all queued up on game system somewhere so that there aren’t strange multiplayer issues, ie: a command queue is the arbiter of ordering. That’s why I don’t this use-case myself.

This may be something I only added to the MOSS version (unreleased but available to patrons as early access). Here is that class for reference:

/*
 * $Id$
 * 
 * Copyright (c) 2021, Simsilica, LLC
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions 
 * are met:
 * 
 * 1. Redistributions of source code must retain the above copyright 
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright 
 *    notice, this list of conditions and the following disclaimer in 
 *    the documentation and/or other materials provided with the 
 *    distribution.
 * 
 * 3. Neither the name of the copyright holder nor the names of its 
 *    contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.simsilica.bpos;

import com.simsilica.es.EntityComponent;
import com.simsilica.es.EntityId;
import com.simsilica.mathd.Quatd;
import com.simsilica.mathd.Vec3d;
import com.simsilica.mathd.trans.PositionTransition3d;
import com.simsilica.mathd.trans.TransitionBuffer;


/**
 *  A component representing the position of mobile objects that internally 
 *  keeps track of a small history of position, rotation, and visibility changes.   
 *  This is not a normal immutable component and thus manages its own threading.  
 *  Furthermore, special care is taken to make sure that all BodyPosition objects for
 *  a particular entity share the same internal data buffer.
 *
 *  <p>This class can be used in true client-server situations where the
 *  client may be viewing server state hundreds of milliseconds behind but
 *  it can also be used in local multithreading where the physics runs on
 *  its own thread.  In the latter case, the history buffer will be small
 *  and provide just enough to tweening to smooth out frame jitter between
 *  the threads. 
 *
 *  @author    Paul Speed
 */
public final class BodyPosition implements EntityComponent {

    private transient int size;
    private transient TransitionBuffer<ChildPositionTransition3d> position;
    private transient ChildPositionTransition3d lastTransition;

    public BodyPosition() {
    }
    
    public BodyPosition( int history ) {
        this.size = (byte)history;
        this.position = ChildPositionTransition3d.createChildBuffer(history);    
    }
 
    /**
     *  Called for a retrieved entity to make sure this BodyPosition
     *  has it's shared transition buffer.  It must be called for
     *  all retrieved BodyPosition components before use.
     */
    public void initialize( EntityId id, int size ) {
        if( this.position == null ) {
            this.size = size;
            this.position = BodyPositionCache.getBuffer(id, size);
        }
    }
 
    public boolean isInitialized() {
        return position != null;
    }
    
    public TransitionBuffer<ChildPositionTransition3d> getBuffer() {
        return position;
    }
 
    public void addFrame( long endTime, Vec3d pos, Quatd quat, boolean visible ) {
        addFrame(endTime, null, pos, quat, visible);
    }
    
    public void addFrame( long endTime, EntityId endParent, Vec3d pos, Quatd quat, boolean visible ) {
        ChildPositionTransition3d trans = new ChildPositionTransition3d(endTime, endParent, pos, quat, visible);
        getBuffer().addTransition(trans);
        this.lastTransition = trans; 
    }        
 
    public ChildPositionTransition3d getFrame( long time ) {
        return getBuffer().getTransition(time);        
    }
 
    /**
     *  A convenience method for the server-side code to get the most recent update
     *  applied through addFrame().
     */
    public ChildPositionTransition3d getLastFrame() {
        return lastTransition;
    }

    /**
     *  A convenience method for the server-side code to get the most recent position
     *  applied through addFrame().
     */    
    public Vec3d getLastLocation() {
        if( lastTransition == null ) {
            return null;
        }
        return lastTransition.getPosition(lastTransition.getEndTime(), true);
    }

    /**
     *  A convenience method for the server-side code to get the most recent orientation
     *  applied through addFrame().
     */    
    public Quatd getLastOrientation() {
        if( lastTransition == null ) {
            return null;
        }
        return lastTransition.getRotation(lastTransition.getEndTime(), true);
    }
    
    public EntityId getLastParentId() {
        if( lastTransition == null ) {
            return null;
        }
        return lastTransition.getParentId(lastTransition.getEndTime(), true);
    }
    
    @Override
    public String toString() {
        return "BodyPosition[" + position + "]";
    } 
}

Yeah, this is in my plan as well.

Thanks for sharing.

I have modified GameLoop to just call safeTime.setCurrentTime() directly. (Effectively rendering its tpf totally inert now.)

I think this fixes the base time issue that you reported. Let me know if I got something else wrong or missed something.

1 Like

Thanks, it looks good to me.

Is “EtherealHost” time source called from the network thread? If so, I wonder if getUnlockedTime() is safe to be used in the below code?

ethereal.setTimeSource(() -> systems.getStepTime().getUnlockedTime(System.nanoTime()));

In the old code, it was depending on baseTime which was set just once and never changed, so it was Ok to call it from another thread I believe but in the new code, it depends on gameTime and lastRealTime which are periodically updated from game loop.

I think it should be ok. lastRealTime is based on the same clock. getUnlockedTime() is meant to get the mid-frame times.

The only weird thing to me that could happen is if lastRealTime is somehow behind because it’s not volatile but because clock time is passed in then it should still be ok to use the previous frame’s version.

Might be worth documenting it on method.

Could it be that lastRealTime and gameTime values are not cached at once but at different times in the networking thread? (for example, the thread reset lastRealTime cache at time X while resetting gameTime cache at time Y) In that case, the returned unlocked time will be invalid.

Yeah, I guess it’s possible. Probably I should synchronize the update and make the two variables volatile.

I will think about if there is a better way.

Maybe you can revert the getUnlockedTime() to use the baseTime like before and make the variable volitile.