C++大白话系列-C++面向对象篇-27-实战项目:粉丝连连看游戏

实战项目:粉丝连连看游戏

> 老子能搞游戏!

C++是个废物吗?

学生的困惑

问题: “C++是个废物吗?除了面对黑乎乎的控制台,我还能干什么?”

大家是不是曾经也这么想过呢?

老师: “如果是这样的话,你肯定误会他了。给你们露一手,老子能搞游戏!

准备工作

选择游戏引擎

我们要用C++来写游戏,就需要选择一个游戏引擎。

本节使用: Cocos2d-x

已经讲过如何安装了: 第24篇文章

学生: “老师,要是自己不会安装怎么办呀?”

老师: “那就不要害羞啊,直接找大白话老师是啊!”

运行第一个游戏程序

惊喜来了

创建好游戏工程后,点击运行,稍等片刻…

我的天哪,这是什么?

  • 这是界面了!
  • 再也不用面对黑乎乎的控制台了!

学生感慨: “果然,学编程的这帮臭男人都靠不住。昨天还叫人家小甜甜,今天就叫人家牛夫人了。”

调整窗口大小

窗口太小了

学生: “大家有没有发现这个窗口这么小,只能搞个毛线,对不对?”

修改代码

找到文件: AppDelegate.cpp

// 找到这行代码
static cocos2d::Size designResolutionSize = cocos2d::Size(480, 320);

// 修改为
static cocos2d::Size designResolutionSize = cocos2d::Size(960, 640);

效果: 窗口变大了!

学生: “感觉编程也就那样子呀,感觉我很快就可以高薪就业迎娶白富美了呀。”

老师: “师娘叫你拿块搓衣板过去一下。”

熟悉游戏引擎

查看文档

学生: “这时候我强劲的英文水平就体现出来了,看英文官网文档完全不在话下!”

老师: “我呸,真不要脸,明明就是点击到这里的中文版去查看的。”

坐标系统

通过仔细阅读,我们发现:

游戏的界面有个坐标系统:

  • 原点在窗口的左下角
  • 坐标范围就是设置的窗口大小

坐标示意图:

(0, 640) ─────────── (960, 640)
                         
                         
           游戏窗口       
                         
                         
(0, 0) ───────────── (960, 0)

游戏元素

三个基本元素

默认场景中有:

  1. 文字 – Label
  2. 图片 – Sprite
  3. 按钮 – Menu

代码位置: HelloWorldScene.cpp

学生: “这几个元素是在这个代码文件的这个地方创建的,可以看到都有代码。那我们是不是可以照着创建自己想要的元素呢?”

添加自己的图片

资源文件夹

图片位置: Resources 文件夹

经过一番搜索,我们发现这些图片都放在了这个文件夹下面。

行动:

  • 把自己的图片放进来
  • 开始制作”粉丝连连看”游戏

测试创建图片

删除默认元素

在 HelloWorldScene.cpp 的 init 函数中:

bool HelloWorld::init()
{
    if ( !Scene::init() )
    {
        return false;
    }

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    // 删除之前的三个元素代码

    // 创建粉丝头像测试
    auto sprite = Sprite::create("fan1.png");
    sprite->setPosition(Vec2(200, 200));
    this->addChild(sprite);

    return true;
}

运行: “我去,成功了!”

学生: “此时此刻,大家还觉得C++是废物吗?”

大胆的想法

粉丝连连看

这个时候我已经有了一个大胆的想法:

游戏设计:

  • 10行15列的按钮
  • 随机生成成对的粉丝头像
  • 连连看的游戏雏形

学生: “说干就干!”

游戏架构

HelloWorldScene.h

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"

class HelloWorld : public cocos2d::Scene
{
public:
    static cocos2d::Scene* createScene();

    virtual bool init();

    CREATE_FUNC(HelloWorld);

private:
    // 游戏数据
    static const int ROWS = 10;     // 行数
    static const int COLS = 15;     // 列数

    int gameData[ROWS][COLS];       // 头像状态(0表明已消除)
    cocos2d::MenuItemImage* buttons[ROWS][COLS];  // 按钮数组

    int lastClickRow;               // 上次点击的行
    int lastClickCol;               // 上次点击的列

    // 点击头像的回调函数
    void onFanClick(cocos2d::Ref* sender);

    // 检查两点是否可连通
    bool canConnect(int row1, int col1, int row2, int col2);
};

#endif

学生: “天才!我真是天才!”

