暑假的时候我自己写了个贪食蛇的小程序,觉得效果还好。现在我将自己的思路简单的说一下,我知道好多人都对这个经典的游戏有兴趣的, 我刚开始学编程的时候也是立志自己写一个,这个目标也算是完成了。写我自己的思路,也练练我的表达能力,要是说的不明不白,请多多见谅。下面这个帖子有这个程序的简单介绍和下载地址,可以去看看。《暑假的时候写了个贪食蛇,可单人或者双人玩》,http://www.programfan.com/club/showbbs.asp?id=188529
也是我发的,要是地址有错误可以搜索。
首先要解决的一个问题是如何来描述蛇的状态。蛇总是在一些小方格上面爬行的,假如横数有cxNumberOfRect个小格, 竖数有cyNumberOfRect个,就共有cxNumberOfRect*cyNumberOfRect个小格。对于每一个小格,经过简单的影射,可以用一个POINT来表示,也可以用一个二维数组的一个元素来表示。而蛇身是由一系列的方格来组成的,自然也可以用一系列的点,或者一个二维数组来描述。
先看第一个方法,就是用一系列的点来表示,其实不用组成蛇身的每个点都记录下来(点对应了方格), 只要记录下关键的点就可以的了. 什么点关键的呢, 自然是头, 尾, 和全部拐弯点. 光知道这些关键点还不够, 蛇爬行的时候还有一定的方向, 尾所在的点也有一定的方向, 在尾遇到拐弯点的时候, 方向也要改变, 所以拐弯点也要有一定的方向, 不然尾就不知道转向哪里了. 这时候, 就知道, 应该要有一个结构来记录每个点的状态, 一个元素是坐标, 可以是POINT类型, 一个用来记录方向, 可以是UINT类型, 我自己是这样定义的
#define DS_UP 0x0001
#define DS_DOWN 0x0002
#define DS_LEFT 0x0003
#define DS_RIGHT 0x0004
struct SNAKE_TURNING_POINT
{
POINT ptTurning; //拐点坐标
UINT Direction; //拐弯方向
};
那几个#define语句是让数值直观, 这是宏的重要用法, 当然也可以用const定义一些常量.
再接着分析, 当蛇要转弯的时候, 头的方向就会改变, 这个时候就会产生一个拐弯点, 当尾遇到一个拐弯点的时候, 尾的方向就改变, 那个拐弯点就不存在了. 简单分析就知道, 最先产生的拐弯点, 也最先消失, 这时候就好自然地想到用一个队列来储存拐弯点了. 所以我们的CSnake类中就应该有个 queue<SNAKE_TURNING_POINT> m_qTurning; 名称自然可以改成其他的. 可能会想, 蛇头和蛇尾也用这个队列储存起来行不行呢? 不行的. 因为头, 尾每时每刻都会变, 自然每时每刻都用修改, 你放在队列中, 就不可以随时取出来. 这个时候, 自然CSnake中也会有变量SNAKE_TURNING_POINT m_Head; 和 SNAKE_TURNING_POINT m_Rear; 根据这个队列和这两个变量, 就可以知道蛇的状态, 也可以好容易的修改蛇的状态.
现在看第二种方法, 用二维数组. 大家都应该想到的了, 就是定义一个bool m_State[cxNumberOfRect][cyNumberOfRect], 数组元素中为1表示蛇占了这个方格(数组元素对应了方格), 为0表示没有占用. 用数组直观, 不过有个最大的缺点, 就是修改蛇的状态时候, 会非常之困难, 要历遍整个数组, 做很多个判断, 而每时每刻蛇的状态都要改变, 整个程序的效率就会很差. 在我自己的CSnake类也有这样的二维数组, 主要是用来判断死亡的情况, 还要产生新的豆的坐标(我叫豆的, 可能有些人叫青蛙之类), 其实也可以不用数组的, 只是这时候, 判断起来要难点. 判断死亡条件后面会慢慢说到的.
有了上面的结构,和队列, 实现转左, 转右, 转上, 转下的功能就好简单了,这时只不过是简单修改头的方向, 跟着将头m_Head放到队列中就可以了, 因为转弯时候, 这个点也成为拐弯点.
蛇前进的功能也不难. 先看m_Head的变化, 我们已经知道了方向, 知道了原来的坐标, 就自然好容易的知道前进之后的坐标, 而方向是不变的.考虑尾, 也就是m_Rear的变化, 也容易得到新的坐标点, 不过方向就要考虑队列的最前面的元素的, 要是这个时候尾的坐标和这个元素的坐标相同, 说明尾部要拐弯了, 尾的方向也要改变了. 这时候, 原来的拐点也不再成为拐点了, 就从队列中弹出, 其他情况下尾的方向也步会改变.(题外话: 有些“聪明”人说学数据结构没有用,我自己是坚决反对的)
吃豆功能只不过是将尾部的坐标修改成之前的坐标就可以了, 也不难. 你想想头坐标不变, 尾坐标改成之前的, 蛇就加长了.当蛇头坐标等于豆的坐标的时候就调用这个功能。
跟着就要考虑死亡条件了, 碰到自己的身体就会死亡. 蛇每前进一步都做出判断, 我前面说过定义了一个bool类型的数组, 这个时候就发挥了作用, 这时候就看看头所在的坐标对应的数组元素, 要是为1表示蛇已经占用了这个格子, 就死亡了. 知道坐标, 再访问数组是好容易的, 方格, 坐标, 数组元素都存在一一对应的关系. 自然这个时候, 每前进一步也要修改数组的值, 只是简单的将新头部对应的元素变1, 旧尾部对应的元素变0就可以了. 死亡的时候还要判断边界, 碰墙壁也会死亡, 可以用>, <边界值来判断. 其实也可以采用另一种方法, 就将我们的数组加大, 就好象在四周围了栏杆, 将靠边的元素初始为1, 这个时候几个判断,就会变成一个判断. 相应地, 我们的数组应该定义成
bool m_State[cxNumberOfRect+2][cyNumberOfRect+2]. 这样的小程序多用一点空间也没有什么所谓.
吃豆之后要产生新的豆坐标, 豆的坐标不能够产生在蛇的身上, 这时候我们的二维数组又要用到了, 因为数组记录了蛇占用的格子. 豆的坐标要有随机性, 所以要用到线性同余来产生随机数, 至于随机数的产生原理, 不在本文的讨论范围. 好多人会想到先产生x坐标, 再产生y坐标, 要是那个坐标对应的数组元素已经为1了, 就再产生一次, 一直到那个元素为0为止. 我开始的时候也是这样想的. 不过这样有个问题, 要是蛇已经很长, 很多数组元素的值为1了, 就要尝试很多次, 要是总共能有1000个格子, 蛇已经占了500个格子, 就要碰运气了. 我是这样做的, 先产生从0到空格数目(没有被蛇占用的格子数)之间的随机数, 假设为X, 跟着从第一个空格开始数, 数到第X个空格, 那个空格就是新的豆所在的方格, 格子和坐标对应, 就可以得到豆的坐标, 这样的简单算法总是可以得到坐标的, 就算是蛇已经很长了, 效率也不会降低到不可忍受的地方, 也不靠碰运气。(题外话:写程序的时候切忌让程序碰运气,要尽你可能考虑出每种可能发生的情况并做出相应处理,就算是发生的几率很少。不要偷懒,认为很少发生就让它就这样过去,要是你这样做,你的程序就会有缺陷。当然有缺陷是很难避免的,不过要尽可能的使缺陷减少到最低)。
对于单人游戏的贪食蛇,规则是怎么样的就不用介绍了。可能会有人问,双人游戏怎么玩啊?我是这样设规则的。玩家一和玩家二都可以吃和自己颜色相同的豆,所以屏幕上会有两种颜色的豆。当其中一个人吃了豆之后,对方会加长,就是调用对方的EatBean函数,跟着两人豆的位置就重新改变,调用CalBeanPos函数。那个人先死就输,而不是看那个吃的豆多。假设Player1吃了100个豆,Player2吃了1个豆,要是Player1先死,也是Player1输,要是同时死亡,就平局,也不看吃豆的多少。容易知道,贪食蛇中越长越容易死亡,可以说吃豆是使对方先死的手段而不是目的。我觉得这样设定规则也算是合理的。当然不同的人想法也不同。两人在屏幕上玩的时候,各人有各人的颜色,要是交叉重叠,重叠的地方就显示成叠加后的颜色,和两人的颜色都不同,离开的时候再恢复。所以看起来会有一条蛇在另一条蛇身上穿行的效果。
上篇就写到这里,主要讲的是如何描述蛇的状态和行为,对于不同的语言,不同的系统,其中的思想都适用。本文的下篇就主要讲蛇的画法,也就是在windows下,屏幕上面显示。
跟着要说显示的问题了, 虽然的前面说的已经可以描述蛇的行为和状态了,不过要在屏幕上显示出来也要有点技巧。在windows中有个函数是Rectangle, 是画矩形的,蛇身可以用这个函数画出来,因为蛇身是由方格,也就是矩形组成的。知道坐标,对应成方格,对应成矩形,跟着调用函数就可以了。那是不是每一次蛇前进的时候都要将组成蛇身的矩形都画一次呢?其实不用的, 蛇行走的时候, 改变的只不过是头和尾,就是画出新的头, 去掉原来的尾, 由于视觉停留,看起来蛇就会动了,这样做效率会比较高, 因为只要重画两个矩形, 也不会有一闪一闪的现象.另外,要是将窗口拖出屏幕,看不见了, 就要将整个蛇身重新画一次, 这种画法也不要采用判断整个数组的元素的值为1就画,为0就不画的方法。因为数组是比较大的,判断起来也费工夫,可以将队列的元素复制到另一个队列,加上头和尾,得到关键的点,就画两个关键点的之间所有方格就可以了,他们总是水平或者是垂直的.具体实现起来也不难,就不多说。本文是讲思路而不是讲具体实现的,不然篇幅会更长.
那怎么画头去尾呢? 假设蛇是红色的,背景是白色的, 好多人大概都会想是选红的画刷,画头, 跟着选白的画刷, 画尾, 因为尾的颜色和背景颜色一样,就看不到了。这样想本来是没有错误的,也是最自然的想法,对于单人的贪食蛇游戏就可以了。不过就如本文标题说的,我们要写的是双人玩的贪食蛇,这样做就会产生问题。双人玩的时候,自然会产生两个CSnake类,在屏幕上要产生两条蛇,蛇身要用不同的颜色来区分,当不互相碰撞的时候,显示不会有问题。当相碰的时候,就会有叠加的方格,这些方格要显示什么的颜色呢?当然要是产生和玩家1,玩家2的颜色都不同的颜色就最好,就算和其中之一相同也没有什么大错。真正的问题是,按照前面做法,当一条蛇和另一条蛇相碰并离开的时候,或者一条蛇在另一条蛇身上穿行的时候,因为去尾是刷成白色,就会令到其中一条蛇,有的时候甚至是两条蛇都会出现断裂,也就是一边是红色了,另一边也是红色,中间却没有了,因为变成了背景色,也就是白色了。这样显示出来会很难看。
对于相碰显示的问题,也可以避开它,可以设计成碰到对方的身体也会死亡的(这个时候玩起来要更加小心,我觉得也会小点乐趣)也可以不避开它,而采用判断,当判断得出重叠时候显示另一种颜色,离开时候再判断,恢复成恰当的颜色。两种做法都要得到对方的状态,也就是说CSnake中必须有些Static的成员数据,或者是有可以得到状态的函数。做判断的也会很复杂,不利于CSnake类的封装。在考虑更极端的情况,要是我不是两个人玩,我是要四个人一起玩,这种写法会令你发狂, 根本不利于程序功能的扩充。
那要如何解决呢?在Windows画图的函数中,有个函数是SetROP2,可以设置光栅操作,平时默认的是R2_COPYPEN,设置这个状态的时候,画图函数的颜色就是画笔或者画刷的颜色。要是设置成R2_NOTXORPEN, 有个特殊的用法,这时候用画图函数画一次,出现图象,再用原来的函数,在原来的地方再画一次,就又变成原先屏幕上的颜色,原先是红就是红,原先是绿就是绿。R2_NOTXORPEN的光栅操作是 not(像素 xor 画笔),这里像素是原先屏幕颜色,画笔不单是画笔也是画刷的颜色,运算的结果就是新的屏幕颜色。(颜色在计算机里面也是用0和1来表示的,所谓的运算是位运算). 让我们简单分析一下为什么用这种运算下画两次可以恢复原来屏幕的颜色。用A表示屏幕像素,B表示画笔,画一次屏幕像素为!(A xor B) 再画一次是屏幕像素就是!( !(A xor B) xor B)化简这个式子,也可以用真值表来判断得到 A,也就是原先的屏幕颜色了。因为我们的开始时候的背景色是白色的,颜色对应的数值都为1,看看!(1 xor B)会为B, 也就是说在背景色为白色的情况之下,画一次会是画刷的颜色,画两次就恢复成背景色。用这种光栅操作之下,就不用考虑对方的状态,要是相交的时候,颜色也会叠加,产生一种新的颜色了,离开的时候也会自动恢复。
上面那段话可能有点费解,可以参考SetROP2函数的用法和一些关于颜色,光栅的基本知识,不再多说了。所以现在我们需要画头的时候,是在这种光栅操作下调用Rectangle,需要去掉尾部时候,也就选取同样的刷子在原先尾部的位置上调用Rectangle.这样做就不再需要考虑各个玩家的状态了,要是写四个人一起玩也是一样的,颜色也自然会叠加. 只不过没有这样多的键盘位置。这时候要是懂一点网络知识的话完全可以写出一个多人联网玩的贪食蛇游戏。我相信不久会也会有些公司会开发出这样的游戏,就好象四国军旗一样,分成两队比赛。
本篇最初开始的时候说了画蛇应该有两种画法,一种是画头去尾,一种是整个蛇身全部重画一次,定为成员函数
void DrawA(HDC hdc); //画头去尾
void DrawB(HDC hdc); //全部重画一次
那什么时候要调用那一个呢?知道DrawA的效率会很高,正常情况下就是调用这个画法,就是响应WM_TIMER消息时候.当窗口拖出屏幕,再拖回来,或者最小化跟着恢复就需要调用DrawB,实现的时候,可以定义一个标志值做出判断, 采用不同的画法. 要是老是用DrawB,屏幕会很有闪烁的。
看到这里,相信有人就知道这个游戏应该怎么写出来的了。
我自己的程序中,还有两个功能是可以选择穿墙,可以选择向后退。穿墙就容易啦,就是在前面分析的基础上,当头的坐标已经碰到一边的墙了,就将它坐标设置成另一边的,尾也一样,自然就会穿过墙壁了。穿墙和不穿墙可以设置一个标志值判断,程序在响应菜单选择时候,就修改这个标志值。至于后退,要难点。你拿个笔来画一下,就会发现头和尾的坐标要改变,拐弯点在队列中位置也正好相反,方向和原来的方向也会有一定的相反的位置关系,对于反转元素的位置,自然容易借助于一个堆栈来实现。看看,那些“没有用”的数据结构又发挥作用了。
整个构思简单回顾一下,蛇的很多行为都是建立在一个队列和头,尾状态的基础之上的,只要改变这些值,蛇就会呈现不同的状态和行为。自然也要用到那个二维数组,其实它不是必要的,通过一些手段,可以将它去掉,就可以省去不少空间,只是实现起来也麻烦一点。对于个人计算机这点空间不算什么,要是对于内存紧张的情况,比如搬到手机上时,就要重新考虑。
这里还有一个细节问题。通常对于这样的有动画效果的程序,都会设置一个定时器,经过一定的时候发送一个WM_TIMER消息。在我自己的程序在也有WM_TIMER消息, 主要是调用GoAhead(向前走), 这个是好显然的, 另外还要处理键盘按键, 主要是调用TurnUp(转上), TurnDown(转下), TurnLeft(转左),TurnRight(转右)来改变方向. 这时候处理WM_TIMER和WM_KEYBOARD的时候就要注意了. 因为WM_TIMER是要隔一定时间才能处理的(因为这个消息是隔一定时间才发送的),键盘却可以随时按,要是按键很快的时候,好可能在再处理一次WM_TIMER期间, 已经产生了不止一次的WM_KEYBOARD消息, 要是每个WM_KEYBOARD都让蛇转弯, 在WM_TIMER消息处理中还没有调用GoAhead的情况下, 考虑我们前面队列的实现方法,就相当于在同一坐标中放了好多拐弯点进队列. 这时候当然程序就会出错. 开始我写这个程序的时候, 就是出现这样的问题, 让我找了一个下午. 所以我再定义了一个标志值F_ENABLEPLAYER1, 在每次处理完WM_TIMER消息后将标志值设置为1, 在每次处理完WM_KEYBOARD消息后将标志值设置为0, 在标志值为0的情况下, 将WM_KEYBOARD消息丢弃, 不做转弯处理, 这样之后,问题就解决了. 这个也应该算是同步问题吧. 在双人游戏下, 可以再设置一个F_ENABLEPLAYER2, 原理同上。速度选项只不过是修改定时器的设置,让它发送消息的时间间隔改变就可以了。
程序中可以两个CSnake类型的指针,Player1和Player2,初始为NULL。程序刚运行的时候就用Player1 new出一个类。当选择双人游戏的时候,Player2也new 出一个类,再选择单人的时候就delete Player2,跟着Player2重新设置成NULL,这样就可以用Player2是否等于NULL,来判断出是否双人游戏,来做出各自的处理了。
至于我自己程序的其它一点小功能, 好象声音, 颜色, 语言之类, 只不过是设置相应的数值和标志值,在进行进行判断, 只要花点时间, 花点耐心去做,就自然可以写出来。 也没有什么好说的.
这个帖子就写到这里, 也挺长的了, 多谢大家耐心的看完(我想没有看完的, 也看不到我多谢他了)。 要是还有什么问题也可以回帖提出, 要是有人已经得到了源代码, 这个帖子也可以算是对代码的一个注释。(完)
正文
写一个贪食蛇的思路,可以双人玩的 转2007-03-21 12:34:00
【评论】 【打印】 【字体:大 中 小】 本文链接:http://blog.pfan.cn/12567/24157.html
阅读(3284) | 评论(0)
版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!
评论