這一篇中勿侯,我們繼續(xù)繼續(xù)進行我們的坦克大戰(zhàn)蛉腌。
位置信息數(shù)據(jù)結構
在游戲設計過程中官份,需要記錄大量的位置信息,如果僅僅使用(x烙丛,y)坐標很容易出錯舅巷。這一篇中,我們先定義兩個簡單的數(shù)據(jù)結構用來保存點和矩形的信息河咽。
在項目中新建Model目錄悄谐,創(chuàng)建下面四個文件:
代碼如下:
Point.h
#ifndef __POINT_H__
#define __POINT_H__
class Point
{
public:
Point(int x = 0, int y = 0) : m_x(x), m_y(y){};
~Point(){};
Point& operator=(const Point &p)
{
m_x = p.m_x;
m_y = p.m_y;
return *this;
}
void Set(int x, int y);
void SetX(int x);
void SetY(int y);
int GetX();
int GetY();
private:
int m_x;
int m_y;
};
#endif
這個頭文件創(chuàng)建了一個Point類,有兩個成員變量m_x库北,m_y用來記錄一個點的橫、縱坐標们陆。一組public方法用來完成給對象賦值和讀取坐標值的操作寒瓦。
這里我們用到了C++的運算符重載功能,將“=”功能進行重載坪仇,方便我們用一個Point對象給另一個Point對象賦值杂腰,同時也能夠使我們將Point作為參數(shù)進行傳遞。
Point.cpp
#include "Point.h"
void Point::Set(int x, int y)
{
m_x = x;
m_y = y;
}
void Point::SetX(int x)
{
m_x = x;
}
void Point::SetY(int y)
{
m_y = y;
}
int Point::GetX()
{
return m_x;
}
int Point::GetY()
{
return m_y;
}
這個文件中是對Point類的實現(xiàn)椅文,大家一看就明白喂很。
這里需要強調的是惜颇,在類的封裝過程中有一個非常重要的原則是不允許將成員變量用public的方法暴露在外。如果類的外部代碼能夠直接對類成員變量進行修改的話少辣,程序將很不安全凌摄。正確的方法是像我們這樣實現(xiàn)一組Get和Set方法進行管理。這樣雖然代碼量多了一些漓帅,但對后期維護帶來的幫助是不可估量的锨亏。
Rect.h
#ifndef __RECTANGLE_H__
#define __RECTANGLE_H__
#include "Point.h"
class Rect
{
public:
Rect(int x1 = 0, int y1 = 0, int x2 = 0, int y2 = 0) : m_startPoint(x1, y1), m_endPoint(x2, y2){};
Rect(const Point p1, const Point p2) : m_startPoint(p1), m_endPoint(p2){};
~Rect(){};
Rect& operator=(const Rect &rect)
{
m_startPoint = rect.GetStartPoint();
m_endPoint = rect.GetEndPoint();
return *this;
}
void Set(const Point pStart, const Point pEnd);
void Set(int x1, int y1, int x2, int y2);
void SetStartPoint(const Point p);
void SetEndPoint(const Point p);
Point GetStartPoint() const;
Point GetEndPoint() const;
int GetWidth();
int GetHeight();
private:
void Check();
Point m_startPoint;
Point m_endPoint;
};
#endif
Rect類是用來定義矩形的,它的成員變量是兩個Point對象忙干,分別表示矩形的左上角和右下角器予。這里我們強行規(guī)定m_startPoint表示左上角,m_endPoint表示右下角捐迫。如果創(chuàng)建對象時兩個點順序反了乾翔,Check()函數(shù)會自動把它們調整過來。
這里需要注意施戴,GetStartPoint()和GetEndPoint()兩個函數(shù)都通過const修飾反浓,表示返回值不能被修改。為什么要這么實現(xiàn)呢暇韧,因為這個函數(shù)的結果將會傳進EasyX接口中勾习,而這些接口大部分都要求參數(shù)是const的,如果這里不做修飾懈玻,在傳參時會報錯巧婶。
Rect.cpp
#include "Rect.h"
void Rect::Set(Point pStart, Point pEnd)
{
m_startPoint = pStart;
m_endPoint = pEnd;
}
void Rect::Set(int x1, int y1, int x2, int y2)
{
m_startPoint.Set(x1, y1);
m_endPoint.Set(x2, y2);
}
void Rect::SetStartPoint(Point p)
{
m_startPoint = p;
}
void Rect::SetEndPoint(Point p)
{
m_endPoint = p;
}
Point Rect::GetStartPoint() const
{
return m_startPoint;
}
Point Rect::GetEndPoint() const
{
return m_endPoint;
}
int Rect::GetWidth()
{
return m_endPoint.GetX() - m_startPoint.GetX();
}
int Rect::GetHeight()
{
return m_endPoint.GetY() - m_startPoint.GetY();
}
void Rect::Check()
{
if (m_startPoint.GetX() > m_endPoint.GetX() || m_startPoint.GetY() > m_endPoint.GetY())
{
Point p = m_startPoint;
m_startPoint = m_endPoint;
m_endPoint = p;
}
}
這個文件中實現(xiàn)了Rect類的成員函數(shù)。
主戰(zhàn)坦克升級
Tank.h
首先涂乌,我們對Tank類進行修改艺栈,新增一部分功能,代碼如下:
#ifndef __TANK_H__
#define __TANK_H__
#include "Graphic.h"
enum Dir { UP, DOWN, LEFT, RIGHT };
class Tank
{
public:
// 繪圖
virtual void Display() = 0;
// 移動
virtual void Move() = 0;
protected:
virtual void CalculateSphere() = 0;
Point m_pos;
Rect m_rectSphere; // 勢力范圍
COLORREF m_color;
Dir m_dir;
int m_step;
};
#endif
我們把坐標用Point對象m_pos表示湾盒,又添加了一個新屬性m_rectSphere湿右,它是一個Rect對象,用來記錄坦克的形狀范圍罚勾。之前我們的坦克總是用一組坐標來表示毅人,這個坐標是坦克的中心點,所有跟坦克相關的行為都通過這個點來計算位置尖殃,實現(xiàn)起來有些復雜丈莺,有了這個Rect對象,相當于我們記錄了這個坦克所在的矩形的位置送丰,這樣在繪制坦克時更容易計算坐標缔俄。
MainTank.h
#ifndef __MAIN_TANK__
#define __MAIN_TANK__
#include "Tank.h"
class MainTank : public Tank
{
public:
MainTank()
{
m_pos.Set(300, 300);
this->CalculateSphere();
m_color = YELLOW;
m_dir = Dir::UP;
m_step = 2;
}
~MainTank(){}
// 設置行駛方向
void SetDir(Dir dir);
void Display();
void Move();
protected:
void CalculateSphere();
// 繪制坦克主體
void DrawTankBody();
};
#endif
這個文件中沒有太大的修改,只是在成員變量初始化時做了一些調整。主戰(zhàn)坦克的顏色改成了黃色俐载,初始化后調用CalculateSphere()函數(shù)計算出矩形位置蟹略。
MainTank.cpp
#include "MainTank.h"
void MainTank::SetDir(Dir dir)
{
m_dir = dir;
}
void MainTank::DrawTankBody()
{
fillrectangle(m_pos.GetX() - 6, m_pos.GetY() - 6, m_pos.GetX() + 6, m_pos.GetY() + 6);
switch (m_dir)
{
case UP:
case DOWN:
fillrectangle(m_rectSphere.GetStartPoint().GetX(), m_rectSphere.GetStartPoint().GetY(),
m_rectSphere.GetStartPoint().GetX() + 4, m_rectSphere.GetEndPoint().GetY());
fillrectangle(m_rectSphere.GetEndPoint().GetX() - 4, m_rectSphere.GetStartPoint().GetY(),
m_rectSphere.GetEndPoint().GetX(), m_rectSphere.GetEndPoint().GetY());
break;
case LEFT:
case RIGHT:
fillrectangle(m_rectSphere.GetStartPoint().GetX(), m_rectSphere.GetStartPoint().GetY(),
m_rectSphere.GetEndPoint().GetX(), m_rectSphere.GetStartPoint().GetY() + 4);
fillrectangle(m_rectSphere.GetStartPoint().GetX(), m_rectSphere.GetEndPoint().GetY() - 4,
m_rectSphere.GetEndPoint().GetX(), m_rectSphere.GetEndPoint().GetY());
break;
default:
break;
}
}
void MainTank::Display()
{
COLORREF fill_color_save = getfillcolor();
COLORREF color_save = getcolor();
setfillcolor(m_color);
setcolor(m_color);
DrawTankBody();
switch (m_dir)
{
case UP:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX(), m_pos.GetY() - 15);
break;
case DOWN:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX(), m_pos.GetY() + 15);
break;
case LEFT:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX() - 15, m_pos.GetY());
break;
case RIGHT:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX() + 15, m_pos.GetY());
break;
default:
break;
}
setcolor(color_save);
setfillcolor(fill_color_save);
}
void MainTank::Move()
{
switch (m_dir)
{
case UP:
m_pos.SetY(m_pos.GetY() - m_step);
if (m_pos.GetY() < Graphic::GetBattleGround().GetStartPoint().GetY())
m_pos.SetY(Graphic::GetBattleGround().GetEndPoint().GetY() - 1);
break;
case DOWN:
m_pos.SetY(m_pos.GetY() + m_step);
if (m_pos.GetY() > Graphic::GetBattleGround().GetEndPoint().GetY())
m_pos.SetY(Graphic::GetBattleGround().GetStartPoint().GetY() + 1);
break;
case LEFT:
m_pos.SetX(m_pos.GetX() - m_step);
if (m_pos.GetX() < Graphic::GetBattleGround().GetStartPoint().GetX())
m_pos.SetX(Graphic::GetBattleGround().GetEndPoint().GetX() - 1);
break;
case RIGHT:
m_pos.SetX(m_pos.GetX() + m_step);
if (m_pos.GetX() > Graphic::GetBattleGround().GetEndPoint().GetX())
m_pos.SetX(Graphic::GetBattleGround().GetStartPoint().GetX() + 1);
break;
default:
break;
}
CalculateSphere();
}
void MainTank::CalculateSphere()
{
switch (m_dir)
{
case UP:
case DOWN:
m_rectSphere.Set(m_pos.GetX() - 13, m_pos.GetY() - 10, m_pos.GetX() + 13, m_pos.GetY() + 10);
break;
case LEFT:
case RIGHT:
m_rectSphere.Set(m_pos.GetX() - 10, m_pos.GetY() - 13, m_pos.GetX() + 10, m_pos.GetY() + 13);
break;
default:
break;
}
}
這個文件修改較多,是不是有些眼花繚亂了遏佣。
- DrawTankBody()
這個函數(shù)的參數(shù)被拿掉了挖炬,在這里我們通過坦克當前方向來判斷它的形狀。
在繪制履帶時贼急,我們利用了m_rectSphere的位置坐標茅茂,雖然看起來代碼變多了,但只有一個數(shù)字4是無意義的太抓,它代表履帶的寬度空闲。如果這個寬度需要經常調整的話,我們還可以考慮把它用一個成員變量管理起來走敌。
在判斷坦克形狀時碴倾,我們利用了switch的一個特性,通過故意少寫break關鍵字掉丽,讓兩個判斷結果公用一段代碼跌榔,這個早已經講過,這里不多說了捶障。
- Display()
之前我們用setfillcolor設置了填充顏色僧须,這里我們加入了setcolor,這樣畫出來的坦克邊框也是我們設置的顏色项炼。
- Move()
這個函數(shù)中担平,比較奇怪的是出現(xiàn)了一個沒見過的函數(shù)Graphic::GetBattleGround()。我們今天要給坦克劃定一個運行區(qū)域锭部,不能讓它滿屏幕行駛了暂论,這個后面再說。
這里要注意拌禾,每移動一次都需要調用CalculateSphere()方法重新計算坦克區(qū)域取胎。
- CalculateSphere()
這個很簡單,計算出左上角和右下角的Point位置即可湃窍。
更新畫布
之前的畫布顏色太深闻蛀,我們要做修改。另外您市,我們后面需要在窗口上顯示游戲信息循榆,因此,要在右邊留出一部分空間墨坚。我們給坦克劃定一個新的區(qū)域,讓它們在里面行駛。
Graphic.h
#ifndef __GRAPHIC_H__
#define __GRAPHIC_H__
#include <graphics.h>
#include "model/Rect.h"
#define SCREEN_WIDTH 1024
#define SCREEN_HEIGHT 768
#define BATTLE_GROUND_X1 5
#define BATTLE_GROUND_Y1 5
#define BATTLE_GROUND_X2 800
#define BATTLE_GROUND_Y2 (SCREEN_HEIGHT - BATTLE_GROUND_Y1)
class Graphic
{
public:
static void Create();
static void Destroy();
static void DrawBattleGround();
static int GetScreenWidth();
static int GetScreenHeight();
static Rect GetBattleGround();
private:
static Rect m_rectScreen;
static Rect m_rectBattleGround;
};
#endif
文件中通過一組宏來定義戰(zhàn)場區(qū)域的位置泽篮。另外通過m_rectBattleGround這個Rect對象來保存盗尸。
細心的讀者應該發(fā)現(xiàn)了,我們在引用Rect類時用了下面這句話:
#include "model/Rect.h"
Rect.h文件的路徑需要加上model目錄帽撑,否則找不到泼各。需要說明的是這個目錄指的是項目文件夾下真實存在的model目錄。如果你用的是VS亏拉,"Solution Explorer"中新建的文件夾是邏輯目錄扣蜻,不需要加載include路徑中。
簡單說及塘,include后面寫的路徑是給編譯器看的莽使,它只認Windows資源管理器中看到的路徑,與IDE中的邏輯路徑無關笙僚。
Graphic.cpp
#include "Graphic.h"
Rect Graphic::m_rectScreen;
Rect Graphic::m_rectBattleGround;
void Graphic::Create()
{
m_rectScreen.Set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
initgraph(SCREEN_WIDTH, SCREEN_WIDTH);
setbkcolor(DARKGRAY);
m_rectBattleGround.Set(BATTLE_GROUND_X1, BATTLE_GROUND_Y1, BATTLE_GROUND_X2, BATTLE_GROUND_Y2);
}
void Graphic::Destroy()
{
closegraph();
}
void Graphic::DrawBattleGround()
{
rectangle(m_rectBattleGround.GetStartPoint().GetX(), m_rectBattleGround.GetStartPoint().GetY(),
m_rectBattleGround.GetEndPoint().GetX(), m_rectBattleGround.GetEndPoint().GetY());
}
int Graphic::GetScreenWidth()
{
return SCREEN_WIDTH;
}
int Graphic::GetScreenHeight()
{
return SCREEN_HEIGHT;
}
Rect Graphic::GetBattleGround()
{
return m_rectBattleGround;
}
代碼在創(chuàng)建畫布是芳肌,重新指定了背景顏色。DrawBattleGround()函數(shù)在屏幕上畫出了戰(zhàn)場的范圍肋层。
main.cpp
主函數(shù)中亿笤,我們只需要再循環(huán)中添加一個DrawBattleGround函數(shù)的調用即可。
if (!skip)
{
cleardevice();
Graphic::DrawBattleGround();
mainTank.Move();
mainTank.Display();
}
好了栋猖,運行一下程序净薛,看看效果吧。
敵人坦克
屏幕上只有一個自己的坦克看著有些孤單蒲拉,我們再添加上些敵人的坦克肃拜。
新建文件EnemyTank.h和EnemyTank.cpp。實現(xiàn)一個敵人坦克類全陨。
EnemyTank.h
#ifndef __ENEMY_TANK__
#define __ENEMY_TANK__
#include "Tank.h"
class EnemyTank : public Tank
{
public:
EnemyTank()
{
RandomTank();
}
~EnemyTank(){}
void Display();
void Move();
protected:
void CalculateSphere();
void RandomTank();
};
#endif
有了Tank這個抽象類爆班,所有的坦克都從它繼承就好了。除了抽象類中繼承的函數(shù)之外辱姨,我們加了一個RandomTank()用來隨機地在戰(zhàn)場區(qū)域生成一個坦克柿菩。
EnemyTank.cpp
#include "EnemyTank.h"
void EnemyTank::RandomTank()
{
m_pos.SetX(rand() % Graphic::GetBattleGround().GetWidth());
m_pos.SetY(rand() % Graphic::GetBattleGround().GetHeight());
m_color = WHITE;
m_dir = (Dir)(Dir::UP + (rand() % 4));
m_step = 2;
}
void EnemyTank::Display()
{
COLORREF fill_color_save = getfillcolor();
COLORREF color_save = getcolor();
setfillcolor(m_color);
setcolor(m_color);
fillrectangle(m_pos.GetX() - 6, m_pos.GetY() - 6, m_pos.GetX() + 6, m_pos.GetY() + 6);
fillrectangle(m_rectSphere.GetStartPoint().GetX(), m_rectSphere.GetStartPoint().GetY(),
m_rectSphere.GetStartPoint().GetX() + 4, m_rectSphere.GetStartPoint().GetY() + 4);
fillrectangle(m_rectSphere.GetEndPoint().GetX() - 4, m_rectSphere.GetStartPoint().GetY(),
m_rectSphere.GetEndPoint().GetX(), m_rectSphere.GetStartPoint().GetY() + 4);
fillrectangle(m_rectSphere.GetStartPoint().GetX(), m_rectSphere.GetEndPoint().GetY() - 4,
m_rectSphere.GetStartPoint().GetX() + 4, m_rectSphere.GetEndPoint().GetY());
fillrectangle(m_rectSphere.GetEndPoint().GetX() - 4, m_rectSphere.GetEndPoint().GetY() - 4,
m_rectSphere.GetEndPoint().GetX(), m_rectSphere.GetEndPoint().GetY());
switch (m_dir)
{
case UP:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX(), m_pos.GetY() - 15);
break;
case DOWN:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX(), m_pos.GetY() + 15);
break;
case LEFT:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX() - 15, m_pos.GetY());
break;
case RIGHT:
line(m_pos.GetX(), m_pos.GetY(), m_pos.GetX() + 15, m_pos.GetY());
break;
default:
break;
}
setcolor(color_save);
setfillcolor(fill_color_save);
}
void EnemyTank::Move()
{
switch (m_dir)
{
case UP:
m_pos.SetY(m_pos.GetY() - m_step);
if (m_pos.GetY() < Graphic::GetBattleGround().GetStartPoint().GetY())
m_pos.SetY(Graphic::GetBattleGround().GetEndPoint().GetY() - 1);
break;
case DOWN:
m_pos.SetY(m_pos.GetY() + m_step);
if (m_pos.GetY() > Graphic::GetBattleGround().GetEndPoint().GetY())
m_pos.SetY(Graphic::GetBattleGround().GetStartPoint().GetY() + 1);
break;
case LEFT:
m_pos.SetX(m_pos.GetX() - m_step);
if (m_pos.GetX() < Graphic::GetBattleGround().GetStartPoint().GetX())
m_pos.SetX(Graphic::GetBattleGround().GetEndPoint().GetX() - 1);
break;
case RIGHT:
m_pos.SetX(m_pos.GetX() + m_step);
if (m_pos.GetX() > Graphic::GetBattleGround().GetEndPoint().GetX())
m_pos.SetX(Graphic::GetBattleGround().GetStartPoint().GetX() + 1);
break;
default:
break;
}
CalculateSphere();
}
void EnemyTank::CalculateSphere()
{
switch (m_dir)
{
case UP:
case DOWN:
m_rectSphere.Set(m_pos.GetX() - 13, m_pos.GetY() - 10, m_pos.GetX() + 13, m_pos.GetY() + 10);
break;
case LEFT:
case RIGHT:
m_rectSphere.Set(m_pos.GetX() - 10, m_pos.GetY() - 13, m_pos.GetX() + 10, m_pos.GetY() + 13);
break;
default:
break;
}
}
這個文件實在沒什么可講的,基本都用的之前提到的方法雨涛。隨機生成坦克用到了星空中隨機產生星星的方法枢舶,相信大家都能看懂。
main.cpp
最后是main函數(shù)替久,代碼如下:
#define MAX_TANKS 10
void main()
{
srand((unsigned)time(NULL));
Graphic::Create();
MainTank mainTank;
Tank* pTank[MAX_TANKS];
for (int i = 0; i < MAX_TANKS; i++)
{
pTank[i] = new EnemyTank();
}
bool loop = true;
bool skip = false;
while (loop)
{
if (kbhit())
{
int key = getch();
switch (key)
{
// Up
case 72:
mainTank.SetDir(Dir::UP);
break;
// Down
case 80:
mainTank.SetDir(Dir::DOWN);
break;
// Left
case 75:
mainTank.SetDir(Dir::LEFT);
break;
// Right
case 77:
mainTank.SetDir(Dir::RIGHT);
break;
case 224: // 方向鍵高8位
break;
// Esc
case 27:
loop = false;
break;
// Space
case 32:
break;
// Enter
case 13:
if (skip)
skip = false;
else
skip = true;
break;
default:
break;
}
}
if (!skip)
{
cleardevice();
Graphic::DrawBattleGround();
mainTank.Move();
mainTank.Display();
for (int i = 0; i < MAX_TANKS; i++)
{
pTank[i]->Move();
pTank[i]->Display();
}
}
Sleep(200);
}
for (int i = 0; i < MAX_TANKS; i++)
{
delete pTank[i];
}
Graphic::Destroy();
}
與之前相比凉泄,添加了下面這幾個內容:
- 新建了一個宏MAX_TANKS用來設置坦克的數(shù)量
- 用一個指針數(shù)組保存每個坦克的指針
- 循環(huán)創(chuàng)建坦克,這里用了new的方法把坦克對象創(chuàng)建在堆空間中
- 每次擦屏之后蚯根,遍歷指針數(shù)組后众,繪制出每個坦克。調用的是Move()和Display()方法。
- 退出程序前蒂誉,釋放每一個坦克所占的堆空間教藻。
注意:C++中依然是new和delete成對出現(xiàn),有申請就要有釋放右锨。否則會出現(xiàn)內存泄露括堤。
下面運行一下代碼,看看我們今天的成果绍移。
是不是一下熱鬧了很多呢悄窃?
這一篇就到這里,源碼請到GitHub上下載蹂窖。
我是天花板轧抗,讓我們一起在軟件開發(fā)中自我迭代。
如有任何問題恼策,歡迎與我聯(lián)系鸦致。