实战项目:粉丝连连看游戏
> 老子能搞游戏!
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)
游戏元素
三个基本元素
默认场景中有:
- 文字 – Label
- 图片 – Sprite
- 按钮 – 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大法强,天才我最骄。
互动时间
思考题:
- 连连看的连通算法有哪几种?
- 如何实现游戏胜利判断?
- 如何添加重新开始功能?
如果本文对你有协助,欢迎:
- 点赞支持
- 关注不迷路
- 评论区分享你的游戏作品
- ⭐ 收藏慢慢看
—本文为”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++告别控制台,游戏开发真简单! ✨