1. 程式人生 > >C++代碼訓練之坦克大戰(2)

C++代碼訓練之坦克大戰(2)

mbo calculate 人的 src ack 交流 學習過程 ima mta

這一篇中,我們繼續繼續進行我們的坦克大戰。

技術分享

位置信息數據結構

在遊戲設計過程中,需要記錄大量的位置信息,如果僅僅使用(x,y)坐標很容易出錯。這一篇中,我們先定義兩個簡單的數據結構用來保存點和矩形的信息。

在項目中新建Model目錄,創建下面四個文件:

技術分享

代碼如下:

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

這個頭文件創建了一個Point類,有兩個成員變量m_x,m_y用來記錄一個點的橫、縱坐標。一組public方法用來完成給對象賦值和讀取坐標值的操作。

這裏我們用到了C++的運算符重載功能,將“=”功能進行重載,方便我們用一個Point對象給另一個Point對象賦值,同時也能夠使我們將Point作為參數進行傳遞。

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類的實現,大家一看就明白。

這裏需要強調的是,在類的封裝過程中有一個非常重要的原則是不允許將成員變量用public的方法暴露在外。如果類的外部代碼能夠直接對類成員變量進行修改的話,程序將很不安全。正確的方法是像我們這樣實現一組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對象,分別表示矩形的左上角和右下角。這裏我們強行規定m_startPoint表示左上角,m_endPoint表示右下角。如果創建對象時兩個點順序反了,Check()函數會自動把它們調整過來。

這裏需要註意,GetStartPoint()和GetEndPoint()兩個函數都通過const修飾,表示返回值不能被修改。為什麽要這麽實現呢,因為這個函數的結果將會傳進EasyX接口中,而這些接口大部分都要求參數是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;
    }
}

這個文件中實現了Rect類的成員函數。

主戰坦克升級

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對象,用來記錄坦克的形狀範圍。之前我們的坦克總是用一組坐標來表示,這個坐標是坦克的中心點,所有跟坦克相關的行為都通過這個點來計算位置,實現起來有些復雜,有了這個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

這個文件中沒有太大的修改,只是在成員變量初始化時做了一些調整。主戰坦克的顏色改成了黃色,初始化後調用CalculateSphere()函數計算出矩形位置。

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()

這個函數的參數被拿掉了,在這裏我們通過坦克當前方向來判斷它的形狀。

在繪制履帶時,我們利用了m_rectSphere的位置坐標,雖然看起來代碼變多了,但只有一個數字4是無意義的,它代表履帶的寬度。如果這個寬度需要經常調整的話,我們還可以考慮把它用一個成員變量管理起來。

在判斷坦克形狀時,我們利用了switch的一個特性,通過故意少寫break關鍵字,讓兩個判斷結果公用一段代碼,這個早已經講過,這裏不多說了。

  • Display()

之前我們用setfillcolor設置了填充顏色,這裏我們加入了setcolor,這樣畫出來的坦克邊框也是我們設置的顏色。

  • Move()

這個函數中,比較奇怪的是出現了一個沒見過的函數Graphic::GetBattleGround()。我們今天要給坦克劃定一個運行區域,不能讓它滿屏幕行駛了,這個後面再說。

這裏要註意,每移動一次都需要調用CalculateSphere()方法重新計算坦克區域。

  • CalculateSphere()

這個很簡單,計算出左上角和右下角的Point位置即可。

更新畫布

之前的畫布顏色太深,我們要做修改。另外,我們後面需要在窗口上顯示遊戲信息,因此,要在右邊留出一部分空間。我們給坦克劃定一個新的區域,讓它們在裏面行駛。

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

文件中通過一組宏來定義戰場區域的位置。另外通過m_rectBattleGround這個Rect對象來保存。

細心的讀者應該發現了,我們在引用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;
}

代碼在創建畫布是,重新指定了背景顏色。DrawBattleGround()函數在屏幕上畫出了戰場的範圍。

main.cpp

主函數中,我們只需要再循環中添加一個DrawBattleGround函數的調用即可。

if (!skip)
{
    cleardevice();

    Graphic::DrawBattleGround();

    mainTank.Move();
    mainTank.Display();
}

好了,運行一下程序,看看效果吧。

技術分享

敵人坦克

屏幕上只有一個自己的坦克看著有些孤單,我們再添加上些敵人的坦克。

新建文件EnemyTank.h和EnemyTank.cpp。實現一個敵人坦克類。

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這個抽象類,所有的坦克都從它繼承就好了。除了抽象類中繼承的函數之外,我們加了一個RandomTank()用來隨機地在戰場區域生成一個坦克。

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函數,代碼如下:

#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用來設置坦克的數量
  • 用一個指針數組保存每個坦克的指針
  • 循環創建坦克,這裏用了new的方法把坦克對象創建在堆空間中
  • 每次擦屏之後,遍歷指針數組,繪制出每個坦克。調用的是Move()和Display()方法。
  • 退出程序前,釋放每一個坦克所占的堆空間。

註意:C++中依然是new和delete成對出現,有申請就要有釋放。否則會出現內存泄露。

下面運行一下代碼,看看我們今天的成果。

技術分享

是不是一下熱鬧了很多呢?

學習過程中遇到什麽問題或者想獲取學習資源的話,歡迎加入學習交流群
639368839,我們一起學C/C++!

C++代碼訓練之坦克大戰(2)