0
\$\begingroup\$

As I've seen at quite a few website, the best way to handle variable frame rate and movement (moving projectiles, monsters, swinging swords)... looks something like this:

//This
position += TotalSpeed * (float)gt.ElapsedGameTime.TotalSeconds;

//Or this (direction is being normalized)
position += direction * (float)(TotalSpeed * gt.ElapsedGameTime.TotalSeconds);

And then in the Game's main constructor (Game1.cs):

        IsFixedTimeStep = false;
        TargetElapsedTime = TimeSpan.FromSeconds(16.666); //16.666 milliseconds, or about 60 FPS.

        graphics.SynchronizeWithVerticalRetrace = true;

I started to notice that whenever my FPS was at 30, my projectiles (in this case, arrows) would move slower than when at full speed. Turning V-Sync to false above unlocked the frame rate so then I'm going around 120 FPS. Projectiles are going much faster.

Unfortunately, this applies to everything else.

Am I doing something wrong, or is TotalSeconds just that unreliable/broken? I plan to switch over to Monogame eventually, but I'm not sure if that would fix the issue.

An additional note that may or may not be related: changing 'IsFixedTimeStep' to true causes my game to not run -- no errors, just a blank window-colored (beige-ish) screen.

Thanks in advance.

EDIT: projectile movement code

        direction = new Vector2((float)Math.Cos(rotation),
                                (float)Math.Sin(rotation));

        if (direction != Vector2.Zero)
            direction.Normalize();

        position += (direction * TotalSpeed) * (float)gt.ElapsedGameTime.TotalSeconds;

Pretty normal movement code. 'rotation' is just the direction the player is looking. 'TotalSpeed' is 2300f.

\$\endgroup\$
  • 3
    \$\begingroup\$ The TimeSpan.FromSeconds(16.666) line looks like you're mixing seconds & milliseconds - should that not be either TimeSpan.FromMilliseconds(16.666) or TimeSpan.FromSeconds(0.016666) ? \$\endgroup\$ – DMGregory Jan 25 '16 at 4:06
  • \$\begingroup\$ Thanks @DMGregory, that fixed the additional note issue. Unfortunately, the speed issue is still present when it is not a fixed time step. \$\endgroup\$ – Shyy Guy Jan 25 '16 at 4:49
  • \$\begingroup\$ It sounds like you are putting logic code in the Draw function instead of the Update function. The update function gets called a set number of times (as dictated by the TargetElapsedTime) even if that requires dropping Draw calls. \$\endgroup\$ – ClassicThunder Jan 25 '16 at 5:46
  • 1
    \$\begingroup\$ blogs.msdn.com/b/shawnhar/archive/2007/07/25/… \$\endgroup\$ – ClassicThunder Jan 25 '16 at 5:46
  • 1
    \$\begingroup\$ "The correct way to structure pretty much any game is to have a strict separation between drawing and updating code. Both share the same state information, but their responsibilities are very different: Update reads inputs, modifies the game state, and never touches the graphics device. Draw reads but does not modifiy the game state, and has no access to inputs" Also credited to Shawn Hargreaves. \$\endgroup\$ – ClassicThunder Jan 25 '16 at 19:32
1
\$\begingroup\$

I suspect there is an error in your update or draw loop (note, both have a gametime parameter- but you should only update game logic in the Update loop).

To demonstrate I made this demo:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Diagnostics;

namespace TimeStepDemo
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Texture2D _pixel; // have a pixel to draw stuff.

        Vector2 _position; //position of the object.
        Vector2 _center; //center of the circle.
        double _angle; //current angle of the circle.
        double _angleSpeed; //how fast the object should go.

        float _frametime; // record the time a frame in the update took.
        long _millisecondsPerCircle; //how many miliseconds to complete a circle (to compare settings).
        Stopwatch _stopwatch; //to recored the time.

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            //----------------------
            // Try different combinations of true and false here!
            //----------------------
            IsFixedTimeStep = true;  
            graphics.SynchronizeWithVerticalRetrace = false;

            graphics.ApplyChanges();
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            //have one white pixel texture.
            _pixel = new Texture2D(GraphicsDevice, 1, 1);
            _pixel.SetData<Color>(new Color[] { Color.White });

            //set the position and angle speed of the object.
            _center = new Vector2(300, 300);
            _angle = 0;
            _angleSpeed = 4;

            //initialize the stopwatch to measure the time it takes for the object to complete a circle.
            _millisecondsPerCircle = 1;
            _stopwatch = new Stopwatch();
            _stopwatch.Start();
            //note: this value should be fairly constant 
            //regardless of the fixed timestep
            //or vertical retrace.

        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            //remember the angle (to measure time).
            double oldangle = _angle;

            //update the moving object:
            _angle = (_angle + _angleSpeed * gameTime.ElapsedGameTime.TotalSeconds) % (2*Math.PI);
            _position = _center + new Vector2((float)Math.Cos(_angle), (float)Math.Sin(_angle)) * 150;

            if(oldangle>_angle) //we have looped if we pass the 0.
            {
                _millisecondsPerCircle = _stopwatch.ElapsedMilliseconds;
                _stopwatch.Restart();
            }

            //record the frametime.
            _frametime = (float)gameTime.ElapsedGameTime.TotalSeconds;

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            //draw a red moving box:
            spriteBatch.Draw(_pixel,new Rectangle((int)_position.X,(int)_position.Y,7,7),Color.Red);

            //draw the frametime:
            spriteBatch.Draw(_pixel, new Rectangle(0, 2, (int)(10000 *_frametime), 10), Color.Blue);

            //draw the time it took to complete a circle:
            spriteBatch.Draw(_pixel, new Rectangle(0, 22, (int)(_millisecondsPerCircle/10), 10), Color.Green);

            spriteBatch.End();


            base.Draw(gameTime);
        }
    }
}

