Upcoming Events
VIEW Conference 2009
11/4 - 11/7 @ Turin, Italy

Project Horseshoe
11/5 - 11/8 @ Burnet, TX

Independent Game Conference West
11/5 - 11/6 @ Los Angeles, CA

IGDA Leadership Forum
11/12 - 11/13 @ San Francisco, CA

More events...


Quick Stats
7100 people currently visiting GDNet.
2337 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  search:   

2D Car Physics


Phase One

Phase one, as I mentioned, is to create the renderer; something graphical so we can actually see what our simulation is doing. This will make it a lot easier to debug. Create a windows form project in C# and place a picturebox control on it (name it "screen"). This control is where we will display our simulation. We could just start drawing to this screen but we’re going to be using double buffering as well to avoid flicker, so we need to create the back buffer now. That bit of code looks like this.

Graphics graphics; //gdi+
Bitmap backbuffer;
Size buffersize;

//intialize rendering
private void Init(Size size)
{
  //setup rendering device
  buffersize = size;
  backbuffer = new Bitmap(buffersize.Width, buffersize.Height);
  graphics = Graphics.FromImage(backbuffer);
}
The Init function must be called with the size of the “screen” control that you created on the form. This will create a bitmap “backbuffer” to which we can do our offscreen rendering. We’ll then take this backbuffer and draw it to the screen to illiminate any flickering. This is how you draw a basic shape to the backbuffer, and present it to the screen.
//main rendering function
private void Render(Graphics g)
{
  //clear back buffer
  graphics.Clear(Color.Black);

  //draw to back buffer
  graphics.DrawLine(new Pen(Color.Yellow), 1, 0, 1, 5);

  //present back buffer
  g.DrawImage(backbuffer, new Rectangle(0, 0, buffersize.Width, buffersize.Height), 0, 0, buffersize.Width, buffersize.Height, GraphicsUnit.Pixel);
}
This function is called from the on_paint method of the "screen" control placed on our form. The on_paint method has a parameter “e” that contains a graphics object we can use to draw to the control. We pass this graphics object to the render function and as you can see, we draw the backbuffer to it as the very last step.

Now by default, the graphics of a picturebox control has the origin in the top-left corner, and extends downward for +y and to the right for +x. This is highly unnatural for most cases. In addition to that, it has extremely large units. Since we will be simulating in the metric system, I recommend introducing a scale factor to scale up the simulation and make it much more visible. The transformation looks like this and takes place after Graphics.Clear() is called.

graphics.ResetTransform();
graphics.ScaleTransform(screenScale, -screenScale);
graphics.TranslateTransform(buffersize.Width / 2.0f / screenScale, -buffersize.Height / 2.0f / screenScale);
That transformation flips the Y axis so that +Y points up. It simultaneously scales the space by our “screenScale” factor (something like 3.0f should work fine). Next, we translate the graphics space into the center of the screen control by half of the screen dimensions divided by our scale (since we are now in the scaled space.)

Now the line should draw starting right at the center of the screen.

Forms Wiring

Up until now, I havn’t explained how to connect all the functions. The first thing you’ll need to do is call the Render function from your on_paint event. Next, you’ll need to create a function that gets called continously to update the simulation. It is preferred to call this function on the Application_Idle event. So create an event handler for Application_Idle and have it call your DoFrame function. Inside this function you’ll need to

  1. Process input
  2. Update the simulation
  3. Invalidate the screen control
The last step is so that an On_Paint gets triggerd and the simulation gets drawn. You’ll also want to wire up some “key_down” and “key_up” events to keep track of key states.

The Timer

Since we don’t know how often our DoFrame function will be getting called, we need to code everything to handle a variable time step. To utilize this we must measure the time between DoFrame calls. So I’ll introduce the timer which, very simply, queries the number of milliseconds that have passed since the computer was turned on. So we store this number every frame and on a subsequent frame we compute the difference, which gives us the amount of time that has passed since the last frame. Here is my very simple timer object. Note: you will need to call GetETime in your intialize function in order to clear the timer, otherwise the first call to it will return the amount of time that has passed since the computer was turned on.

class Timer
{
  //store last time sample
  private int lastTime = Environment.TickCount;
  private float etime;

  //calculate and return elapsed time since last call
  public float GetETime()
  {
    etime = (Environment.TickCount - lastTime) / 1000.0f;
    lastTime = Environment.TickCount;

    return etime;
  }
}

Conclusion of Phase One

So up until now we’ve covered: setting up a rendering surface using GDI, wiring a form to process a game loop and draw it to the screen, and computing the time that has passed since the last frame. Our application looks like this:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Text;
using System.Windows.Forms;

