C++代码训练营 | 坦克大战(1)

坦克大战

终于等到今天了。在《21天C语言代码训练营》中,我就想讲这个项目了,只是用C语言写会比较麻烦,我怕自己水平有限讲不清楚砸了自己的招牌,不得已就放弃了。如今到了C++,我想是时候拿出来讲一讲了。

对坦克大战情有独钟是因为大学时候第一次参加程序设计比赛就做的这个游戏。当时用的语言是Java,那个比赛让我悟出了面向对象的强大之处,我也是从那时开始接触设计模式的。对我而言,坦克大战有着非同寻常的意义,所以一定要带大家用C++实现一下。

知识准备

前面我们用一个星空的项目给大家介绍面向对象编程的三个重要特性。没有看过的同学建议你看了这些文章之后再来学习后面的内容。

  • 封装

C++代码训练营 | 绘制星空

  • 继承

C++代码训练营 | 另一片星空

  • 多态

C++代码训练营 | 多样的星空

代码分享

这个项目中的代码会在GitHub上发布,里面的每一个分支对应着简书中的每一篇文章。

比如,今天是第一篇文章,这一篇里完成的代码会上传在day1这个分支中,以此类推。

坦克大战

我们依然使用EasyX在控制台程序中制作这个游戏程序。这一篇的主要任务是在屏幕上画出一个白色的主战坦克,可以通过方向键控制它的前进方向。效果如下:

下面我们正式开始。

画布类

在这个工程中,我们将EasyX画布相关的功能封装在一个Graphic类中,创建两个文件:Graphic.h和Graphic.cpp。

Graphic.h

#ifndef __GRAPHIC_H__
#define __GRAPHIC_H__

#include <graphics.h>
    
#define SCREEN_WIDTH    1024
#define SCREEN_HEIGHT   768

class Graphic
{
public:
    static void Create();
    static void Destroy();

    static int GetScreenWidth();
    static int GetScreenHeight();

private:
    static int m_screen_width;
    static int m_screen_height;
};

#endif

Graphic.cpp

#include "Graphic.h"

int Graphic::m_screen_width = SCREEN_WIDTH;
int Graphic::m_screen_height = SCREEN_HEIGHT;

void Graphic::Create()
{
    initgraph(m_screen_width, m_screen_height);
}

void Graphic::Destroy()
{
    closegraph();
}

int Graphic::GetScreenWidth()
{
    return m_screen_width;
}

int Graphic::GetScreenHeight()
{
    return m_screen_height;
}

这个类使用的是静态成员变量和静态成员函数,要注意静态成员变量的初始化时在类外进行的。通过这个类可以实现创建和销毁画布的功能,还能够让其他代码随时通过这个类拿到画布的尺寸。

坦克抽象类

由于我们的程序是要通过EasyX画在屏幕上,各种元素都需要统一放在可以遍历的数据结构中方便操作,所以我们在实现坦克代码时会用到多态。这里先创建一个坦克的抽象类。新建文件Tank.h,加入下面代码:

#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:
    int m_x;
    int m_y;
    COLORREF m_color;

    Dir m_dir;
    int m_step;
};

#endif

所有坦克都需要引用的东西会定义在这个文件中。这里定义了一个枚举类型,表示方向用的。我们的程序只考虑四个方向,如果需要让坦克可以有八个前进方向后面可以在这里扩充其他方向。

坦克抽象类中,我们定义了两个函数,Display()和Move()大家很熟悉了,在星空项目里用的很多,主要是负责将自己画在屏幕上和移动自己。

属性中m_dir保存坦克的行驶方向,Display和Move都需要使用它。

主战坦克

所谓主战坦克就是玩家控制的坦克,所有的坦克中,只有这个一个是可以控制的。这一点它比较特殊。

创建文件MainTank.h,写入下面代码:

#ifndef __MAIN_TANK__
#define __MAIN_TANK__

#include "Tank.h"

class MainTank : public Tank
{
public:
    MainTank()
    {
        m_x = 400;
        m_y = 300;
        m_color = WHITE;
        m_dir = Dir::UP;
        m_step = 2;
    }

    ~MainTank(){}

    // 设置行驶方向
    void SetDir(Dir dir);

    void Display();
    void Move();

protected:
    // 绘制坦克主体
    void DrawTankBody(int style);
};

#endif

这个类继承了Tank类,在初始化时给各个属性赋初值。我们默认主战坦克一开始在屏幕中间,行驶方向向上,颜色为白色。

我们主战坦克的形状如下:

我们来看看怎么实现它。创建文件MainTank.cpp代码如下:

#include "MainTank.h"

void MainTank::SetDir(Dir dir)
{
    m_dir = dir;
}

