飞行射击游戏的C++实现:一次课程作业

目录

程序简介

  该程序为可视化界面下的飞行射击游戏。程序中,下方的 * 符号代表玩家的飞机,上方的 + 符号代表敌方飞机,o 代表射出的子弹,使用a键和d键控制飞机向左向右移动,使用空格键退出游戏。当子弹击中敌方飞机后,得一分,当玩家被击中后,屏幕上会显示You are hit!

程序亮点

  • 将逻辑层和显示层分开,便于更换显示样式。
  • 逻辑层可以用于之后类似游戏的开发中。
  • 底层使用双精度浮点数表示坐标,具有更高的精度。
  • 定义了异常类型。
  • 定义了模板类fsPoint<T>
  • 代码中有丰富的注释,代码风格良好,便于阅读。
  • 使用chrono库处理有关计时的问题。

程序结构

  该程序在设计上包括逻辑层和显示层两层,目前显示层使用控制台有关函数实现。逻辑层不依赖于现实层,从而保证了一定的扩展性。

逻辑层(fsObject.h)

  逻辑层的定义和实现在fsObjects.hfsObjects.cpp中。下面是对逻辑层的介绍。

自定义数据类型

fsPoint Class

  存储程序中所需的点的坐标。这是一个模板类

    template <typename T>
    class fsPoint {
    public:
        T x, y; // 归一化之后的坐标位置,左上角为0,0  右下角为1,1
        fsPoint() {};
        fsPoint(T xval, T yval) :x(xval), y(yval) {};
        fsPoint<T> & operator = (const fsPoint<T> & rval);
    };
一些枚举类型
    // 飞机子弹来源
    enum bulletSource {
        player = 1,  // 子弹来自玩家
        enemy = 2,  // 子弹来自敌人
        playerFriend = 3  // 子弹来自玩家的友军
    };
    // 子弹射击方向
    enum Direction
    {
        up = 1, // 子弹向上飞行
        down = 2,  // 子弹向下飞行
        right = 3, // 子弹向左飞行
        left = 4 // 子弹向右飞行
    };

异常类型

fsInvalidInitializePointValException Class

  创建fsObject时所给出的点不满足左上点和右下点之间的坐标关系。

    class fsInvalidInitializePointValException : public std::logic_error {
    public:
        fsInvalidInitializePointValException() ;
    };
fsInvalidFireTimeException Class

  如果在规定的两次开火间隔时间内执行fsAirCraft.fire()就会抛出此异常。提示需要等待足够的时间后才能执行上述方法。

    class fsInvalidFireTimeException :public std::logic_error {
    public:
        fsInvalidFireTimeException() ;
    };
fsInvalidBulletSpeedInException Class

  输入的子弹速度无效。

class fsInvalidBulletSpeedInException : public std::logic_error {
    public:
        fsInvalidBulletSpeedInException();
    };
fsOutRangeMoveToPointException Class

moveToPoint()方法中,输入的目标点超出了屏幕显示范围(也即目标点的坐标之一不在0到1之间)。

class fsOutRangeMoveToPointException : public std::logic_error {
    public:
        fsOutRangeMoveToPointException();
    };

游戏中使用的物体

  所有的物体,包括玩家飞机,敌方飞机和子弹,以及分数显示,都是一个类,其基类为fsObject Class
  每一个物体都通过两个点,即两个fsPoint<double>类型的变量来确定所处位置,两个点分别为左上角的点和右下角的点。程序中将所处的空间进行了归一化,所有的物体的横纵坐标都只能在0到1之间。之后再通过显示层将其绘制到屏幕上。

fsObject Class

  游戏中所用物体的基类,该类的定义如下:

    class fsObject {
    public:
        fsObject() {};
        fsObject(const fsPoint<double> & upperleft, // 指定物体左上角的位置
            const fsPoint<double> & lowerright, // 指定物体右下角的位置,这两个点确定后,一个物体的位置和大小也就完全确定了
            const fsColor & fgc,  // 指定物体的显示颜色,如果使用控制台进行输出,这一参数将会被忽略
            char sym = 0  // 指定物体的符号,主要用于在控制台中显示该物体。
            );


        // 取得物体左上角的位置
        fsPoint<double> getUpperLeftCorner const();
        // 取得物体右下角的位置
        fsPoint<double> getLowerRightCorner const();
        // 取得该物体的符号
        char getSymbol() const ;
        // 为物体设置新的符号
        void setSymbol(char c) ;
        // 得到或设置物体的可见性,返回0为不可见,返回1为完全可见,如果物体不可见,其也就不能击中飞机
        int getVisible() const { return this->visible; }
        void setVisible(int newVisible) { this->visible = newVisible; return; }


        // 移动位置,向左移动一个单位(步长为FS_DEFAULT_MOVE_STEP) 移动成功则返回0 移动失败返回1
        int moveleft() ;
        // 移动位置,向右移动一个单位(步长为FS_DEFAULT_MOVE_STEP) 移动成功则返回0 移动失败返回1
        int moveright();
        //在竖直方向上移动位置,如果输入的delta导致物体的左上角超出屏幕,就将其置为最近的FS_DEFAULT_COORD_UPPER_LIMIT_Y或FS_DEFAULT_COORD_UPPER_LIMIT_Y
        int moveVertically(double delta) ;
        //在水平方向上移动位置,如果输入的delta导致物体的左上角超出屏幕,就将其置为最近的FS_DEFAULT_COORD_UPPER_LIMIT_X或FS_DEFAULT_COORD_UPPER_LIMIT_X
        int moveHorizontally(double delta);
        // 直接将物体移到左上角与某一点重合的位置,成功则返回1,失败返回0
        int moveToPoint(const fsPoint<double> & dest);

        // 打印出物体左上角的位置
        void printpos() const ;
    };