namespace racing_simulation_2d
{
  //our main application form
  public partial class frmMain : Form
  {
    //graphics
    Graphics graphics; //gdi+
    Bitmap backbuffer;
    Size buffersize;
    const float screenScale = 3.0f;
    Timer timer = new Timer();

    //keyboard controls
    bool leftHeld = false, rightHeld = false;
    bool upHeld = false, downHeld = false;

    //vehicle controls
    float steering = 0; //-1 is left, 0 is center, 1 is right
    float throttle = 0; //0 is coasting, 1 is full throttle
    float brakes = 0; //0 is no brakes, 1 is full brakes

    public frmMain()
    {
      InitializeComponent();
      Application.Idle += new EventHandler(ApplicationIdle);

      screen.Paint += new PaintEventHandler(screen_Paint);
      this.KeyDown += new KeyEventHandler(onKeyDown);
      this.KeyUp += new KeyEventHandler(onKeyUp);

      Init(screen.Size);
    }

    //intialize rendering
    private void Init(Size size)
    {
      //setup rendering device
      buffersize = size;
      backbuffer = new Bitmap(buffersize.Width, buffersize.Height);
      graphics = Graphics.FromImage(backbuffer);

      timer.GetETime(); //reset timer
    }

    //main rendering function
    private void Render(Graphics g)
    {
      //clear back buffer
      graphics.Clear(Color.Black);
      graphics.ResetTransform();
      graphics.ScaleTransform(screenScale, -screenScale);
      graphics.TranslateTransform(buffersize.Width / 2.0f /
          screenScale, -buffersize.Height / 2.0f / screenScale);

      //draw to back buffer
      DrawScreen();

      //present back buffer
      g.DrawImage(backbuffer, new Rectangle(0, 0, buffersize.Width,
       buffersize.Height), 0, 0, buffersize.Width, 
       buffersize.Height, GraphicsUnit.Pixel);
    }

    //draw the screen
    private void DrawScreen()
    {
      //draw our simulation here
    }

    //process game logic
    private void DoFrame()
    {
      //get elapsed time since last frame
      float etime = timer.GetETime();

      //process input
      ProcessInput();

      ////////////////////////////////
      //integrate our simulation here
      ////////////////////////////////

      //redraw our screen
      screen.Invalidate();
    }


    //process keyboard input
    private void ProcessInput()
    {
      if (leftHeld)
        steering = -1;
      else if (rightHeld)
        steering = 1;
      else
        steering = 0;

      if (upHeld)
        throttle = 1;
      else
        throttle = 0;

      if (downHeld)
        brakes = 1;
      else
        brakes = 0;
    }

    private void onKeyDown(object sender, KeyEventArgs e)
    {
      switch (e.KeyCode)
      {
        case Keys.Left:
          leftHeld = true;
          break;
        case Keys.Right:
          rightHeld = true;
          break;
        case Keys.Up:
          upHeld = true;
          break;
        case Keys.Down:
          downHeld = true;
          break;
        default: //no match found
          return; //return so handled dosnt get set
      }

      //match found
      e.Handled = true;
    }

    private void onKeyUp(object sender, KeyEventArgs e)
    {
      switch (e.KeyCode)
      {
        case Keys.Left:
          leftHeld = false;
          break;
        case Keys.Right:
          rightHeld = false;
          break;
        case Keys.Up:
          upHeld = false;
          break;
        case Keys.Down:
          downHeld = false;
          break;
        default: //no match found
          return; //return so handled dosnt get set
      }

      //match found
      e.Handled = true;
    }

    //rendering - only when screen is invalidated
    private void screen_Paint(object sender, PaintEventArgs e)
    {
      Render(e.Graphics);
    }

    //when the os gives us time, run the game
    private void ApplicationIdle(object sender, EventArgs e)
    {
      // While the application is still idle, run frame routine.
      DoFrame();
    }

    private void MenuExit_Click(object sender, EventArgs e)
    {
      this.Close();
    }
  }

  //keep track of time between frames
  class Timer
  {
    //store last time sample
    private int lastTime = Environment.TickCount;
    private float etime;

    //calculate and return elapsed time since last call
    public float GetETime()
    {
      etime = (Environment.TickCount - lastTime) / 1000.0f;
      lastTime = Environment.TickCount;

      return etime;
    }
  }
}




Phase Two

Contents
  Introduction
  Phase One
  Phase Two
  Phase Three
  Conclusion

  Source code
  Printable version
  Discuss this article