实现游戏逻辑

HelloWorldScene.cpp

#include "HelloWorldScene.h"
#include <ctime>
#include <cstdlib>

USING_NS_CC;

Scene* HelloWorld::createScene()
{
    return HelloWorld::create();
}

bool HelloWorld::init()
{
    if ( !Scene::init() )
    {
        return false;
    }

    srand(time(0));

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    // 初始化数据
    lastClickRow = -1;
    lastClickCol = -1;

    // 准备150个头像(75对)
    std::vector<int> fanIds;
    for (int i = 1; i <= 5; i++) {  // 假设有5种头像
        for (int j = 0; j < 30; j++) {  // 每种30个
            fanIds.push_back(i);
        }
    }

    // 随机打乱
    for (int i = 0; i < 150; i++) {
        int j = rand() % 150;
        int temp = fanIds[i];
        fanIds[i] = fanIds[j];
        fanIds[j] = temp;
    }

    // 创建按钮
    int index = 0;
    for (int row = 0; row < ROWS; row++) {
        for (int col = 0; col < COLS; col++) {
            gameData[row][col] = fanIds[index++];

            // 创建按钮
            std::string filename = "fan" + std::to_string(gameData[row][col]) + ".png";
            auto button = MenuItemImage::create(
                filename,
                filename,
                CC_CALLBACK_1(HelloWorld::onFanClick, this)
            );

            button->setScale(0.2f);  // 缩放到合适大小
            button->setPosition(Vec2(
                origin.x + 50 + col * 60,
                origin.y + 550 - row * 60
            ));
            button->setTag(row * COLS + col);  // 用tag保存位置信息

            buttons[row][col] = button;

            auto menu = Menu::create(button, nullptr);
            menu->setPosition(Vec2::ZERO);
            this->addChild(menu);
        }
    }

    return true;
}

void HelloWorld::onFanClick(Ref* sender)
{
    auto button = (MenuItemImage*)sender;
    int index = button->getTag();
    int row = index / COLS;
    int col = index % COLS;

    // 排除不合理的情况
    if (gameData[row][col] == 0) {
        return;  // 已经消除了
    }

    // 第一次点击
    if (lastClickRow == -1) {
        lastClickRow = row;
        lastClickCol = col;
        button->setOpacity(128);  // 半透明提示
        return;
    }

    // 第二次点击
    if (row == lastClickRow && col == lastClickCol) {
        button->setOpacity(255);  // 恢复
        lastClickRow = -1;
        lastClickCol = -1;
        return;
    }

    // 检查是否可以连接
    if (gameData[row][col] == gameData[lastClickRow][lastClickCol] &&
        canConnect(lastClickRow, lastClickCol, row, col)) {
        // 消除
        gameData[row][col] = 0;
        gameData[lastClickRow][lastClickCol] = 0;

        button->setVisible(false);
        buttons[lastClickRow][lastClickCol]->setVisible(false);

        CCLOG("Match! (%d,%d) - (%d,%d)", lastClickRow, lastClickCol, row, col);
    } else {
        // 不匹配
        buttons[lastClickRow][lastClickCol]->setOpacity(255);
    }

    lastClickRow = -1;
    lastClickCol = -1;
}

bool HelloWorld::canConnect(int row1, int col1, int row2, int col2)
{
    // 简化版连通判断(直线连接)
    // 实际连连看需要更复杂的逻辑(0转弯、1转弯、2转弯)

    // 同一行
    if (row1 == row2) {
        int minCol = std::min(col1, col2);
        int maxCol = std::max(col1, col2);
        for (int c = minCol + 1; c < maxCol; c++) {
            if (gameData[row1][c] != 0) {
                return false;
            }
        }
        return true;
    }

    // 同一列
    if (col1 == col2) {
        int minRow = std::min(row1, row2);
        int maxRow = std::max(row1, row2);
        for (int r = minRow + 1; r < maxRow; r++) {
            if (gameData[r][col1] != 0) {
                return false;
            }
        }
        return true;
    }

    return false;
}

开发过程的真实写照

第一次运行

学生: “我去,这什么情况,这按钮怎么会这么大?”

发现: 原来是图片太大了

解决: 设置缩放倍数为0.2

button->setScale(0.2f);

第二次运行

学生: “我靠,肯定是编译器有bug!”

three days later…

学生: “虽然经过短暂的2小时修复,我发现是我这里写错了个变量,但依然掩盖不了我是天才的实际啊。”