void MainTank::DrawTankBody(int style)
{
    fillrectangle(m_x - 4, m_y - 4, m_x + 4, m_y + 4);

    if (style == 1)
    {
        fillrectangle(m_x - 8, m_y - 6, m_x - 6, m_y + 6);
        fillrectangle(m_x + 6, m_y - 6, m_x + 8, m_y + 6);
    }
    else
    {
        fillrectangle(m_x - 6, m_y - 8, m_x + 6, m_y - 6);
        fillrectangle(m_x - 6, m_y + 6, m_x + 6, m_y + 8);
    }
}

void MainTank::Display()
{
    COLORREF color_save = getfillcolor();

    setfillcolor(m_color);

    switch (m_dir)
    {
    case UP:
        DrawTankBody(1);
        line(m_x, m_y, m_x, m_y - 10);
        break;
    case DOWN:
        DrawTankBody(1);
        line(m_x, m_y, m_x, m_y + 10);
        break;
    case LEFT:
        DrawTankBody(0);
        line(m_x, m_y, m_x - 10, m_y);
        break;
    case RIGHT:
        DrawTankBody(0);
        line(m_x, m_y, m_x + 10, m_y);
        break;
    default:
        break;
    }

    setfillcolor(color_save);
}

void MainTank::Move()
{
    switch (m_dir)
    {
    case UP:
        m_y -= m_step;
        if (m_y < 0)
            m_y = Graphic::GetScreenHeight() - 1;
        break;
    case DOWN:
        m_y += m_step;
        if (m_y > Graphic::GetScreenHeight())
            m_y = 1;
        break;
    case LEFT:
        m_x -= m_step;
        if (m_x < 0)
            m_x = Graphic::GetScreenWidth() - 1;
        break;
    case RIGHT:
        m_x += m_step;
        if (m_x > Graphic::GetScreenWidth())
            m_x = 1;
        break;
    default:
        break;
    }
}

SetDir()

这个很简单,就是修改成员变量的值。通过这个函数能够改变坦克的行驶方向。

DrawTankBody()

这个函数负责画坦克的主题部分,一个正方形的坦克身和两个矩形的履带。由于坦克上下行驶和左右行驶形状不同,因此通过一个参数负责绘制不同的形状。

Display()

这个是核心的绘制方法,提供给外部调用的。这里主要是两部分工作:

  • 判断坦克的行驶方向,之后调用DrawTankBody绘制出坦克身
  • 根据行驶方向画上炮管

Move()

这个函数每执行一次,坦克向前移动m_step长度。当超出屏幕边沿时,从另一侧重新出现,行驶方向不变。是不是很简单。

键盘事件监听

最后到了我们的主程序了,新建文件main.cpp,加入下面代码:

#pragma warning(disable:4996)

#include <iostream>
#include <conio.h>
#include <time.h>

#include "Graphic.h"
#include "MainTank.h"

using namespace std;

void main()
{
    Graphic::Create();

    MainTank mainTank;

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

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

        Sleep(200);
    }
    
    Graphic::Destroy();
}

这里需要说明的是,我们通过kbhit()捕捉键盘动作,之后再通过getch()得到按下键的码值。有了码值,我们就可以做相应的操作了。这里主要实现了下面几个功能:

  • 方向键

按了上下左右方向键之后,坦克相应地进行转向。

  • Esc键

按了Esc键之后,程序退出。

  • Enter键

按了Enter键之后,程序暂停,再按一下程序继续。

好了,我们现在来运行一下程序。是不是看到了一辆神奇的白色坦克呢?

我是天花板,让我们一起在软件开发中自我迭代。
如有任何问题,欢迎与我联系。


下一篇:C++代码训练营 | 坦克大战(2)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 这一篇中,我们继续继续进行我们的坦克大战。 位置信息数据结构 在游戏设计过程中,需要记录大量的位置信息,如果仅仅使...
    天花板阅读 7,464评论 14 25
  • 上一篇中,我们添加了可以自动行驶的敌人坦克,今天我们给主战坦克添加最核心的功能——开炮。 第一次重构 既然要开炮,...
    天花板阅读 4,064评论 1 13
  • 现在我们的游戏已经初具规模,但如果主战坦克一直是无敌状态那也很无趣。今天我们来让敌人的炮火发挥作用。 主战坦克被击...
    天花板阅读 2,672评论 1 8
  • 战场范围 之前我们的坦克从战场的一边走出之后会从另一边重新进入战场。这样不符合我们游戏的定义。我们需要把它们改成遇...
    天花板阅读 4,447评论 0 12
  • 上一篇中,我们的主战坦克发出的炮弹还没有实际的作用,今天我们就让它拥有击毁敌军坦克的功能。 新增基础API Rec...
    天花板阅读 3,386评论 11 6