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自绘项目实战网络编程教程我有原版

项目实战C++多文件写法轻松实现练手小游戏:贪吃蛇

多人在线游戏架构实战-基于C++的分布式游戏编程开篇

便宜出课!2019年零基础C语言C++视频教程QT编程MFCLinux数据库项目实战我有原版

C++游戏编程教程

实战案例|摇奖游戏的开发教程