CV大法

学生: “我演不下去了,我明明写好了代码,还要装着写给你们看。不行的,我不演了,我要开始CV大法了!”

老师: “我靠,你作为程序员的谦逊呢?作为演员的自我修养呢?”

学生: “所以这里检查通过我们清除一下头像,复制粘贴一下…”

学生: “喂,这里怎么报错了呀?我的粘贴大法从来都不会失误的呀。哦,原来是少了粘贴一个,我们只要到前面去再粘贴一个代码就大功告成了。”

最终效果

大功告成

学生: “接着大家就可以来欣赏我的大作了。大家看一下,有没有,有没有!我们的粉丝连连看就完成了!”

Nice!

老师: “同学,想学CV大法吗?”

学生: “啊,不,同学学编程吗?”

完整连通算法

改善版canConnect函数

bool HelloWorld::canConnect(int row1, int col1, int row2, int col2)
{
    // 检查直线连接(0转弯)
    if (checkDirectLine(row1, col1, row2, col2)) {
        return true;
    }

    // 检查一次转弯
    if (checkOneCorner(row1, col1, row2, col2)) {
        return true;
    }

    // 检查两次转弯
    if (checkTwoCorner(row1, col1, row2, col2)) {
        return true;
    }

    return false;
}

bool HelloWorld::checkDirectLine(int row1, int col1, int row2, int col2)
{
    // 同一行
    if (row1 == row2) {
        int minCol = std::min(col1, col2);
        int maxCol = std::max(col1, col2);
        for (int c = minCol + 1; c < maxCol; c++) {
            if (gameData[row1][c] != 0) {
                return false;
            }
        }
        return true;
    }

    // 同一列
    if (col1 == col2) {
        int minRow = std::min(row1, row2);
        int maxRow = std::max(row1, row2);
        for (int r = minRow + 1; r < maxRow; r++) {
            if (gameData[r][col1] != 0) {
                return false;
            }
        }
        return true;
    }

    return false;
}

bool HelloWorld::checkOneCorner(int row1, int col1, int row2, int col2)
{
    // 尝试拐点1: (row1, col2)
    if (gameData[row1][col2] == 0 || (row1 == row2 && col2 == col1)) {
        if (checkDirectLine(row1, col1, row1, col2) &&
            checkDirectLine(row1, col2, row2, col2)) {
            return true;
        }
    }

    // 尝试拐点2: (row2, col1)
    if (gameData[row2][col1] == 0 || (row2 == row1 && col1 == col2)) {
        if (checkDirectLine(row1, col1, row2, col1) &&
            checkDirectLine(row2, col1, row2, col2)) {
            return true;
        }
    }

    return false;
}

bool HelloWorld::checkTwoCorner(int row1, int col1, int row2, int col2)
{
    // 沿行扩展
    for (int col = 0; col < COLS; col++) {
        if (col != col1 && col != col2 &&
            (gameData[row1][col] == 0 || col == col1) &&
            (gameData[row2][col] == 0 || col == col2)) {
            if (checkDirectLine(row1, col1, row1, col) &&
                checkDirectLine(row1, col, row2, col) &&
                checkDirectLine(row2, col, row2, col2)) {
                return true;
            }
        }
    }

    // 沿列扩展
    for (int row = 0; row < ROWS; row++) {
        if (row != row1 && row != row2 &&
            (gameData[row][col1] == 0 || row == row1) &&
            (gameData[row][col2] == 0 || row == row2)) {
            if (checkDirectLine(row1, col1, row, col1) &&
                checkDirectLine(row, col1, row, col2) &&
                checkDirectLine(row, col2, row2, col2)) {
                return true;
            }
        }
    }

    return false;
}

游戏优化

功能扩展

1. 添加计时器

// HelloWorldScene.h 添加
cocos2d::Label* timeLabel;
int gameTime;

// HelloWorldScene.cpp
timeLabel = Label::createWithTTF("Time: 0", "fonts/Marker Felt.ttf", 24);
timeLabel->setPosition(Vec2(origin.x + visibleSize.width - 100, 
                            origin.y + visibleSize.height - 30));
this->addChild(timeLabel);

this->schedule([this](float dt) {
    gameTime++;
    timeLabel->setString("Time: " + std::to_string(gameTime));
}, 1.0f, "timer");

2. 添加得分

// HelloWorldScene.h 添加
int score;
cocos2d::Label* scoreLabel;

