Main Loop with Fixed Time Steps

Hi all,

Hope my questions make some sort of sense. New to game dev (Some friends and I decided to make an RTS game).

From reading around most RTS games seems to use some sort of fixed-step game logic update (e.g. 15 fps) to be perfectly deterministic while allowing the rendering to run as fast as possible (smoothly interpolate from one logic state to the next). Here is a link to an algorithm: http://www.flipcode.com/cgi-bin/fcarticles.cgi?show=63823

I was thinking of setting this up in panda by using 2 Task.TaskManager, splitting the tasks (panda tasks and my tasks) between them and calling step on them separately. Here is some code:

FRAMES_PER_SECOND = 15
TICK_TIME = 1.0 / FRAMES_PER_SECOND

percentWithinTick = 0.0
time0 = globalClock.getFrameTime()

indTickTaskMgr = Task.TaskManager()
indTickTaskMgr.add(base.igLoop, "igLoop")
indTickTaskMgr.add(IndependentTickRun, "IndependentTickRun")

taskMgr = taskMgr
taskMgr.remove("igLoop")
indTickTaskMgr.add(GameTickRun, " GameTickRun")

while(True):
    time1 = globalClock.getFrameTime()
    while (time1 - time0) > TICK_TIME:
        taskMgr.step()
        time0 += TICK_TIME
    
    percentWithinTick = min(1.0,(time1 - time0)/TICK_TIME)
    
    indTickTaskMgr.step()
  1. Is using two TaskManager an OK way of doing this type of thing?
  2. Is this a reasonable way to smoothly interpolate from one logic state to the next (could do it myself with percentWithinTick if needed)?

This is some what similar to https://discourse.panda3d.org/viewtopic.php?t=346&highlight=orig. I add the ivalLoop to run at render fps.

indTickTaskMgr.add(base.ivalLoop, "ivalLoop")
taskMgr.remove("ivalLoop")

At the beginning of logic update (taskMgr.step). I first store the entity original position. Than run a task (e.g. Interpolate) after the collisionLoop to get the new position, than use posInterval with TICK_TIME (1.0 / FRAMES_PER_SECOND).

  1. My thinking is that all the internals of panda systems (collision detection) should run the same, well the same for 15 fps. Is this corrected? Will resetPrevTransform be ok running at a different speed that render?

  2. Any deterministic issues with this?

Any help or opinion on any of these would be much appreciated. By the way panda is great.

Thanks your time,
Niall

Heard a hack together example of this with interpolation:

from pandac.PandaModules import *
from direct.task import Task
from direct.actor import Actor
from direct.interval.IntervalGlobal import *
import direct.directbase.DirectStart
import sys

FRAMES_PER_SECOND = 15
TICK_TIME = 1.0 / FRAMES_PER_SECOND

class World(DirectObject):
    def __init__(self):
        self.smiley_posInterval = None
        self.orig = None
        
        base.disableMouse()
        camera.setPosHpr(50, -150, 0, 0, 0, 0)
    
        
        self.smiley = loader.loadModel("smiley")
        self.smiley.reparentTo(render)
    
    #logic update
    def gameTickRun(self, task):
        #stop the interval if still playing
        if self.smiley_posInterval != None and self.smiley_posInterval.isPlaying():
            self.smiley_posInterval.finish()
        
        #move the entity
        self.orig = self.smiley.getPos()
        self.smiley.setX(self.smiley.getX() + (TICK_TIME * 10))
        
        return Task.cont
    
    #smoothly interpolate from one logic state to the next
    def Interpolate(self, task):
        newPos = self.smiley.getPos()
        
        self.smiley.setPos(self.orig)
        self.smiley_posInterval = self.smiley.posInterval(TICK_TIME, newPos, startPos = self.orig)
        self.smiley_posInterval.start()
        return Task.cont
    
    #run every frame
    def independentTickRun(self, task):
        return Task.cont
    
w = World()

indTickTaskMgr = Task.TaskManager()
indTickTaskMgr.add(base.igLoop, "igLoop")
indTickTaskMgr.add(base.ivalLoop, "ivalLoop")
indTickTaskMgr.add(w.independentTickRun, "independentTickRun")

taskMgr = taskMgr
taskMgr.add(w.gameTickRun,"gameTickRun")

#setup posInterval after collisionLoop
taskMgr.add(w.Interpolate,"Interpolate", 47)
taskMgr.remove("igLoop")
#run interval system every frame 
taskMgr.remove("ivalLoop")

time0 = globalClock.getFrameTime()

while(True):
    time1 = globalClock.getFrameTime()
    while (time1 - time0) > TICK_TIME:
        taskMgr.step()
        time0 += TICK_TIME
        
    indTickTaskMgr.step()

:slight_smile:

It looks like you are taking away the functionality fo the original task manager and then re-implementing it into indTickTaskMgr. Why not just leave the original task manager alone and just create a new fixed-step task manager? Or even simpler, just create a fixed-step task within the taskmanager that is running at full speed.

import direct.directbase.DirectStart
from direct.task import Task
import math

lastTick = 0
def printTime(task):
    """"prints the time of this task about every second"""
    global lastTick
    dt = task.time - lastTick
    if dt >= 1.0:
        #put logic updates here
        print task.time
        lastTick = math.floor(task.time)
    return Task.cont

taskMgr.add(printTime, "printTime")

run()

this was put togethere very briefly. I hope it gets the point across.

The tasks running non fixed-stepped (every frame) would be the exceptions to general rule of 15fps (Possible only 2 ivalLoop, igLoop). I am afraid that taskMgr (being global) may be used else where inside panda (e.g. like enabling physics base.enableParticles() which adds a task to taskMgr, I think).

I think I need some of panda built-in tasks (well the ones involved with collisions anyway, collisionLoop) to run at 15 fps in order to get deterministic results on different speed pc’s. I may be able to overwrite/rewrite panda system tasks like collisionLoop in ShowBase.py to run 15fps but not sure if this would be a good idea.

Thanks for reply. Probably should be clearer on why deterministic results are important for my project.

I think a lot of my problems are going to be getting deterministic results. The reason deterministic seems so important is for networking in an RTS game (lockstep). Something like this article on Age of Empires is what I’m thinking of doing http://www.gamasutra.com/features/20010322/terrano_01.htm

Niall

I see. I didn’t realize you wanted to place those specific loops in the fixed step manager. At any rate, I dont think what you are trying to do would be a problem, at least no major flaws are jumping out at me right now. I would still use the new task manager you create as the fixed-step manager just to be safe. You can pull out whatever tasks you need from the original and add them to your fixed-step manager as needed. My reasons for that would be that panda’s task manager has specific hooks for when its minimized (and maybe some other states) to lower processing power when it doesn’t have to render the screen. If you transfer the rendering loops to a different task manager, you might not get the same results.

Good to hear this approach at least looks reasonable. I like your thinking about panda’s task manager, makes sense. I think I’ll push ahead with this approach now, will post back any findings. Thanks for the help.

Niall