Tutorial 93: Building a First-Person 3D Game
CNA — C++ XNA 4.0 reimplementation
FPS camera (mouse look + WASD)
class FPSCamera {
public:
Vector3 Position;
float Yaw = 0.0f; // horizontal rotation (radians)
float Pitch = 0.0f; // vertical rotation (radians, clamped)
void Update(const GameTime& gt) {
float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());
// Mouse look
auto ms = Mouse::GetState();
int cx = 400, cy = 300; // screen center
float dx = (float)(ms.X - cx) * 0.002f;
float dy = (float)(ms.Y - cy) * 0.002f;
Yaw += dx;
Pitch = std::clamp(Pitch + dy, -MathHelper::PiOver2 + 0.01f,
MathHelper::PiOver2 - 0.01f);
Mouse::SetPosition(cx, cy);
// WASD movement
Vector3 forward = Forward();
Vector3 right = Vector3::Cross(forward, Vector3::Up);
right.Normalize();
auto ks = Keyboard::GetState();
float speed = 5.0f * dt;
if (ks.IsKeyDown(Keys::W)) Position += forward * speed;
if (ks.IsKeyDown(Keys::S)) Position -= forward * speed;
if (ks.IsKeyDown(Keys::A)) Position -= right * speed;
if (ks.IsKeyDown(Keys::D)) Position += right * speed;
}
Vector3 Forward() const {
return Vector3(
std::cos(Pitch) * std::sin(Yaw),
-std::sin(Pitch),
std::cos(Pitch) * std::cos(Yaw));
}
Matrix ViewMatrix() const {
return Matrix::CreateLookAt(Position, Position + Forward(), Vector3::Up);
}
};
Raycasting for hit detection
struct HitResult { bool hit; Vector3 point; float distance; };
HitResult Raycast(const Vector3& origin, const Vector3& dir,
const std::vector<BoundingBox>& boxes) {
HitResult best { false, {}, std::numeric_limits<float>::max() };
for (auto& box : boxes) {
float near, far;
if (box.Intersects(Ray(origin, dir), near)) {
if (near < best.distance) {
best = { true, origin + dir * near, near };
}
}
}
return best;
}
Enemy AI (simple state machine)
enum class EnemyState { Idle, Chase, Attack, Dead };
class Enemy {
public:
Vector3 Position;
EnemyState State = EnemyState::Idle;
float Health = 100.0f;
float AlertRadius = 10.0f;
void Update(const GameTime& gt, const Vector3& playerPos) {
float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());
float dist = Vector3::Distance(Position, playerPos);
switch (State) {
case EnemyState::Idle:
if (dist < AlertRadius) State = EnemyState::Chase;
break;
case EnemyState::Chase: {
Vector3 dir = playerPos - Position;
dir.Normalize();
Position += dir * 3.0f * dt;
if (dist < 1.5f) State = EnemyState::Attack;
if (dist > AlertRadius * 2.0f) State = EnemyState::Idle;
break;
}
case EnemyState::Attack:
attackTimer_ -= dt;
if (attackTimer_ <= 0.0f) {
// Deal damage to player here
attackTimer_ = 1.0f;
}
if (dist > 2.0f) State = EnemyState::Chase;
break;
case EnemyState::Dead:
break;
}
}
void TakeDamage(float dmg) {
Health -= dmg;
if (Health <= 0.0f) State = EnemyState::Dead;
}
private:
float attackTimer_ = 1.0f;
};
Minimap on RenderTarget2D
// Create minimap render target
minimap_ = std::make_unique<RenderTarget2D>(
getGraphicsDeviceProperty(), 128, 128,
false, SurfaceFormat::Color, DepthFormat::None);
void DrawMinimap(GraphicsDevice& gd, SpriteBatch& sb) {
// Render to minimap RT
gd.SetRenderTarget(*minimap_);
gd.Clear(Color(20, 20, 20, 180));
sb.Begin();
// Draw rooms, corridors, enemies as colored dots
for (auto& e : enemies_) {
if (e.State == EnemyState::Dead) continue;
Vector2 mp = WorldToMinimap(e.Position);
sb.Draw(*pixel_, Rectangle((int)mp.X-2, (int)mp.Y-2, 4, 4), Color::Red);
}
Vector2 pp = WorldToMinimap(camera_.Position);
sb.Draw(*pixel_, Rectangle((int)pp.X-3, (int)pp.Y-3, 6, 6), Color::Yellow);
sb.End();
gd.SetRenderTarget(nullptr);
// Draw minimap RT onto screen HUD (top-right corner)
sb.Begin();
sb.Draw(*minimap_, Vector2(800-138, 10), nullptr,
Color::White, 0, Vector2::Zero, 1.0f, SpriteEffects::None, 0);
sb.End();
}
FPS camera class + raycasting hit test
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionColor.hpp"
#include "Microsoft/Xna/Framework/Graphics/RenderTarget2D.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Mouse.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/BoundingBox.hpp"
#include "Microsoft/Xna/Framework/Ray.hpp"
#include <vector>
#include <memory>
#include <cmath>
#include <algorithm>
#include <limits>
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;
class FPSGame final : public Game {
public:
FPSGame() : graphics_(this) {
graphics_.setPreferredBackBufferWidth(800);
graphics_.setPreferredBackBufferHeight(600);
}
protected:
void LoadContent() override {
effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
sb_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
pixel_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
Color w = Color::White;
pixel_->SetData(&w, 1);
minimap_ = std::make_unique<RenderTarget2D>(
getGraphicsDeviceProperty(), 128, 128,
false, SurfaceFormat::Color, DepthFormat::None);
// Floor quad
VertexPositionColor floor[] = {
{ Vector3(-10,0,-10), Color(80,80,80) },
{ Vector3( 10,0,-10), Color(80,80,80) },
{ Vector3(-10,0, 10), Color(80,80,80) },
{ Vector3( 10,0, 10), Color(80,80,80) },
};
floorVB_ = std::make_unique<VertexBuffer>(
getGraphicsDeviceProperty(),
VertexPositionColor::VertexDeclaration, 4, BufferUsage::None);
floorVB_->SetData(floor, 4);
camPos_ = Vector3(0, 1.7f, 0);
enemies_.push_back({ Vector3(3, 0.5f, -5) });
Mouse::SetVisible(false);
}
void Update(const GameTime& gt) override {
float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());
auto ms = Mouse::GetState();
float dx = (ms.X - 400) * 0.002f;
float dy = (ms.Y - 300) * 0.002f;
yaw_ += dx;
pitch_ = std::clamp(pitch_ + dy, -1.5f, 1.5f);
Mouse::SetPosition(400, 300);
Vector3 fwd(std::cos(pitch_)*std::sin(yaw_),
-std::sin(pitch_),
std::cos(pitch_)*std::cos(yaw_));
Vector3 right = Vector3::Cross(fwd, Vector3::Up);
right.Normalize();
auto ks = Keyboard::GetState();
float sp = 5.0f * dt;
if (ks.IsKeyDown(Keys::W)) camPos_ += fwd * sp;
if (ks.IsKeyDown(Keys::S)) camPos_ -= fwd * sp;
if (ks.IsKeyDown(Keys::A)) camPos_ -= right * sp;
if (ks.IsKeyDown(Keys::D)) camPos_ += right * sp;
if (ks.IsKeyDown(Keys::Escape)) Exit();
for (auto& e : enemies_) e.Update(gt, camPos_);
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color(30, 30, 50));
Vector3 fwd(std::cos(pitch_)*std::sin(yaw_),
-std::sin(pitch_),
std::cos(pitch_)*std::cos(yaw_));
Matrix view = Matrix::CreateLookAt(camPos_, camPos_+fwd, Vector3::Up);
Matrix proj = Matrix::CreatePerspectiveFieldOfView(
MathHelper::ToRadians(75.0f), 800.0f/600.0f, 0.1f, 100.0f);
effect_->setView(view);
effect_->setProjection(proj);
effect_->setVertexColorEnabled(true);
effect_->setWorld(Matrix::Identity);
gd.setVertexBuffer(*floorVB_);
for (auto& p : effect_->getCurrentTechnique().Passes) {
p.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleStrip, 0, 2);
}
gd.Present();
}
private:
struct SimpleEnemy {
Vector3 pos;
void Update(const GameTime& gt, const Vector3& pp) {
float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());
Vector3 d = pp - pos; d.Y = 0;
float len = std::sqrt(d.X*d.X+d.Z*d.Z);
if (len > 0) { d = d/len; pos += d*1.5f*dt; }
}
};
GraphicsDeviceManager graphics_;
std::unique_ptr<BasicEffect> effect_;
std::unique_ptr<SpriteBatch> sb_;
std::unique_ptr<Texture2D> pixel_;
std::unique_ptr<RenderTarget2D> minimap_;
std::unique_ptr<VertexBuffer> floorVB_;
std::vector<SimpleEnemy> enemies_;
Vector3 camPos_;
float yaw_ = 0, pitch_ = 0;
};
int main() { FPSGame game; game.Run(); }