资讯专栏INFORMATION COLUMN

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

phpmatt / 1968人阅读

摘要:代码下载点击此处下载游戏截图游戏编写项目框架这个游戏主要的框架是这样的其中有三个类派生自类,两个派生自类。用于控制时间间隔。更新角色位置,并判断是否与飞机或子弹碰撞。构造函数初始化成员。

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

游戏介绍

游戏功能

玩家驾驶飞机在窗口下方左右移动,按下空格发射子弹(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#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#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 #include "Actor.h"#include"Plane.h"#include"DrawPlaneComponent.h"#include"DrawRectangleComponent.h"#include"Stone.h"#include"Enemy.h"#include #include

接着,在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!
注:博主马上就要开学了,可能最近无法更新。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/121856.html

相关文章

  • SegmentFault 技术周刊 Vol.40 - 2018,来学习一门新的编程语言吧!

    摘要:入门,第一个这是一门很新的语言,年前后正式公布,算起来是比较年轻的编程语言了,更重要的是它是面向程序员的函数式编程语言,它的代码运行在之上。它通过编辑类工具,带来了先进的编辑体验,增强了语言服务。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不觉已经到来了,总结过去的 2017,相信小伙们一定有很多收获...

    caspar 评论0 收藏0
  • SegmentFault 技术周刊 Vol.40 - 2018,来学习一门新的编程语言吧!

    摘要:入门,第一个这是一门很新的语言,年前后正式公布,算起来是比较年轻的编程语言了,更重要的是它是面向程序员的函数式编程语言,它的代码运行在之上。它通过编辑类工具,带来了先进的编辑体验,增强了语言服务。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不觉已经到来了,总结过去的 2017,相信小伙们一定有很多收获...

    nihao 评论0 收藏0
  • SegmentFault 技术周刊 Vol.40 - 2018,来学习一门新的编程语言吧!

    摘要:入门,第一个这是一门很新的语言,年前后正式公布,算起来是比较年轻的编程语言了,更重要的是它是面向程序员的函数式编程语言,它的代码运行在之上。它通过编辑类工具,带来了先进的编辑体验,增强了语言服务。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不觉已经到来了,总结过去的 2017,相信小伙们一定有很多收获...

    Drummor 评论0 收藏0
  • 系统地学习C++

    摘要:本书主要围绕一系列逐渐复杂的程序问题,以及用以解决这些问题的语言特性展开讲解。你不只学到的函数和结构,也会学习到它们的设计目的和基本原理。因此我们把精力集中在最有价值的地方。本书不仅是对模板的权威解释,而且本书还深入地介绍了其他一般的思想。 C++ 入门教程(41课时) - 阿里云大学 C+...

    joyqi 评论0 收藏0
  • 区块链开发中使用的最流行的编程语言

    摘要:我们目前正处于一个新兴的区块链开发行业中。,一种在以太坊开发人员中流行的新的简单编程语言,因为它是用于开发以太坊智能合约的语言。它是全球至少万开发人员使用的世界上最流行的编程语言之一。以太坊,主要是针对工程师使用进行区块链以太坊开发的详解。 我们目前正处于一个新兴的区块链开发行业中。区块链技术处于初期阶段,然而这种颠覆性技术已经成功地风靡全球,并且最近经历了一场与众不同的繁荣。由于许多...

    2shou 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<