2D Car Physics
Phase OnePhase 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.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 WiringUp until now, I havn’t explained how to connect all the functions. The first thing you’ll need to do is call the
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 TimerSince we don’t know how often our
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 OneSo 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;
}
}
}
|
|