1.0:Tank Game Tutorial
From DXFWiki
Create the DXTank project
- Run new-project.py
- short_name: tank
- long_name: DXTank
- Make sure it prints the following message, if it doesn't there was an error!
That was easy! You can now add your new project to the DXFramework solution. Right click the solution file (inside Visual Studio) and select 'Add -> Existing project...' Note: if you are using the free Visual C++ Express, you will need to remove tank.rc from the project! Don't delete the file, just remove it from within the IDE. See the dxframework.org FAQ for more information. Press enter to continue.
Add DXTank to the solution
- Verify the new tank folder exists.
- Open the solution (if it isn't opened already).
- Right-click "Solution 'dxf'" and select Add -> Existing project...
- Navigate in to the new tank folder and open tank.vcproj
- You may need to change the resolution if your res is equal to or less than 1024x768.
Changing resolution
- Do this by opening main.cpp in your project and change 1024 and 768 to 800 and 600.
Build everything
- At this point you may remove the other example projects (demo, dxpong, template, worldmap) to speed up future builds but usually it is nice having them around so you can cut and paste code from them.
- If you really know what you are doing, make a new configuration and don't build those projects in that new configuration!
- Build everything (control-shift-b)
- Make sure there were no errors (your output may vary, there should be no "failed" projects:
========== Build: 7 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
- Set tank as the StartUp project (right click the tank project -> Set as StartUp project)
- Press F5 to debug it, make sure it works, you should see a black window with a quit button.
- If it crashes here, see Changing_resolution.
- Press quit or escape or q to exit.
Determine state diagram
The next step is to figure out what states we need and to create the blank states. We could get away with only having one state, but lets use two to illustrate state changes.
- Title: The title state will be the "splash screen" and "menu", we could have options and such here.
- Play: The play state will be the state where the game is actually played.
Next, we need to define how we will transition between Title and Play. Again, very simple for this game:
- Title
- Play button -> Play state
- Quit button, ESC -> OS
- Play
- ESC -> Title state
- One tank dies -> Title state
Create the states and any easy transitions
The title state is already pretty much done, we just need to add a Play button. The Play button needs a state to change in to so we need to create that as well. Once in the Play state, we need to create a transition back to title, and since we have no tanks yet we'll just do the ESC key.
Create the Play state
We don't need a GUI for the Play state, so we'll use the provided blank.cpp/h as the starting point.
- Rename blank.cpp and blank.h to play.cpp and play.h
- Open play.h
- Replace "Blank" with "Play" (3 occurances)
- Open play.cpp
- Replace "Blank" with "Play" (11 occurances)
- Replace blank.h with play.h (include)
- Lets clear the screen with magenta so we know we're in the state. In Load(), change DXFSetClear to true and add:
dxf::DXFSetClearColor(MAGENTA);
- Build everything.
Register the Play state
- Open registrar.h
- Add a string to denote the Play state
static const wchar_t* kPlay;
- Open registrar.cpp
- Include play.h at the top
- Define that string
const wchar_t* Registrar::kPlay = L"Play";
- Register the state
dxf::DXFRegisterState(kPlay, Play::Instance());
- Build everything.
Add a play button to Title
- Open title.cpp
- Add an ID for the button
#define IDC_PLAY 2
- Add the button, placing it just below the other button
V_RETURN(dialog.AddButton(IDC_PLAY, L"Play", 20, 50, 100, 25, 'P'));
- Handle the event in OnGUIEvent
case IDC_PLAY: dxf::DXFChangeState(Registrar::kPlay); break;
- Build everything.
Handle the ESC key in Play
- Open play.cpp
- Since this is so popular, the code for it is almost written for you. Add the state change inside the if in MsgProc
if (uMsg == WM_KEYDOWN && wParam == VK_ESCAPE) {
dxf::DXFChangeState(Registrar::kTitle);
return true;
}
- Build everything.
Test the new states and transitions
- Start the program
- Click play
- The new magenta state should show up!
- Hit ESC
- The title screen should be back!
- Hit ESC or q or click Quit
Background and menu
Our tanks are going to drive around in mud, mud is brown, change the clear color to brown.
- Open title.cpp
- Change the clear color to D3DCOLOR_XRGB(66,33,00)
dxf::DXFSetClearColor(D3DCOLOR_XRGB(66,33,00));
- Make the same change in play.cpp
Having Quit above Play doesn't make much sense, and the buttons don't belong in the upper-left corner. Move the buttons to the center of the screen.
- Goal: Stick the play button in the middle horizontally, and place its top side in the middle vertically. Put Quit under it.
- Open title.cpp
- Get the back buffer surface description before adding the buttons:
const D3DSURFACE_DESC* d = DXUTGetBackBufferSurfaceDesc();
- Use the height and width from this to set the AddButton parameters
- The 50 comes from 1/2 of the widget's width (5th parameter), the 30 is a bit more than its height (6th parameter)
V_RETURN(dialog.AddButton(IDC_PLAY, L"Play", (d->Width / 2) - 50, (d->Height / 2), 100, 25, 'P')); V_RETURN(dialog.AddButton(IDC_QUIT, L"Quit", (d->Width / 2) - 50, (d->Height / 2) + 30, 100, 25, 'Q'));
- Build and run everything.
- Note, changing in to play will just look like the buttons dissappear.
DXTank logo
Next, lets add the DXTank logo:
- In Windows explorer, create a subfolder in media for the Tank project, the name DXTank is fine.
- Save Logo.png (the DXTank logo image) in to that folder.
- Open title.h, add a sprite member
dxf::Sprite logo;
- Open title.cpp, load the sprite in Load
- The L prefix is for wide characters
- The V macro is for error handling, use it with functions that return HRESULT. If the containing function also returns HRESULT, use V_RETURN
V(logo.CreateFromFile(L"DXTank/Logo.png"));
- Set its position since it shouldn't move (d is from the above steps).
- We'll center it on x and put it a few pixels south of 0
logo.SetPosition((d->Width / 2) - (logo.GetWidth() / 2), 50);
- Unload it in Unload
logo.Unload();
- Render it in Render
logo.Render2D();
- Build everything and run it
Add tanks to Play state
Create tank objects to represent the tanks:
- Save Tank.png in to the media/DXTank folder.
- Right click the project, add -> new item
- Add a cpp file, name it tank
- Right click the project, add -> new item
- Add a header file, name it tank
Tanks will have a sprite and other properties later, create a class for this.
- Open tank.h, start it:
#pragma once
#include "dxf.h"
class Tank {
public:
void Load(D3DXVECTOR2 centerPosition, D3DCOLOR color, float rotation);
void Render();
void Unload();
dxf::Sprite tank;
D3DXVECTOR2 position;
float rotation;
};
- Open tank.cpp, start it, load, set, and unload the sprite:
- The V macro needs the hr variable.
- We're going to center the tank on the passed position
#include "tank.h"
void Tank::Load(D3DXVECTOR2 centerPosition, D3DCOLOR color, float rotation) {
HRESULT hr;
V(tank.CreateFromFile(L"DXTank/Tank.png"));
position = centerPosition - tank.GetAbsoluteCenter();
tank.SetPosition(centerPosition - tank.GetAbsoluteCenter());
tank.SetColor(color);
this->rotation = rotation;
tank.SetRotation(rotation);
tank.SetRotationCenterAsCenter();
}
void Tank::Render() {
tank.Render2D();
}
void Tank::Unload() {
tank.Unload();
}
Next, lets add the tanks to the Play state:
- Open play.h
- Add tank.h include
- Add tank members
- We'll do the standard red vs blue, we can pass the sprite through a color filter to color it
Tank red; Tank blue;
- Open play.cpp
- Load the tanks, we'll arbitrarily place them 100 from each corner.
- Cast to avoid warning
- Rotate blue 180 so its pointing up
const D3DSURFACE_DESC* d = DXUTGetBackBufferSurfaceDesc(); red.Load(D3DXVECTOR2(100,100), RED, 0); blue.Load(D3DXVECTOR2(static_cast<float>(d->Width) - 100, static_cast<float>(d->Height) - 100), BLUE, D3DX_PI);
- Unload the tanks in Unload
red.Unload(); blue.Unload();
- Render the tanks in Render2D
red.Render(); blue.Render();
- Build and run
Driving around
Lets make a tank move in response to the arrow keys.
- Open tank.h
- Add update method
void Update(float elapsed, bool forward, bool left, bool right, bool reverse);
- Add a few key properties
- velocity is basically an arrow, what direction to travel
- the static speed variables are in pixels per second, could be const but maybe we want to tweak them at runtime
D3DXVECTOR2 velocity; static float forwardSpeed; static float reverseSpeed;
- Open tank.cpp
- Define the speeds
float Tank::forwardSpeed = 75; float Tank::reverseSpeed = -25;
- Define update logic
void Tank::Update(float elapsed, bool forward, bool left, bool right, bool reverse) {
if (left) {
rotation += elapsed;
}
if (right) {
rotation -= elapsed;
}
- The hidden constant in the rotation here is 1, we could multiply elapsed by some other constant to increase/decrease the rotation speed.
if (left || right) {
tank.SetRotation(rotation);
velocity = D3DXVECTOR2(sin(rotation), cos(rotation));
}
- If we're rotating, our velocity vector is going to change
float speed = 0;
if (forward) {
speed = forwardSpeed;
}
if (reverse) {
speed = reverseSpeed;
}
- Determine the speed to use depending on input
if (forward || reverse) {
position += (velocity * elapsed * speed);
tank.SetPosition(position);
}
}
- We need to make sure velocity is set to the initial rotation, so add this to the Load function:
velocity = D3DXVECTOR2(sin(rotation), cos(rotation));
- Next, open up play.cpp and call the tank's update function, test buttons for input
red.Update(fElapsedTime,
dxf::DXFCheckKeyboard('W') == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard('A') == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard('D') == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard('S') == dxf::BUTTON_DOWN);
blue.Update(fElapsedTime,
dxf::DXFCheckKeyboard(VK_UP) == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_LEFT) == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_RIGHT) == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_DOWN) == dxf::BUTTON_DOWN);
- Build and test. You should be able to drive the tanks around.
Arm the tank
Next, we'll make the tanks shoot stuff:
To make things really easy for now, lets say each tank can fire only one shot until the previous shot is not on the screen anymore.
- Save Bullet.png in to the media/DXTank folder.
- Because of our semantics, we can make the bullet part of the tank object.
- Open tank.h
- add a bullet sprite member, position, velocity, speed, and a convenient "active" bool
dxf::Sprite bullet; D3DXVECTOR2 bulletPosition; D3DXVECTOR2 bulletVelocity; bool bulletActive; static float bulletSpeed;
- add a fire parameter to update:
void Update(float elapsed, bool forward, bool left, bool right, bool reverse, bool fire);
- Open up tank.cpp
- Define bullet speed
float Tank::bulletSpeed = 200;
- Load the sprite in the load function
V(bullet.CreateFromFile(L"DXTank/Bullet.png"));
- Set active to false in the load function
bulletActive = false;
- Unload in unload
bullet.Unload();
- add bullet logic to the update function
- (add the fire param here too)
- if the bullet is active, update its position, check to see if it is off the screen and then deactivate if so
- if not active, then check for fire input and fire it if necessary
void Tank::Update(float elapsed, bool forward, bool left, bool right, bool reverse, bool fire) {
if (bulletActive) {
bulletPosition += (elapsed * bulletVelocity);
const D3DSURFACE_DESC* d = DXUTGetBackBufferSurfaceDesc();
if (bulletPosition.x < -static_cast<int>(bullet.GetWidth())
|| bulletPosition.x > d->Width
|| bulletPosition.y < -static_cast<int>(bullet.GetHeight())
|| bulletPosition.y > d->Height)
{
bulletActive = false;
}
} else {
if (fire) {
bulletActive = true;
bulletPosition = position + tank.GetAbsoluteCenter() - bullet.GetAbsoluteCenter();
bulletVelocity = D3DXVECTOR2(sin(rotation), cos(rotation));
bulletVelocity *= bulletSpeed;
bulletVelocity += velocity;
}
}
- Draw the bullet if it is active, in render
- Make sure to draw the bullet under (before) the tank!
if (bulletActive) {
bullet.Render2D(bulletPosition);
}
- Next, open up play.cpp and add fire input to update calls
- We'll use BUTTON_PRESSED here
red.Update(fElapsedTime,
dxf::DXFCheckKeyboard('W') == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard('A') == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard('D') == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard('S') == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_SPACE) == dxf::BUTTON_PRESSED);
blue.Update(fElapsedTime,
dxf::DXFCheckKeyboard(VK_UP) == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_LEFT) == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_RIGHT) == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_DOWN) == dxf::BUTTON_DOWN,
dxf::DXFCheckKeyboard(VK_RCONTROL) == dxf::BUTTON_PRESSED);
- Build and test. You should be able to fire wimpy projectiles around.
Destroy the tank
Next, we need to detect when the bullet hits the tank and destroy the tank when it happens.
We want to make this check in Update after the tanks have been updated. We'll make another method on tank and pass the other tank. Because everything is public we can just check if the center of the bullet is on the tank sprite. Not perfect or very OO, but better collision detection is left as an excercise for the reader :)
- Open tank.h
- Create collision detection function. This takes a pointer to a tank and checks to see if our bullet hits it.
bool Hits(Tank* other);
- Create Die function to kill tank, add boolean to keep track of death
void Die(); bool dead;
- Open tank.cpp, define the function
- I know this conversion from D3DXVECTOR2 to POINT is clunky, this is on the todo list
bool Tank::Hits(Tank* other) {
D3DXVECTOR2 myCenter = bulletPosition + bullet.GetAbsoluteCenter();
POINT p;
p.x = static_cast<int>(myCenter.x);
p.y = static_cast<int>(myCenter.y);
return other->tank.CheckIntersection(p);
}
- Make not dead in Load
dead = false;
- Define Die
void Tank::Die() {
dead = true;
}
- Open play.cpp, add checks to update after tank's updated
if (red.Hits(&blue)) {
blue.Die();
}
if (blue.Hits(&red)) {
red.Die();
}
Now, the tanks know they are dead but we don't. Lets make the tanks explode when they die:
- Save Boom.png in to the media/DXTank folder.
- Open tank.h and add boom sprite
dxf::Sprite boom;
- Open tank.cpp
- Load the sprite in load
V(boom.CreateFromFile(L"DXTank/Boom.png"));
- Unload it in Unload
boom.Unload();
- If the tank is dead, draw boom instead
void Tank::Render() {
if (bulletActive) {
bullet.Render2D(bulletPosition);
}
if (!dead) {
tank.Render2D();
} else {
boom.Render2D(position);
}
}
- Finally, don't update anything except the bullet if the tank is dead
if ((left || right) && !dead) {
if ((forward || reverse) && !dead) {
if (bulletActive) {
...
} else if (!dead) {
- Build and test!
