写在前面
用C++语言写游戏再适合不过了,当然不是因为用它写起来简单,(相反那并不简单),但是其性能绝对是其他语言没法比的。所以这里我会用C++实现一个贪吃蛇的游戏。当然我可能有意隐瞒了你,因为我们不仅仅是用C++纯语言来干这件事,那会很别扭,因为我们需要图像渲染、声音、甚至是碰撞检测(我最喜欢的一个版块)!所以仅仅用语言是不够的。
(注:在文章最后我会给出两个版本的贪吃蛇源码及涉及到的一些资源)
写在最前面就是为了说明我们会用其他的一些工具:DirectX(9.0)、Windows的窗口编程,这些真的没那么简单!如果你之前没听说过这些,也不要太过于担心,因为我主要是介绍贪吃蛇实现的核心逻辑,严格的说,你可以当成数据结构的知识来学,因为整条蛇是以链表为基础的!
另外,我用纯C语言也实现过一个贪吃蛇的玩意(如果你觉得是的话),先看看游戏的运行效果:
贪吃蛇版本1:
不要小瞧它!它有音乐,也有碰撞,虽然体验实在是不咋滴,不过他的游戏编写过程和游戏元素的构成还是对之后进一步编写更棒的游戏提供了十足的基础。因为他是用纯语言做的,不需要其他库等等的支持,所以很适合我们学习借鉴!
至于第二个版本的贪吃蛇就有很大的改变,尽管还有很多地方需要改进和优化,但是他已经超越了第一个版本很多!下面看看游戏运行效果:
贪吃蛇版本2:
是不是有摆脱Dos找到新大陆的感觉,他加入了新的计分模块。下面我就第二个版本的核心实现做出解释。
版本二游戏核心代码实现
1,蛇身的单个节点实现:
//蛇身单个节点
struct SNAKE {
bool IsSurvivor; //当前结点是否存在(被画)
int coor_x; //节点横坐标
int coor_y; //节点纵坐标
SNAKE *link; //指向下一个节点的指针
//构造函数
SNAKE(int x, int y, bool survivor = true,SNAKE *link = NULL) {
//初始化坐标值,赋值方式为tail派生
coor_x = x;
coor_y = y;
}
};
的确,一个十分明白的结构体,我无需做出任何解释!
2,蛇的整个类实现(基于链表)
//蛇精灵类定义——基于单链表实现蛇身
class SnakeSprite {
public:
SnakeSprite(int x = 300, int y = 200);
~SnakeSprite() { delete snakeHead; }
bool addTail(); //蛇尾增加长度
void drawThisSnake(); //绘制当前蛇身
void positionAction(); //完成蛇身移动(更新每个节点的坐标)
void turnLeft(); //蛇头的基本转向
void turnRight();
void turnUp();
void turnDown();
void recordCurrentDirection(int d = LEFT); //记录蛇的当前运动方向,借助枚举类
int getDirection();
bool IsDeath(); //是否碰撞草丛,是蛇死亡返回true,否则返回false
void getCurrentPosRect(RECT &rect);
void getCurrentCoor(int &x, int &y);
protected:
int len; //蛇身长度_以块为单位
SNAKE *snakeHead; //蛇头指针
SNAKE *tail; //蛇尾指针
SNAKE *beforeTail; //尾巴节点的前一个节点,方便移动
int directions;
};
似乎也没什么特别之处,但是有几个地方需要注意,我会在下面着重强调。
3,部分函数实现的解释
我想强调的就在这里,贪吃蛇整个游戏的确简单,但是真正编写的时候则需要考虑全面,因为游戏的逻辑还是特别强的。
1》整个蛇动起来的立足点:
我们必须记住,游戏中的蛇并不是你想象的那样在随着你的控制而‘游动’,他是电脑在以飞快的速率刷新屏幕,而你只是改变了蛇的节点坐标,而人的眼睛是存在视觉暂留的,这样就会给你一种游戏精灵在走动的效果!
2》怎样用键盘控制蛇?
//输入控制
if (Key_Down(DIK_UP) && !Key_Down(DIK_RIGHT)
&& !Key_Down(DIK_LEFT) && !Key_Down(DIK_DOWN)) {
theSnake.turnUp();
if (DOWN != theSnake.getDirection()) {
theSnake.recordCurrentDirection(UP);
}
}
if (Key_Down(DIK_RIGHT) && !Key_Down(DIK_UP)
&& !Key_Down(DIK_LEFT) && !Key_Down(DIK_DOWN)) {
theSnake.turnRight();
if (LEFT != theSnake.getDirection()) {
theSnake.recordCurrentDirection(RIGHT);
}
}
if (Key_Down(DIK_LEFT) && !Key_Down(DIK_UP)
&& !Key_Down(DIK_RIGHT) && !Key_Down(DIK_DOWN)) {
theSnake.turnLeft();
if (RIGHT != theSnake.getDirection()) {
theSnake.recordCurrentDirection(LEFT);
}
}
if (Key_Down(DIK_DOWN) && !Key_Down(DIK_UP)
&& !Key_Down(DIK_RIGHT) && !Key_Down(DIK_LEFT)) {
theSnake.turnDown();
if (UP != theSnake.getDirection()) {
theSnake.recordCurrentDirection(DOWN);
}
}
//蛇类的成员函数
void SnakeSprite::turnDown() {
//向下转头
if (directions != UP) {
snakeHead->coor_y += 1;
}
}
void SnakeSprite::turnLeft() {
//想左转头
if (directions != RIGHT) {
snakeHead->coor_x -= 1;
}
}
void SnakeSprite::turnRight() {
//向右转头
if (directions != LEFT) {
snakeHead->coor_x += 1;
}
}
void SnakeSprite::turnUp() {
if (directions != DOWN) {
snakeHead->coor_y -= 1;
}
}
应该是你想象的那样,我每检测到玩家按下相应的方向键,我会调用snake class的转弯的成员函数(这就是用class的好处,多么统一的代码!),然后紧接着判断玩家是否企图直接来个180°的大逆转(这在贪吃蛇游戏中是违背规则的),如果真的是这样我就在函数中不做任何处理,玩家休想达到这种阴谋!但是如果是合法的转弯(也就是90°),我会改变头结点(就是蛇的头部)的坐标变化趋势,就是代码中那样做。这样蛇就任我们控制摆布了。
3》怎样实现蛇的移动?
//蛇类的成员函数
void SnakeSprite::positionAction() {
//实现蛇的自动运动,即依次更新每个节点内坐标的值
if (UP == directions) {
snakeHead->coor_y--;
}
if (DOWN == directions) {
snakeHead->coor_y++;
}
if (LEFT == directions) {
snakeHead->coor_x--;
}
if (RIGHT == directions) {
snakeHead->coor_x++;
}
SNAKE *current = snakeHead;
int LEN = len;
for (int i = 1; i < len; i ++) {
current = snakeHead;
for (int j = 1; j < LEN - 1 && len >= 3; j ++) {
//令current循环到指定位置
current = current->link;
}
current->link->coor_x = current->coor_x;
current->link->coor_y = current->coor_y;
LEN--;
}
}
这是一个相当重要的功能,因为只有可以动起来才有游戏的感觉。就是上面这个简单的函数实现了蛇的移动,他在主函数中是循环调用的,所以他的核心功能就是改变蛇头节点的坐标,让蛇头节点可以沿着当前的运动方向一直移动下去。你也许会问,那蛇的身子是怎么跟着蛇头运动的呢?那就是函数中最后的一个双重循环,内层循环会通过一个指针沿着蛇身链表找到蛇的尾巴的前一个节点,然后把此节点内的坐标值给尾巴节点,这样就实现了尾巴‘跟着动’的效果。第二次进入后内层循环会找到蛇尾巴前一个节点的前一个节点,然后把他的坐标给了尾巴的前一个节点,这样倒数第二个节点也跟上了!之后便一直重复上述循环,其实就是在用每个节点的坐标来刷新其后一个节点的坐标,这样不就让每一段蛇身都与蛇头形影不离了吗!如果还是感觉理解上有困难,可以看看下面的模拟图:
这里应该十分注意赋值的顺序!,我为何要‘多此一举’地用循环先找到倒数第二个节点,而不是直接从头部开始,因为那样会让蛇的坐标提前丢失,导致我们没法把真正有效的坐标值更新到对应的节点中,从而只看到蛇头在移动!不信?你可以在本子上比划比划。
4》蛇的死亡碰撞事件检测!
bool SnakeSprite::IsDeath() {
//判断是否超出规定范围 67coor_x > 67 && snakeHead->coor_x < 460
&& snakeHead->coor_y > 87 && snakeHead->coor_y < 470) {
return false;
}
else {
return true;
}
}
嗯,这取决于你在屏幕上蛇的移动场地面积的尺寸,当检测的某个方向的坐标超过了对应方向上的场地长度,那就GAMEOVER吧!
食物类的实现代码:
//FOOD CLASS
class Food {
protected:
int coor_x; //食物出现的横坐标
int coor_y; //纵坐标
public:
Food(int x = 100, int y = 100);
bool drawThisFood(bool &again); //绘制当前食物
bool checkFoodPosition(); //检查当前食物出现的位置是否合法(即不能与蛇体重合)
void getRandCoor(int & x, int & y); //食物的随机坐标生成
};
//APPLE CLASS
class Apple : public Food { //苹果是Food的一种
private:
int color; //扩展功能,标定当前Apple的颜色
public:
bool beenCollision(RECT snakeRect); //检测apple是否被碰撞到,是返回true否则返回false
void getCurrentPosRect(RECT &rect); //得到当前的位置矩形
//bool beenCollision2(int x, int y);
};
Food::Food(int x, int y) : coor_x(x), coor_y(y)
{}
void Apple::getCurrentPosRect(RECT &rect) {
RECT currentRect = {coor_x, coor_y, coor_x + 19, coor_y + 22};
rect = currentRect;
}
bool Apple::beenCollision(RECT snakeRect) {
RECT rect_apple, rect;
getCurrentPosRect(rect_apple);
if (IntersectRect(&rect, &rect_apple, &snakeRect)) {
return true;
}
else {
return false;
}
}
bool Food::drawThisFood(bool &again) {
//绘制当前食物到屏幕
int posX , posY ;
if (again) {
getRandCoor(posX, posY);
again = false;
}
RECT rectApple = { 0, 0, 19, 22 };
D3DXVECTOR3 position(coor_x, coor_y, 0);
D3DCOLOR red = D3DCOLOR_XRGB(255, 255, 255);
spriteoj->Draw(apple, &rectApple, NULL, &position, red);
return true;
}
void Food::getRandCoor(int & x, int & y) {
//指定范围内的随机函数生成器
srand((unsigned)time(NULL)); //随机种子,以系统时间作为基数
x = foodAllowPosX + (rand() % 350);
y = foodAllowPosY + (rand() % 330);
coor_x = x;
coor_y = y;
}
bool Food::checkFoodPosition() {
//检查食物位置的合法性
return true;
}
这里我用FOOD作为基类,然后用APPLE来继承它,这主要是想在以后扩展这个游戏的时候加入一些新的玩法,让每种不同的食物都有自己各自属性和反应事件。
5》食物的随机位置产生
void Food::getRandCoor(int & x, int & y) {
//指定范围内的随机函数生成器
srand((unsigned)time(NULL)); //随机种子,以系统时间作为基数
x = foodAllowPosX + (rand() % 350);
y = foodAllowPosY + (rand() % 330);
coor_x = x;
coor_y = y;
}
这个成员函数用了随机数生成器来产生指定区间内的坐标,并把这个坐标当做食物出现的坐标。因为屏幕是在无休止刷新的,所以食物的擦除就不劳我们费心了。
6》其他
我在这里只是调了一些关键的地方作了阐述。其他还有琐碎的地方都需要一块块完善,但是都相对简单。至于将蛇绘制到屏幕上,这是件麻烦事!我不能展开讲,我的水平也不敢讲,但是这真的会令你沮丧,如果你只是看某些实现逻辑而其他的可以自己搞定,那么上面的解释还是挺有帮助的;如果你是个新手,那就会觉得知道逻辑和流程却无法把他们绘制到屏幕上,似乎是本我狠狠的放了鸽子。
也许不必那么沮丧!因为我讲了你也未必能懂(哈哈),你有其他途径可以实现自己的贪吃蛇游戏:
(1)实现纯语言版本的,没错,就是在那个黑黑的Dos框里的,因为他的实现相对简单,关键是他避免了图形渲染和Windows的窗口创建!这真的是一个不错的入门Demo。你还可以在网上找一些关于他的代码来提高自己的开发效率,如果你仍然感觉逻辑上有困难,那么我也会尽快整理出他的写作思路~
(2)学习一些图形渲染的库和工具(如DirectX),抑或是一些简单的游戏引擎,如果那样的话,你真的会瞧不起我做的这个贪吃蛇。再不就看看我提供的两套源码吧!
4,不足之处
上面的代码尽管解决了一些核心的游戏逻辑,但是依然存在不足。计分系统由于食物出现的位置不当而暴增、食物万一出现在蛇身上怎么办?而我在食物位置的合法性检查上只是返回了个字面量true!希望我们一块交流和改进。
5,资源链接
说明:你休想直接复制粘贴上面的代码块来放在编译器里运行它,并且天真的等待游戏画面的出现,因为我早说过这些需要相应工具的支持!你甚至不能运行起版本2的EXE程序,因为可能需要DirectX的游戏环境,而恰好你的机器上没有!但是版本1的贪吃蛇是可以运行起来的,源码也是可以编译的(如果你的编译器正常的话!),因为他真的是用纯语言做的,当然性能也会有不足。
贪吃蛇版本一资源链接:http://pan.baidu.com/s/1c2EUVc8 密码:na6m
贪吃蛇版本二资源链接:http://pan.baidu.com/s/1hsb7k92 密码:6sbg
想一起解决编程中遇到的麻烦吗?想一起学习独立游戏开发吗?想找一群志同道合的朋友吗?想找到自己关于计算机真正的兴趣所在吗?那就加入我们吧!(老学长公众号刚刚开通不久,每隔3天会发表一篇有质量的文章,希望大家多支持!)
6,版权声明
游戏中的音乐、图片、图标等资源均来源于网络,仅供学习之用。
借鉴数目:《游戏编程入门》、《Windows游戏编程大师技巧》
最后谢谢大家可以看我分享的一些经验,这些都是在项目过程中遇到的麻烦,希望大家可以收获到一些知识,少走点弯路!