fsBullet Class

  用于表示游戏中飞行的子弹,其定义及有关方法如下:

    // 子弹
    class fsBullet : public fsObject (
    public:
        fsBullet() {};
        fsBullet(bulletSource sourceOfBullet,  // 子弹的来源
            Direction directionOfBullet,  // 子弹的飞行方向
            int damageOfBullet, // 子弹的威力,也即子弹击中后玩家获得多少分
            fsPoint<double> sourceulc, // 发出子弹物体的左上角坐标,通过该坐标可以确定子弹的初始位置
            double bulletVec = FS_DEFAULT_BULLET_FLY_SPEED  // 子弹的飞行速度
            ) ;

        // 更新子弹的位置,每次循环中都要执行此函数以更新子弹的位置,如果返回0,则表示子弹已经飞行到边缘。
        int updataPosition();
    };
fsAircraft Class

  游戏中所有飞机的基类,玩家的飞机和敌人的飞机均由此派生。

class fsAircraft : public fsObject {
    public:
        fsAircraft() {};
        fsAircraft(fsPoint<double> upperleft,  // 飞机左上角的位置
            fsPoint<double> lowerright,   // 飞机右下角的位置
            fsColor fgc,  // 飞机的显示颜色,在控制台中此参数无效
            std::chrono::milliseconds fireinterval = FS_DEFAULT_FIRE_INTERVAL  // 飞机的子弹发射间隔,在经过此段时间后飞机才能发射子弹
            ) ;
        // 发射子弹的相关逻辑,判断能否发射子弹,也即自上一次发射子弹之后,是否经历了足够的时间
        // 如果可以发射子弹则返回1,不能则返回0
        inline int fireReady();
        // 如果能够发射子弹,则执行此函数,以返回fsBullet的对象,即该飞机所发射的子弹
        fsBullet fire();

        // 设置新的飞机子弹伤害
        int setBullteDamage(int newBulletDamage);
        // 设置飞机是玩家的飞机还是敌人的飞机
        void setSource(bulletSource newSource);
        // 设置新的子弹飞行方向
        void setFireDirection(Direction newDirection);

        // 返回剩余生命值,当返回0或者负数时说明死亡(本程序中没有使用)
        int getLifeLeft() const;

        // 当被击中后调用此函数,返回剩余生命值  当返回0或者负数时说明死亡  子弹威力为负数说明为生命补给包
        int gotHit(int bulletDamage = 1);
}
fsMyAircraft Class

  玩家的飞机,结合fsObject中的moveleft()moveright()方法玩家可以通过键盘操纵其位置。

    class fsMyAircraft : public fsAircraft {
    public:
        fsMyAircraft(fsPoint<double> upperleft,  // 飞机左上角的位置
            fsPoint<double> lowerright,   // 飞机右下角的位置
            fsColor fgc,   // 飞机的显示颜色,在控制台中此参数无效
            std::chrono::milliseconds  fireinterval = FS_DEFAULT_FIRE_INTERVAL  // 飞机的子弹发射间隔,发射子弹后,再经过此段时间后才能再次发射子弹
            ) ;
    };
fsEnemyAircraft Class

  敌人的飞机,飞行方向随机决定。

class fsEnemyAircraft : public fsAircraft {
    public:
        fsEnemyAircraft(fsPoint<double> upperleft, // 飞机左上角的位置
            fsPoint<double> lowerright,  // 飞机右下角的位置
            fsColor fgc,  // 飞机的显示颜色,在控制台中此参数无效
            std::chrono::milliseconds  fireinterval = FS_DEFAULT_FIRE_INTERVAL  // 飞机的子弹发射间隔,在经过此段时间后飞机才能发射子弹
            );