The result is a red box going in circles. A blue bar shows the frametime (shorter without fixed timestep). The green bar shows the time it took to complete a circle. The green bar- shown after 1 completed circle, is a constant length regardless of the frametime. This shows that the red object moved equally fast in each scenario. I used bars to visualize the timeframe, since I didn't want to introduce a spritefont- just copy this code and it should work.

In the Game() constructor you can change the values for fixed timestep and the vsync to true or false. Note how the fixedtimestep influences the length of the blue bar. Also the vsync influences the frametime. To max out fps, set both to false.

The code for moving an object is a bit different (for demonstration purposes) but the principle of movement and frametime is the same. Hopefully this demonstrates how to unlock the fps and have higher framerates while the objects move at the same speed. Perhaps it helps you debug your own code.

\$\endgroup\$
  • \$\begingroup\$ Thank you for taking the time to write this. I applied this code in game1.cs of my project and did some testing (with both my original code enabled and disabled). I realized that it's probably just the projectile code that is messing up. Swords actually do swing as expected so it's probably more specific to my project. I'll mark yours as the answer. \$\endgroup\$ – Shyy Guy Jan 25 '16 at 20:32
0
\$\begingroup\$

My best guess is that you have some rounding errors. You aren't actually showing the movement code for the bullets so it's really hard to tell.

Edit: Looking at the code, it should(tm) work. It definitely works fine to do things like this in the games I've written with XNA. The thing that bothers me is that your game doesn't seem to be running at all when you set FixedTimeStep to true, so something really weird must be going on somewhere. Could it be that you didn't separate Update and Draw logic properly?

Edit2: The TargetElapsedTime is set to 16.666 seconds instead of milliseconds, so that's most likely causing the issues.

\$\endgroup\$
  • \$\begingroup\$ I edited my question to include the usual movement code. \$\endgroup\$ – Shyy Guy Jan 25 '16 at 3:41
  • \$\begingroup\$ I added an edit to my post. You must be doing something really weird. So that FixedTimeStep will cause your game not to run. I recommend you debug it when it doesn't run and you look into the actual values of TotalSeconds when your game is running fast or slow. \$\endgroup\$ – Nils Ole Timm Jan 25 '16 at 3:52
  • \$\begingroup\$ I did some debugging to get to the root of that issue. It goes through everything in the first update just fine, then draw doesn't get called. I put a breakpoint on the base.Update(gameTime); in game1.cs, and another breakpoint on the very first line in Draw();, which is GraphicsDevice.Clear(Color.Black);. It doesn't make it to the Draw code. After writing this comment, I tried commenting out TargetElapsedTime = ..., and now it runs. Odd... \$\endgroup\$ – Shyy Guy Jan 25 '16 at 4:01
  • \$\begingroup\$ DUDE: TargetElapsedTime = TimeSpan.FromSeconds(16.666); Are you sure that isn't 16.666 seconds target time? \$\endgroup\$ – Nils Ole Timm Jan 25 '16 at 4:05
  • 1
    \$\begingroup\$ Some good news: setting IsFixedTimeStep to true and VSync to false, I tried shooting the arrow at both 60 FPS and 2 FPS. Both went almost the exact same distance. So that way works, though it is still somewhat perplexing why it won't work when not in a fixed time step... I don't think people care that much about having FPS unlocked(... or do they?), so I'll leave it like that for now. \$\endgroup\$ – Shyy Guy Jan 25 '16 at 4:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.