// 消除时加分
score += 10;
scoreLabel->setString("Score: " + std::to_string(score));

3. 添加音效

#include "SimpleAudioEngine.h"

// 匹配成功音效
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("match.mp3");

// 背景音乐
CocosDenshion::SimpleAudioEngine::getInstance()->playBackgroundMusic("bgm.mp3", true);

4. 添加粒子效果

// 消除时添加特效
auto particle = ParticleExplosion::create();
particle->setPosition(button->getPosition());
this->addChild(particle);
particle->setAutoRemoveOnFinish(true);

本文要点回顾

  • C++不是废物:可以做游戏
  • Cocos2d-x引擎:跨平台游戏开发
  • 坐标系统:左下角为原点
  • 游戏元素:文字、图片、按钮
  • 连连看逻辑:连通判断算法
  • CV大法:复制粘贴的艺术
  • 调试技巧:仔细检查变量

记忆口诀

> C++不废物,游戏做得牛。
>
> Cocos引擎好,界面不再愁。
>
> 头像随机摆,连通算法妙。
>
> CV大法强,天才我最骄。

互动时间

思考题:

  1. 连连看的连通算法有哪几种?
  2. 如何实现游戏胜利判断?
  3. 如何添加重新开始功能?

如果本文对你有协助,欢迎:

  • 点赞支持
  • 关注不迷路
  • 评论区分享你的游戏作品
  • ⭐ 收藏慢慢看

—本文为”C++ 大白话”系列第 27 篇:实战项目

常见问题

Q1:为什么我的按钮这么大?

A:

  • 图片尺寸太大
  • 使用setScale()缩放
  • 提议缩放到0.1-0.3

Q2:如何实现更复杂的连通判断?

A:

  • 0转弯:直线连接
  • 1转弯:一个拐点
  • 2转弯:两个拐点
  • 使用递归或BFS算法

Q3:游戏卡顿怎么办?

A:

  • 减少按钮数量
  • 优化图片大小
  • 使用纹理缓存
  • 减少粒子效果

Q4:如何打包发布?

A:

  • Windows: 生成exe
  • Android: 生成apk
  • iOS: 生成ipa
  • 参考Cocos文档

扩展功能

提示功能

void HelloWorld::showHint()
{
    for (int r1 = 0; r1 < ROWS; r1++) {
        for (int c1 = 0; c1 < COLS; c1++) {
            if (gameData[r1][c1] == 0) continue;

            for (int r2 = 0; r2 < ROWS; r2++) {
                for (int c2 = 0; c2 < COLS; c2++) {
                    if (gameData[r2][c2] == 0) continue;
                    if (r1 == r2 && c1 == c2) continue;

                    if (gameData[r1][c1] == gameData[r2][c2] &&
                        canConnect(r1, c1, r2, c2)) {
                        // 找到可连接的一对
                        buttons[r1][c1]->runAction(Blink::create(2, 4));
                        buttons[r2][c2]->runAction(Blink::create(2, 4));
                        return;
                    }
                }
            }
        }
    }
}

洗牌功能

void HelloWorld::shuffle()
{
    std::vector<int> remaining;
    for (int r = 0; r < ROWS; r++) {
        for (int c = 0; c < COLS; c++) {
            if (gameData[r][c] != 0) {
                remaining.push_back(gameData[r][c]);
            }
        }
    }

    // 随机打乱
    for (int i = 0; i < remaining.size(); i++) {
        int j = rand() % remaining.size();
        std::swap(remaining[i], remaining[j]);
    }

    // 重新分配
    int index = 0;
    for (int r = 0; r < ROWS; r++) {
        for (int c = 0; c < COLS; c++) {
            if (gameData[r][c] != 0) {
                gameData[r][c] = remaining[index++];
                std::string filename = "fan" + std::to_string(gameData[r][c]) + ".png";
                buttons[r][c]->setNormalImage(Sprite::create(filename));
            }
        }
    }
}

总结

从控制台到游戏开发:

1. C++的威力

  • 不仅仅是控制台
  • 可以开发游戏
  • 跨平台能力强

2. 游戏引擎

  • Cocos2d-x简单易用
  • 坐标系统清晰
  • 组件丰富

3. 实战经验

  • 从简单到复杂
  • 调试很重大
  • CV有技巧

你已经制作了自己的第一个游戏!

继续加油,创造更多精彩作品!

C++告别控制台,游戏开发真简单!

© 版权声明

相关文章

2 条评论

none
暂无评论...