        // 更新敌机的位置,敌机随机移动,每隔一段时间更换一个方向,移动会在一个矩形框中进行
        int updatePosition();
fsScoreBoard Class

  计分版 用于记录玩家的分数

class fsScoreBoard : public fsObject {
    public:
        fsScoreBoard() {};
        fsScoreBoard(const fsPoint<double> & upperleft, // 计分版左上角的位置 推荐将左上角的横坐标设置为大于1的数,避免分数的显示与游戏画面重叠
            const fsPoint<double> & lowerright,  // 计分版右下角的位置
            const fsColor & fgc = FS_DEFAULT_SCOREBOARD_COLOR  // 计分版颜色,在控制台情况下无效
            ) ;
        // 获取计分版分数
        int getCurScore() const ;
        // 增加得分
        void addScore(int delta = 1) ;
    };

逻辑层中提供的一些函数

    // 判断两个物体是否重叠 如果重叠将返回1 否则返回0,主要用来判定子弹是否击中了飞机
    int fsOverlap(const fsObject & obj1, const fsObject & obj2);

    // 判断一个点是否在一个矩形中,包括在矩形边上的情况,主要用来实现fsOverlap
    template <typename T>   
    bool fsPointInRect(fsPoint<T> point, fsPoint<T> RectUpperLeft, fsPoint<T> RectLowerRight);

逻辑层中定义的一些常量

#define FS_DEFAULT_FIRE_INTERVAL std::chrono::milliseconds(500)  // 默认的开火间隔
#define FS_DEFAULT_FRAME_INTERVAL 10 // 默认的刷新频率,即每隔多少毫秒刷新一次
#define FS_DEFAULT_MOVE_STEP 0.01   // 在moveleft()方法和moveright()方法中默认的每次移动距离
#define FS_MOVE_LEFT_KEY 'a'  // 控制玩家飞机向左移动一个单位所对应的按键
#define FS_MOVE_RIGHT_KEY 'd' // 控制玩家飞机向右移动一个单位所对应的按键
#define FS_ESCAPE_KEY ' '  // 退出游戏所使用的按键
#define FS_DEFAULT_BULLET_FLY_SPEED 3.0  // 默认子弹飞行速度,3代表每秒飞行三个屏幕
#define FS_DEFAULT_AIRCRAFT_FLY_SPEED 0.1  // 默认的飞机飞行速度
#define FS_DEFAULT_BULLET_COLOR fsColor(255,0,0,0)  // 默认子弹颜色
#define FS_DEFAULT_BULLET_SIZE 0.1 //默认的子弹正方形边长
#define FS_DEFAULT_BULLET_SIZE_X 0.05 //默认的子弹长方形在x轴方向的长度
#define FS_DEFAULT_BULLET_SIZE_Y 0.1 //默认的子弹长方形在y轴方向的长度
#define FS_DEFAULT_PLAYER_SYMBOL '*'  // 默认的玩家的飞机的符号
#define FS_DEFAULT_ENEMY_SYMBOL '+' // 默认的敌人的飞机的符号
#define FS_DEFAULT_BULLET_SYMBOL 'o' // 默认的子弹的符号
#define FS_DEFAULT_SCOREBOARD_COLOR fsColor(0,255,0,0)  // 默认的计分版颜色
// 默认的边界位置,这些位置主要在move*方法中限制物体不要超出屏幕边界
#define FS_DEFAULT_COORD_LOWER_LIMIT 0.05
#define FS_DEFAULT_COORD_UPPER_LIMIT 0.95
#define FS_DEFAULT_COORD_LOWER_LIMIT_X 0.05
#define FS_DEFAULT_COORD_UPPER_LIMIT_X 0.95
#define FS_DEFAULT_COORD_LOWER_LIMIT_Y 0.05
#define FS_DEFAULT_COORD_UPPER_LIMIT_Y 0.95

显示层(fsDraw.h

  该层主要提供了将物体显示在控制台中的函数,如下:

    //设置控制台光标位置(x,y)
    void gotoxy(int x, int y) {
        COORD pos;
        pos.X = x;
        pos.Y = y;
        SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
    }
    // 设置控制台颜色
    void setcolor(WORD color) {
        SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
    }
    // 画出对象,对象的位置由其左上角位置决定,对象的符号由getSymbol()方法返回的字符决定,可以通过
    // setSymbol()方法改变这一符号
    int fsDraw(const fsObject & fso) {
        gotoxy((fso.getUpperLeftCorner()).x * FS_DEFAULT_CONSOLE_BUFFER_SCALE,
            (fso.getUpperLeftCorner()).y * FS_DEFAULT_CONSOLE_BUFFER_SCALE);
        setcolor(FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_RED);
        cout << fso.getSymbol();
        return 0;
    }
    // 画出计分版并显示分数
    int fsDrawScoreBoard(const fsScoreBoard & fsb) {
        gotoxy((fsb.getUpperLeftCorner()).x * FS_DEFAULT_CONSOLE_BUFFER_SCALE,
            (fsb.getUpperLeftCorner()).y * FS_DEFAULT_CONSOLE_BUFFER_SCALE);
        setcolor(FOREGROUND_GREEN);
        cout << "SCORE: " << fsb.getCurScore();
        return 0;   
    }

程序执行流程(fsTest.h

  在包含的程序提供的类文件(fsObjects.h, fsDraw.h)后,简单的射击小游戏可以如下操作(例子请见fsTest.h中的gameTestOneBuf()函数):
(1)初始化有关物体,主要是玩家飞机和敌方飞机、计分版,以及有关的子弹

    // 初始化玩家飞机
    fsMyAircraft playerAircraft = fsMyAircraft(fsPoint<double>(0.5, 0.99),
        fsPoint<double>(0.51, 1.00),
        fsColor(255, 255, 255, 0));
    // 初始化敌方飞机
    fsEnemyAircraft enemy1 = fsEnemyAircraft(fsPoint<double>(0.5, 0.10),
        fsPoint<double>(0.51, 0.11),
        fsColor(255, 0, 0, 0));
    // 初始化计分板
    fsScoreBoard scoreBoard = fsScoreBoard(fsPoint<double>(1.1, 0.3), fsPoint<double>(1.11, 0.31));
    // 等待1s以便能够通过fire函数成功初始化子弹
    Sleep(1000);
    fsBullet mybullet;
    if (playerAircraft.fireReady()) {
        mybullet = playerAircraft.fire();
    }
    fsBullet enemybullet;
    if (enemy1.fireReady()) {
        enemybullet = enemy1.fire();
    }
    // 初始化键盘按键存储变量
    char ch = '/0';

(2) 进入游戏循环,在游戏循环中,需要依次完成读取按键,更新各物体状态,绘出物体,子弹击中判断等过程

    // 游戏循环
    while (1) {
        // 获取按键
        if (_kbhit())
        {
            ch = getche();

        }
        // 依据不同的按键做出对应的反应
        switch (ch)
        {
        case FS_MOVE_LEFT_KEY:
            playerAircraft.moveleft();          
            break;
        case FS_MOVE_RIGHT_KEY:
            playerAircraft.moveright();
            break;
        case FS_ESCAPE_KEY:
            cout << "You ended this game!";
            return 0;
        default:
            break;
        }
        ch = 0;
        // 更新各对象的状态
        if (playerAircraft.fireReady()) {
            mybullet = playerAircraft.fire();
        }
        if (enemy1.fireReady()) {
            enemybullet = enemy1.fire();
        }
        mybullet.updataPosition();
        enemybullet.updataPosition();
        enemy1.updatePosition();

        // 呈现图像
        system("cls");
        fsDraw(playerAircraft);
        fsDraw(mybullet);
        fsDraw(enemy1);
        fsDraw(enemybullet);
        fsDrawScoreBoard(scoreBoard);
        // 子弹击中的判定,如果子弹击中,则将原来的子弹设为不可见
        if (fsOverlap(mybullet, enemy1)) {
            scoreBoard.addScore();
            mybullet.setVisible(0);
        }
        if (fsOverlap(enemybullet, playerAircraft)) {
            cout << "you are hit!";
            enemybullet.setVisible(0);
            Sleep(500);
        }
        Sleep(FS_DEFAULT_FRAME_INTERVAL);
    }

小结

  这次大作业大概花了三四天时间,这个过程中,我在可视化界面的选择上比较纠结,本想用QT,但感觉学QT也要花不少时间,而且画面也不是这门课程的重点,所以最后选择了控制台来做可视化界面,虽然效果不太理想,但至少可以运行。
  在大作业的过程中,我不仅运用了课上学习的关于模板和类的知识,还自学了有关控制台绘制的控制等技能,让我初步体验了完成一个项目是什么样的经历,很有意义。在写说明文档的过程中,我发现Word在处理代码的时候非常麻烦,又学习了Markdown的使用,也算是另外一个小小的收获吧。
  这次的作业虽然已经可以运行,但离真正的飞行射击游戏还有很大的差距,比如,画面不够精美,没有升级制度,没有设置多个敌机,没有Boss,不支持PVP(玩家VS玩家)等,以后我还会再抽时间不断完善。值得一提的是,本来想要使用双缓冲技术避免屏幕闪烁,但这样就难以显示出完整的画面,所以最后我搁置了这个方案,这点可能是我首先需要完善的地方。

最新代码获取

请访问我的github:https://github.com/archimekai/flightShooting/
这里包含了VS2015的解决方案,可以直接编译运行。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值