Results 1 to 10 of 10

Thread: Implementing a Game Loop in Delphi

  1. #1

    Implementing a Game Loop in Delphi

    Up until now all of my efforts in Delphi have been of the turn-based variety - expecting user input before it is necessary to do any processing etc.

    I figured a good question for a getting started forum would be;

    How do you implement your game loop?

    I figured on using a TTimer component or just starting a never-ending loop...

    What're the pros and cons of each method? Do you have a preference and why? Is there a different method you prefer?

    Thanks

    Neil

  2. #2

    Re: Implementing a Game Loop in Delphi

    Quote Originally Posted by lnpneil
    How do you implement your game loop?

    I figured on using a TTimer component or just starting a never-ending loop...
    Yes that's the way i do it.. But i use the TDxTimer component (included in DelphiX) that is more precise and performat.
    the pros are automatically calculated fps, easy of use (just fill the ontimer event) and auto-calulated lag (if your routine takes more time than the interval between timer events)
    The cons.. umm nothing particular..
    I've seen other methods too.. for example implementing a procedure that is called when the application is idle.. but i never used it.

    Bye!
    If you save your data in a proprietary format, the owner of the format owns your data.
    <br /><A href="http://msx80.blogspot.com">http://msx80.blogspot.com</A>

  3. #3

    Implementing a Game Loop in Delphi

    Here's how the "idle" methods work:
    note that OnIdle is an event of TApplication, not TForm. So you must set it in the code.


    [pascal]procedure TForm1.AppIdle(Sender: Tobject; var Done: Boolean);
    begin
    // do you dirty jobs here!
    Done := False;
    end;

    procedure TForm1.FormCreate(Sender: TObject);
    begin
    Application.OnIdle := AppIdle;
    end;
    [/pascal]
    If you save your data in a proprietary format, the owner of the format owns your data.
    <br /><A href="http://msx80.blogspot.com">http://msx80.blogspot.com</A>

  4. #4

    Implementing a Game Loop in Delphi

    Whatever you do, don't use TTimer! It sucks big-time because it's based on the Windows WM_TIMER message. This is low-priority, meaning that TTimer only guarantees worse than 55msec per update. If you set its interval to less than 55 msecs (e.g. 1000/30 = timer w/ 33 interval for 30fps), it won't always update that quickly, meaning that your animation gets jerky. I suspect this mistake is common. TTimer sucks for a game loop - avoid it!

    If you're going to do that, you might want to check out a better timer such as the ones from Powerdraw or DelphiX (I forget their names).

    There's a thread about game loop timing over at Turbo

    In general, you use one of the following three functions to get the amount of time passed: GetTickCount, timeGetTime [in "mmsystem" unit] or QueryPerformanceFrequency/QueryPerformanceCounter. These are much more accurate than TTimer (note that timeGetTime requires some setup/deinit code for its best performance under NT).

    You can use one of those three functions (in update speed: QueryPerformanceFrequency/Counter, then timeGetTime, then GetTickCount) to figure out how much time has passed. You can have your timer twiddling its thumbs until the wanted time has passed.

    If you're worried about CPU usage, you can chuck a call to sleep(1) in there if you have more than 10 milliseconds to wait, or alternatively you can use the extra time to do some calculations.

    There are two main choices for a good timer: use Application.OnIdle or a thread-based timer. Again, decide what you want.

    The timer from DelphiX uses timeGetTime and the Application.OnIdle handler (which gets called when the app's not doing anything). Unfortunately, it doesn't init/deinit using timeGetDevCaps+timeBeginPeriod / and timeEndPeriod, which may result in it being less than good on NT systems (IIRC).

    The TRXTimer from RXLib (where have those Russian dudes gone anyway?) uses a thread. I quite like this approach, but I'm sure there are pluses and minuses (synchro is a bit sometimes, though I believe that the VCL graphics are thread-safe from D5+, correct me if I'm wrong here).

    To answer your question, here's how I do mine just now, though I await other ideas (I'm flexible on this issue just now):

    I run the game as fast as I can, using time-based movement (rather than basing movement on "x pixels per frame", I use "x pixels per time_unit", which means that characters will move the same speed regardless of computer power. This means that better computers get nicer, smoother movement, but that both a good and a bad computer will still move the characters at the same speed.

    I use the elapsed time period (in terms of seconds) w/ the default timer being QueryPerformance*. If that's not available then I fall back to timeGetTime. Also, the elapsed times are averaged over several frames to avoid jumps in framerate.

    Here's an email I sent to someone. This may give you some ideas:

    The problem is simply that if you maximize the window, a lot more drawing must take place. This is a bottleneck for the VCL (which is crappy), assuming that you're using that (if you are, think about using a better API such as DirectDraw, SDL, OpenGL, DelphiX...). This causes your game to slow down - which will be noticeable, since less frames will be drawn. What you want is time based movement. If you use, for example, "move 3 pixels for each game frame drawn", then your program will have the problem noted above - it will run at different frame rates depending on the amount of time it takes to draw frames. Instead of doing this, you want to base the movement on *time* rather than *frame*. For example, "move 20 pixels per second (not per frame)". This means that your game will run the same speed regardless of the frames per second. A machine that runs slowly will *move the characters at the same speed, only with more jerkiness*. The faster the computer, the smoother the animation -- but both will end up in the same place at the same time!

    Here's a quick example. Say you had a character that you wanted to move twenty pixels per second. Imagine that we have two computers - one running at ten frames per second, the other two frames per second (yes, they're very slow . Each update, we would calculate how far to move the object based on the current time - this is pretty simple, really (in this case, it'd be (time passed in milliseconds) / 1000, since there are 1000 milliseconds per second). This would give you a 'percent' of how far it should move. This value is then multiplied by the speed of the object. For the fast machine, the updates might take 100 milliseconds (ten frames per second here...). This would result in the character moving (100 / 1000) * 20 each update, equal to 2000 / 1000 = 2 pixels. If we have 10 updates at this speed, the character will move 20 pixels! This holds true for the slower machine: each update will move (500 / 1000) * 20, meaning 10000 / 1000 = 10 pixels. If we have two updates, it will move 2 * 10 = 20 pixels (the same value as the faster computer)! You can see how this effects things, too: the faster computer will display the character more often as it moves across the screen, which will mean smoother movement. The other computer displays only two frames, which means the results will be jerky but accurate.

    Remember to use singles or doubles instead of integers!

    The basic idea:

    Declare two variables:

    [pascal]var
    OldTime: DWORD; { or Int64 if you plan on using queryperf... }
    NewTime: DWORD; { likewise, Int64 for queryperf...}

    { do this once, before your main game starts (maybe at the end of FormCreate or whatever }
    OldTime := TimeGetTime; { or queryperformancecounter, or whatever }

    (inside the timer)
    procedure TForm1.YourTimer(Sender: TObject);
    const
    OneMSec: Single = 1 / 1000; { see notes if you want queryperf... }
    var
    NewTime: DWORD;
    UpdateSpeed: Single;
    i: Integer;
    begin
    NewTime := TimeGetTime;

    UpdateSpeed := (NewTime - OldTime) * OneMSec; { get the speed to update objects }

    OldTime := NewTime; { store this value for the next update }

    { update all your game objects }
    for i := 0 to High(AllGameObjects) do
    begin
    AllGameObjects[i].x := AllGameObjects[i].x + (UpdateSpeed * AllGameObjects[i].X_PixelsPerSec);
    AllGameObjects[i].y := AllGameObjects[i].y + (UpdateSpeed * AllGameObjects[i].Y_PixelsPerSec);
    end;
    end;[/pascal]

    Quick thing to test - is the "OldTime := NewTime" line better off before or after updating the game objects? I haven't checked yet, so be sure to try both.

    Now, a few comments about the above. First of all, it's pseudocode - obvious, but I'd better point that out since it might somehow be wrong. Note that the objects are assumed to have their speed in pixels per second. We're timing our updates in msecs, not seconds - this means that any value from newtime-oldtime must be adjusted (divided by 1000, getting it in terms of how long in seconds). This is doing the same thing I explained in the example. Note, btw, that you'd have to adjust this if you wanted to use QueryPerformanceCounter for ultra-good timing. If that was the case, you'd get the value for the high performance timer using QueryPerformanceFrequency. You'd store that value as 1 / (performance_Freq_value) (again, you have to use a real, not an integer!). This is equivalent to the one_msec constant in the above code - for timeGetTime, you need to know how many milliseconds are in a second (giving the value 1/1000 for the constant). With QueryPerformanceCounter, there are QueryPerformanceFrequency updates per second, giving the time for each as (1/QueryPerformanceFrequency).

    I think that it might be a good idea to store the time difference for the last few frames (picking a number, 8, maybe 16 frames?...) and using the average. This means that the frames per second won't jump about as quickly - not sure if that's a good plan, but I think it probably is. Again, this is something for you to investigate.

    You shouldn't really have to resort to limiting the frame rate. You might, but it only means that faster machines will waste their time, while slower machines will *still* be slow. However, it can simply things a bit...

    This dude has some nice articles (the code's in C++, though, so I don't know if it'll help much...)
    http://www.mvps.org/directx/articles...ady_motion.htm
    http://www.mvps.org/directx/articles..._functions.htm

    I also found this *very* interesting article. Be sure to check it out:
    http://www.flipcode.com/tfiles/steven03.shtml
    I have a work-in-progress article over here. It's unfinished and has been for a while. Here's my current game timer class for non-VCL stuff.

    The main loop for me is your usual PeekMessage/TranslateMessage/DispatchMessage/doUpdates thing (non VCL).

    Hmm. Can't think of anything more off-hand.

    [Edit: added extra 'l' to last link from email. Thanks MSX!]
    "All paid jobs absorb and degrade the mind."
    <br />-- Aristotle

  5. #5

    Implementing a Game Loop in Delphi

    Good replies so far guys thanks!

    One thing I noted though is from the help files in delphi;

    OnIdle is called only once, as the application transitions into an idle state. It is not called continuously unless Done is set to false. Applications that set Done to false consume an inordinate amount of CPU time, which affects overall system performance.
    In all the examples I've seen Done is set to false (obviously so that the game loop is a *loop* I guess ) ... does this imply that using a timer in some fasion is more efficient for the CPU?

    Thanks for any enlightenment!

    Neil

  6. #6

    Implementing a Game Loop in Delphi

    You could use a TThread

  7. #7

    Implementing a Game Loop in Delphi

    Quote Originally Posted by lnpneil
    OnIdle is called only once, as the application transitions into an idle state. It is not called continuously unless Done is set to false. Applications that set Done to false consume an inordinate amount of CPU time, which affects overall system performance.

    In all the examples I've seen Done is set to false (obviously so that the game loop is a *loop* I guess ) ... does this imply that using a timer in some fasion is more efficient for the CPU?

    Neil
    No, it's not more efficient.. simply, setting to done the method is called again and again forever (until it have done set to true). That's why it consumes all the cpu time.
    If you use TDxTimer and set interval to 0, you get the same situation: the event handler is called again and again.
    In both case you can set pauses in the cicle, setting the timer interval to a nonzero value or using the instruction slepp() in the Idle event.
    Pauses are used if you need a fixed fps. Fixed fps is usually good for 2d (that's no need to get a fps faster than 30), but 3d games usually try to do as many cicle as they can, adjusting the world movement by the reciprocal of fps (ie: if fps=20 move the character by 10, if fps=40 move the character by 5)

    PS: for alimonster: your last link is broken, you must add an "L" at the end
    If you save your data in a proprietary format, the owner of the format owns your data.
    <br /><A href="http://msx80.blogspot.com">http://msx80.blogspot.com</A>

  8. #8

    Implementing a Game Loop in Delphi

    Well, here is a basic template I was working on (based on code from from Delphi Graphics and Game Programming Exposed). It's not the most amazing thing in world, but it's simple enough for beginners to grab a hold of. Note that I don't set Done := false, because I immediately call GameLoop upon entering OnIdleHandler, which loops until GameRunning := false (almost the same effect). Also I throttle the loop to 30fps, this of course could be placed elsewhere or increased/decreased as necessary.


    [pascal]
    [background=#FFFFFF][normal=#000080][number=#FF0000][string=#0000FF][comment=#8080FF][reserved=#000000]
    {================================================= ==============}
    { }
    { File: unt_Game.pas }
    { Created By: Name Goes Here }
    { Modified By: N/A }
    { Creation Date: N/A }
    { Last Modified On: N/A }
    { }
    { Description: Simple template for a game in Delphi. }
    { }
    {---------------------------------------------------------------}
    { Copyright Ac XXXX-XXXX All Rights Reserved }
    {================================================= ==============}

    UNIT unt_Game;


    {================================================= =============================}
    INTERFACE
    USES Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
    {================================================= =============================}

    {Global DataType / Constant / Variable Declarations}
    {------------------------------------------------------------------------------}

    TYPE
    TGameState = (gsDemo, gsPlaying, gsIntermission, gsPaused, gsGameOver);

    Tfrm_Game = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure OnIdleHandler(Sender: TObject; var Done: Boolean);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    private
    { Private declarations }
    public
    { Public declarations }
    end;

    CONST
    Throttle = trunc(1000 / 30); {30fps Throttle}

    VAR
    frm_Game: Tfrm_Game;
    GameRunning: Boolean;
    GameState: TGameState;

    {Function / Procedure ProtoTypes}
    {------------------------------------------------------------------------------}

    {None needed yet} {mainly used for forward delcarations}


    {================================================= =============================}
    IMPLEMENTATION {$R *.DFM}
    {================================================= =============================}

    {Game Event Handlers}
    {------------------------------------------------------------------------------}

    procedure Pause(MilliSeconds: Integer);
    var
    CurrentTime: LongInt;
    PauseTime: LongInt;
    begin
    CurrentTime := GetTickCount;
    PauseTime := CurrentTime + MilliSeconds;
    while CurrentTime < (PauseTime) do begin
    Application.ProcessMessages;
    CurrentTime := GetTickCount;
    end;
    end;

    procedure doDemo();
    begin
    //Display automated demonstration/intro here
    Application.ProcessMessages;
    end;

    procedure doPlaying();
    begin
    //Perform all gameplay here
    Application.ProcessMessages;
    end;

    procedure doIntermission();
    begin
    //Perform inter-level loads etc. here
    Application.ProcessMessages;
    end;

    procedure doPause();
    begin
    //Pause gameplay here
    Application.ProcessMessages;
    end;

    procedure doGameOver();
    begin
    //Display game over screen and cleanup here
    Application.ProcessMessages;
    end;

    procedure GameLoop();
    var
    TimeIndex: LongInt;
    begin
    while GameRunning do begin
    //Record time before processing
    TimeIndex := LongInt(GetTickCount);
    //Process game
    case GameState of
    gsDemo: doDemo;
    gsPlaying: doPlaying;
    gsIntermission: doIntermission;
    gsPaused: doPause;
    gsGameOver: doGameOver;
    end;
    //Calculate time it took to process
    TimeIndex := LongInt(GetTickCount) - TimeIndex;
    //Pause if it was too quick
    if (TimeIndex < Throttle) then begin
    Pause(Throttle - TimeIndex);
    end;
    Application.ProcessMessages;
    end;
    end;


    {Form Event Handlers}
    {------------------------------------------------------------------------------}

    procedure Tfrm_Game.OnIdleHandler(Sender: TObject; var Done: Boolean);
    begin
    GameLoop;
    end;

    procedure Tfrm_Game.FormCreate(Sender: TObject);
    begin
    GameState := gsDemo;
    GameRunning := True;
    Application.OnIdle := OnIdleHandler;
    end;

    procedure Tfrm_Game.FormClose(Sender: TObject; var Action: TCloseAction);
    begin
    GameRunning := False;
    end;


    {================================================= ==============}
    { Copyright Ac XXXX-XXXX All Rights Reserved }
    {================================================= ==============}

    END.[/pascal]


    If anyone wants to add, revise, or update the code, please feel free.
    My DGDev forum pascal syntax highlight settings:
    <br />[background=#FFFFFF][comment=#8080FF][normal=#000080]
    <br />[number=#C00000][reserved=#000000][string=#00C000]

  9. #9

    Implementing a Game Loop in Delphi

    Quote Originally Posted by {MSX}
    No, it's not more efficient.. simply, setting to done the method is called again and again forever (until it have done set to true). That's why it consumes all the cpu time.
    As a professional game developer myself, I can assure everyone that most commercial games will use all the CPU time that they can get. You just don't see it because the game is running full-screen. There is no harm in this. It doesn't 'hurt' the CPU. It is no less efficient. The CPU is there to be used. Why not use it? The basic loop for almost every game out there is similar to this:

    1. Are any Windows messages waiting? If yes, process them
    2. Update game.
    3. Draw game.
    4. Go to step 1.

  10. #10

    Implementing a Game Loop in Delphi

    Hehe Alistair helped me with this.

    This is what I did..
    Using the timer that comes with Omega (dunno if its out for public yet though) at http://www.blueworldtech.com/ds/ , and before I was using DXTimer .. so either will do.. make sure their interval is set to 0 (yes zero)
    [pascal]
    const
    OneMSec: Real = 1/1000;
    PixelSpeed: real = (5/Tiles.W); //Speed of player (in tiles)
    [/pascal]

    then you declare
    [pascal]
    OldTime: real;
    [/pascal]
    in your global variables

    then in your timer to get the speed of the object you do

    [pascal]
    NewTime:=TimeGetTime;

    Speed:=(NewTime-OldTime)*OneMSec;

    Speed:=Speed*Tiles.W*PixelSpeed;

    OldTime:=NewTime;
    [/pascal]
    -AOTA Head Programmer
    <br />www.aotaonline.com
    <br />LiquidIce@aotaonline.om

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  
Comodo SSL