2

C++游戏编程教程(五)——项目实战

 2 years ago
source link: https://blog.csdn.net/qq_54121864/article/details/120624039
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

今天,我们来用所学知识做一个简易的飞机大战游戏。

玩家驾驶飞机在窗口下方左右移动,按下空格发射子弹(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!
注:博主马上就要开学了,可能最近无法更新。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK