C++游戏编程教程——项目实战
Posted Visual Studio 2022
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++游戏编程教程——项目实战相关的知识,希望对你有一定的参考价值。
今天,我们来用所学知识做一个简易的飞机大战游戏。
游戏介绍
游戏功能
玩家驾驶飞机在窗口下方左右移动,按下空格发射子弹(0.3秒一个),而上方会有石块落下,打中飞机会死亡,玩家可以使用子弹攻击石块,如果打到了石块就消失,同时之后的石块下落会加速。屏幕上方还会有敌人的飞机出现,会随机发射子弹,还会随机移动,玩家碰到敌人发来的子弹会死亡,敌人碰到玩家的子弹也会消失。敌人有20个,随机出现,同一时刻屏幕上最多有5个敌人。玩家消灭所有的敌人就胜利了。
代码下载
游戏截图
游戏编写
项目框架
这个游戏主要的框架是这样的:
其中有三个类派生自Actor类,两个派生自DrawComponent类。
创建项目
首先,用我们的项目模板创建一个项目。
注意:在所有出现显示中文内容的文件中都应该加入一行#pragma execution_character_set("utf-8")
,否则是乱码。
Plane类
这个类是玩家控制的飞机类,功能主要有移动和发射子弹。
代码
Plane.h:
#pragma once
#include"Actor.h"
class Plane :
public Actor
{
public:
Plane(class Game* game, const Vector2& pos);
virtual void ActorInput(const uint8_t* keyState);
virtual void UpdateActor(float deltaTime);
private:
short mPlaneDir;
Uint32 mTick;
};
Plane.cpp:
#include "Plane.h"
#include "Bullet.h"
#include "DrawRectangleComponent.h"
Plane::Plane(Game* game, const Vector2& pos) :Actor(game), mPlaneDir(0)
{
SetPosition(pos);
mTick = SDL_GetTicks();
}
void Plane::ActorInput(const uint8_t* keyState)
{
mPlaneDir = 0;
if (keyState[SDL_SCANCODE_RIGHT])
mPlaneDir += 1;
if (keyState[SDL_SCANCODE_LEFT])
mPlaneDir -= 1;
if (keyState[SDL_SCANCODE_SPACE] && SDL_TICKS_PASSED(SDL_GetTicks(), mTick + 300))//0.3秒发射一颗子弹
{
mTick = SDL_GetTicks();
Vector2 pos = GetPosition();
pos.x += 20;
pos.y -= 40;
new DrawRectangleComponent(new Bullet(GetGame(), pos, -700), Vector2(10, 20), 255, 0, 0, 0);
}
}
void Plane::UpdateActor(float deltaTime)
{
Vector2 pos = GetPosition();
pos.x += mPlaneDir * 300 * deltaTime;
if (pos.x < 0)
pos.x = 0;
if (pos.x > 1024 - 50)
pos.x = 1024 - 50;
SetPosition(pos);
}
代码分析
成员变量
mTick:上次发射子弹的时间。用于控制时间间隔。
mPlaneDir:飞机移动方向。
构造函数
初始化变量。
ActorInput
重写的虚函数。首先设置移动方向,然后判断是否按下空格,如果按下就new一个子弹。
UpdateActor
更新位置。
Stone类
这个类是石头类。
代码
Stone.h:
#pragma once
#include"Actor.h"
class Stone
:public Actor
{
public:
Stone(Game* game, const Vector2& pos, float speed);
virtual void UpdateActor(float deltaTime);
private:
float mSpeed;
};
Stone.cpp:
#include "Stone.h"
#include"Bullet.h"
#include"Plane.h"
#include<typeinfo>
#pragma execution_character_set("utf-8")
Stone::Stone(Game* game, const Vector2& pos, float speed):Actor(game),mSpeed(speed)
{
SetPosition(pos);
}
void Stone::UpdateActor(float deltaTime)
{
Vector2 pos = GetPosition();
pos.y += deltaTime * mSpeed;
if (pos.y > 768)
SetState(EDead);
SetPosition(pos);
for (auto i : GetGame()->mActors)
{
if (typeid(*i) == typeid(Bullet))//运行时类型检查
{
Vector2 bPos = i->GetPosition();
if (bPos.x + 20 > pos.x && bPos.x < pos.x + 50 && bPos.y < pos.y + 50)
{
SetState(EDead);
i->SetState(EDead);
GetGame()->mStoneSpeed *= 1.02;
}
}
else if (typeid(*i) == typeid(Plane))
{
Vector2 bPos = i->GetPosition();
if (bPos.x + 50 > pos.x && bPos.x < pos.x + 50 && bPos.y < pos.y + 50 && bPos.y + 30>pos.y)
{
SetState(EDead);
i->SetState(EDead);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "游戏结束", "游戏结束,你输了!", GetGame()->mWindow);
GetGame()->mIsRunning = false;
}
}
}
}
代码分析
成员变量
mSpeed:速度。
构造函数
初始化变量。
UpdateActor
更新角色位置,并判断是否与飞机或子弹碰撞。
其实,这里的代码我写得非常非常非常非常非常非常非常非常非常非常(注:此处省略
1
0
1000000
10^{1000000}
101000000个非常)不规范。为什么呢?因为按照我们的代码规范来说,这里应该建立一个专门的碰撞检测组件,并把它加到Actor里,如果简单地写在UpdateActor里面,会使代码混乱,非常非常非常
(注:此处省略
1
0
1000000
10^{1000000}
101000000个非常)不便于阅读和后续添加代码。不过这里比较简单 (其实是我偷懒) ,就将就看吧。
Enemy类
这个类是敌方飞机类,它可以自动移动,并且随机发射子弹。其实这里也写得不太规范,其实我们大可不必建这个类,只需要建立Plane类,并不添加任何代码,然后建两个组件InputComponent和AutoMoveComponent,new对象的时候分别加上,这样可以提高代码复用率。在本例中,这个功能的好处并不明显,但设想一下,如果飞机除了用户控制和电脑控制之外,还有很多很多其它功能(比如为飞机添加弹夹和油量属性),这样专门写两个类就太麻烦了,不如使用组件。
代码
Enemy.h:
#pragma once
#include "Actor.h"
class Enemy :
public Actor
{
public:
Enemy(Game* game, const Vector2& pos);
virtual void UpdateActor(float deltatime);
private:
Uint32 mTicks;
Uint32 mMoveTicks;
short mMove;
};
Enemy.cpp:
#include "Enemy.h"
#include"Bullet.h"
#include"DrawRectangleComponent.h"
Enemy::Enemy(Game* game, const Vector2& pos) :Actor(game), mTicks(SDL_GetTicks()), mMoveTicks(SDL_GetTicks())
{
SetPosition(pos);
mMove = 200 + rand() % 100;
if (rand() % 2)
mMove = -mMove;
}
void Enemy::UpdateActor(float deltatime)
{
Vector2 pos = GetPosition();
if (SDL_TICKS_PASSED(SDL_GetTicks(), mMoveTicks + 1000))//随机移动位置
{
mMoveTicks = SDL_GetTicks();
mMove = 100 + rand() % 100;
if (rand() % 2)
mMove = -mMove;
}
pos.x += deltatime * mMove;
if (pos.x > 1024 - 50)
pos.x = 1024 - 50;
if (pos.x < 0)
pos.x = 0;
SetPosition(pos);
if (SDL_TICKS_PASSED(SDL_GetTicks(), mTicks + 1000)&&!(rand()%25))//1秒发射子弹
{
mTicks = SDL_GetTicks();
pos.x += 20;
pos.y += 40;
new DrawRectangleComponent(new Bullet(GetGame(), pos, 700), Vector2(10, 20), 255, 0, 0, 0);
}
}
代码分析
成员变量
mTicks:记录上一次射击的时间。
mMoveTicks:记录以这个速度移动的时间(因为要随机移动,所以需要频繁更新移动速度和方向)。
mMove:移动速度。
构造函数
初始化成员。
UpdateActor
先更新随机移动的速度,然后随机移动位置,最后发射子弹(1秒后,每帧有 1 25 \\frac{1}{25} 251几率发射子弹)。
Bullet类
这个类是子弹类。
代码
Bullet.h:
#pragma once
#include "Actor.h"
class Bullet :
public Actor
{
public:
Bullet(class Game* game, const Vector2& pos, float speed);
virtual void UpdateActor(float deltaTime);
private:
float mSpeed;
};
Bullet.cpp:
#include "Bullet.h"
#include"Plane.h"
#include"Enemy.h"
#include<typeinfo>
#pragma execution_character_set("utf-8")
Bullet::Bullet(Game* game, const Vector2& pos, float speed) :Actor(game), mSpeed(speed)
{
SetPosition(pos);
}
void Bullet::UpdateActor(float deltaTime)
{
Vector2 pos = GetPosition();
pos.y += mSpeed * deltaTime;
SetPosition(pos);
if (pos.y > 768 || pos.y < 0)
SetState(EDead);
for (auto i : GetGame()->mActors)
{
if (typeid(*i) == typeid(Enemy))//运行时类型检查
{
Vector2 bPos = i->GetPosition();
if (bPos.x - 10 < pos.x && bPos.x + 50 > pos.x && bPos.y + 50 > pos.y)
{
SetState(EDead);
i->SetState(EDead);
}
}
else if (typeid(*i) == typeid(Plane))
{
Vector2 bPos = i->GetPosition();
if (bPos.x - 10 < pos.x && bPos.x + 50 > pos.x && bPos.y < pos.y + 20 && bPos.y + 30 > pos.y)
{
SetState(EDead);
i->SetState(EDead);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "游戏结束", "游戏结束,你输了!", GetGame()->mWindow);
GetGame()->mIsRunning = false;
}
}
}
}
代码分析
Bullet类的代码和Stone类的代码基本相同,此处不再介绍。
Game类
最后,我们的任务是修改Game类。
首先,要在Game.cpp中包含所有自定义类的头文件:
#include "Game.h"
#include "SDL_image.h"
#include <algorithm>
#include "Actor.h"
#include"Plane.h"
#include"DrawPlaneComponent.h"
#include"DrawRectangleComponent.h"
#include"Stone.h"
#include"Enemy.h"
#include <ctime>
#include<typeinfo>
接着,在Game.h中加入:
float mStoneSpeed;//石头的速度
unsigned short mEnemyCount;//屏幕上敌人数量
unsigned short mAllEnemyCount;//剩余敌人数量
并在构造函数里初始化这几个变量。
然后,在LoadData函数里添加:
new DrawPlaneComponent(new Plane(this, Vector2(492, 700)));
new出飞机对象。
然后在UpdateGame里添加:
if (!(rand() % 100))
{
new DrawRectangleComponent(new Stone(this, Vector2(rand() % (1024 - 50), 0), mStoneSpeed + rand() % 10), Vector2(50, 50), 255, 255, 0, 0);
}
if (mEnemyCount < 5 && mAllEnemyCount)
{
new DrawPlaneComponent(new Enemy(this, Vector2(rand() % 984, 10)), true);
--mAllEnemyCount;
++mEnemyCount;
}
用来new出敌人和石头。
最后,在GenerateOutput函数里添加:
if (!mAllEnemyCount && !mEnemyCount)
{
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "游戏结束", "游戏结束,你赢了!", mWindow);
mIsRunning = false;
}
用来提示胜利。注:最好添加在SDL_RenderPresent后面,这样能显示最后一个敌人消失的场景,要不然提示结束的时候屏幕上还有一个敌人。
总结
到现在,整个的游戏就编写完毕了,效果也就是开头出示的那样。通过这个项目,我们真正体会到了面向对象的好处,以及将程序模块化的重要性。最后,祝大家编程顺利,代码无bug!
注:博主马上就要开学了,可能最近无法更新。
以上是关于C++游戏编程教程——项目实战的主要内容,如果未能解决你的问题,请参考以下文章
便宜出课!C++视频windows界面UI编程MFCGDI自绘项目实战网络编程教程我有原版