请保留-> 【原文: https://blog.csdn.net/luobing4365 和 http://yiiyee.cn/blog/author/luobing/】
是时候实现个有趣的项目了,我选择在UEFI下实现贪吃蛇的游戏。
在Option ROM上直接实现,会很难调试。因此,我首先实现了贪吃蛇的UEFI应用。调试成功后,再将其移植到YIE001上。
1 贪吃蛇框架设计
考虑到代码最终是要移植到YIE001上,我尽量减少所用到的Protocol。主要是在平常开发中发现,不同的主机,在运行Option ROM时能支持的Protocol不大一致。比如在我目前测试用的平台上,就不支持SimplePointer的Protocol(鼠标)。
为简化对程序的思考,我们设计的贪吃蛇由一个个的矩形块组成。主要需要解决的问题包括:
(1) 地图的设计;
(2) 贪吃蛇的数据结构设计;
(3) 如何判断蛇撞墙;
(4) 如何判断蛇咬自身了;
(5) 随机出现蛇吃的食物。
实际上,解决了问题1和2,基本上后续的几个问题都迎刃而解了。
我们把地图、蛇和食物设计成如图1所示:
贪吃蛇本身是由矩形块组成,整个地图也是由矩形块组成。因此,在蛇运动过程中,只需要处理矩形块的色彩变换就可以了。
另外,采用这种设计方式,也很容易判断蛇是否撞墙、是否吃到食物或吃到自身了。直接通过判断其行进方向上的下一矩形块,是否为墙、食物或者自身的矩形块就可以了。
2 代码实现
程序基本上是按照图1的概念图来进行编写的。我们将整个地图的矩形块从左往右、从上往下进行编号,序号从0开始。
因此,可通过矩形块序号或者矩形块的第一个像素坐标(左上角)来判断蛇是否撞墙或咬到自身。实际上,知道了矩形块的序号,就可以计算出其左上角第一个像素的坐标了。这两种方式,在程序中混合着使用,并没有区别。
下面详细解释代码的实现过程。
2.1 数据结构、全局变量和宏定义
所用到的数据结构、全局变量和宏定义如下所示。
#define X_MAP 50 //容纳多少个SnakeBlock
#define Y_MAP 50
#define SNAKEBLOCK 8
#define SNAKEBLANK 2
#define CROSSWALL 1
#define BITESELF 2
#define USEREXIT 3
#define SnakeUP 1
#define SnakeDOWN 2
#define SnakeLEFT 3
#define SnakeRIGHT 4
typedef struct GREEDSNAKE //贪吃蛇的数据结构
{
INT32 x;
INT32 y;
INT32 BlockNumber; //总共X_MAP*Y_MAP个SnakeBlock
struct GREEDSNAKE *next;
}greedsnake;
greedsnake *head,*food; //蛇头指针,食物指针
greedsnake *pSnake; //遍历所用指针
UINT8 foodColor = BLACK;
UINT8 snakeColor = BLACK;
INT32 FoodBlocks[X_MAP*Y_MAP]; //保存可用的SnakeBlock
INT32 FoodBlockCounts;
INT32 SnakeStatus,SleepTime = 200; //运行的延时时间,ms为单位
INT32 Score=0; //成绩,吃到一个食物则加10分
其中,X_MAP和Y_MAP表示X坐标和Y坐标上的矩形块数目。SNAKEBLOCK表示正方形矩形块的边长,SNAKEBLANK为矩形块的外部。在实际的画图中,矩形块是这样绘制的:
计算的时候,是当作一个整体来看待的,数据结构greedsnake中x坐标和y坐标,是指整体矩形块的左上角第一个像素点坐标。
数据结构greedsnake中的BlockNumber和x、y坐标可以相互转换,其转换公式为:
x = ((BlockNumber) % X_MAP) *(SNAKEBLOCK + SNAKEBLANK);
y = ((BlockNumber) / X_MAP) *(SNAKEBLOCK + SNAKEBLANK);
2.2 绘制地图和初始化贪吃蛇
矩形块的绘制,可通过Graphic.c中提供的rectblock()函数来实现。代码中将此功能封装在函数SnakeElement()中实现。
实现地图绘制和贪吃蛇初始化的函数如下所示:
/**
绘制地图
@param VOID
@retval VOID
**/
VOID CreateMap(VOID)
{
INT32 xnum;
INT32 ynum;
INT32 counter=0;
for(xnum=0;xnum<X_MAP;xnum++)
{
SnakeElement(xnum*(SNAKEBLOCK+SNAKEBLANK),0,BLACK);
SnakeElement(xnum*(SNAKEBLOCK+SNAKEBLANK),(Y_MAP-1)*(SNAKEBLOCK+SNAKEBLANK),BLACK);
}
for (ynum = 0; ynum < Y_MAP; ynum++)
{
SnakeElement(0, ynum*(SNAKEBLOCK + SNAKEBLANK), BLACK);
SnakeElement((X_MAP - 1)*(SNAKEBLOCK + SNAKEBLANK), ynum*(SNAKEBLOCK + SNAKEBLANK), BLACK);
}
for(ynum=1;ynum<Y_MAP-1;ynum++)
for(xnum=1;xnum<X_MAP-1;xnum++)
{
FoodBlocks[counter]=ynum*X_MAP + xnum;
counter++;
}
FoodBlockCounts=counter;
}
/**
初始化蛇身
@param VOID
@retval VOID
**/
VOID InitSnake(VOID)
{
greedsnake *tail;
INT32 i;
tail = (greedsnake*)AllocateZeroPool(sizeof(greedsnake));//从蛇尾开始,头插法,以x,y设定开始的位置//
tail->x = (X_MAP/2)*(SNAKEBLOCK + SNAKEBLANK);
tail->y = (Y_MAP/2)*(SNAKEBLOCK + SNAKEBLANK);
tail->BlockNumber = (Y_MAP/2) * X_MAP + (X_MAP/2);
tail->next = NULL;
for (i = 1; i <= 4; i++) //4个SnakeBlock
{
head = (greedsnake*)AllocateZeroPool(sizeof(greedsnake));
head->next = tail;
head->x = (X_MAP/2)*(SNAKEBLOCK + SNAKEBLANK) + i*(SNAKEBLOCK + SNAKEBLANK);
head->y = (Y_MAP/2)*(SNAKEBLOCK + SNAKEBLANK);
head->BlockNumber = tail->BlockNumber + 1;
tail = head;
}
while (tail != NULL)//从头到尾,输出蛇身
{
SnakeElement(tail->x, tail->y,snakeColor);
tail = tail->next;
}
}
贪吃蛇本身采用了链表的方式存储,每增加一个蛇身,就向系统申请一部分内存。因此,在后续的编程中,需要随时注意链表内存的释放。
2.3 撞墙或自咬
如前所述,这可以通过数据结构中的x、y坐标或者BlockNumber来进行判断。代码如下:
/**
判断是否咬到自己
@param VOID
@retval 1 咬到自己
0 没有咬到
**/
UINT8 BiteSelf()
{
greedsnake *self;
self = head->next;
while (self != NULL)
{
if (self->x == head->x && self->y == head->y)
{
return 1;
}
self = self->next;
}
return 0;
}
/**
不能穿墙
@param VOID
@retval 1 穿墙了
0 没有穿墙
**/
UINT8 NotCrossWall(VOID)
{
UINT32 BlockX,BlockY;
BlockX = ((head->BlockNumber) % X_MAP);
BlockY = ((head->BlockNumber) / X_MAP);
if((BlockX==0) || (BlockX==(X_MAP-1)) || (BlockY==0) || (BlockY==(Y_MAP-1)))
{
EndGameFlag = CROSSWALL;
return 1;
}
return 0;
}
自咬是通过x、y坐标来判断的,撞墙则通过BlockNumber来进行判断。
2.4 随机食物
全局变量FoodBlocks、FoodBlockCounts和foodColor都是用来实现产生随机食物的。
FoodBlocks数组中,包含了除墙以外的所有SnakeBlock的序号。由于墙的存在,可是用来产生食物的SnakeBlock不连续了,因此才采用了这个小手段。
而foodColor是用来表示食物的颜色的。在实际程序中,我增加了一个小功能,当贪吃蛇吃到食物后,其本身的颜色会变得和食物颜色相同。
产生随机数的函数为robin_rand(),在之前的博客中已经介绍过了,这是个伪随机数生成器。
生成随机食物的代码如下:
/**
随机出现食物
@param VOID
@retval VOID
**/
VOID RandomFood(VOID)
{
greedsnake *tempfood;
INT32 randNum;
randNum = robin_rand() % FoodBlockCounts; //不能超过食物可出现位置的总数
tempfood = (greedsnake*)AllocateZeroPool(sizeof(greedsnake));
tempfood->BlockNumber = FoodBlocks[randNum];
//递归判断蛇身与食物是否重合
pSnake=head;
while(pSnake == NULL)
{
if(pSnake->BlockNumber == FoodBlocks[randNum]) //重合了
{
FreePool(tempfood); //释放内存
RandomFood(); //递归产生食物
}
pSnake = pSnake->next;
}
tempfood->x = ((tempfood->BlockNumber) % X_MAP) *(SNAKEBLOCK + SNAKEBLANK);
tempfood->y = ((tempfood->BlockNumber) / X_MAP) *(SNAKEBLOCK + SNAKEBLANK);
tempfood->next = NULL;
food = tempfood;
foodColor = (UINT8)(robin_rand() % 10); //共10个颜色可选
if(foodColor == DEEPBLUE)
foodColor = BLACK;
SnakeElement(food->x,food->y,foodColor);
}
2.5 贪吃蛇的移动
贪吃蛇只能朝一个方向移动,其头部的下一个BlockSnake可以为食物、墙或者空BlockSnake。其移动的实现代码为:
/**
蛇的移动
@param VOID
@retval 1 撞到自己或墙了
0 啥事没有
**/
UINT8 SnakeMove(VOID)
{
greedsnake *nexthead;
if(NotCrossWall())
return 1;
nexthead = (greedsnake*)AllocateZeroPool(sizeof(greedsnake));
switch(SnakeStatus)
{
case SnakeUP:
nexthead->BlockNumber = head->BlockNumber - X_MAP;
break;
case SnakeDOWN:
nexthead->BlockNumber = head->BlockNumber + X_MAP;
break;
case SnakeLEFT:
nexthead->BlockNumber = head->BlockNumber -1;
break;
case SnakeRIGHT:
nexthead->BlockNumber = head->BlockNumber +1;
break;
default:
break;
}
if(nexthead->BlockNumber == food->BlockNumber) //找到食物!
{
nexthead->x = food->x;
nexthead->y = food->y;
nexthead->next=head;
head=nexthead;
pSnake = head; //准备遍历
snakeColor = foodColor;
while(pSnake != NULL)
{
SnakeElement(pSnake->x,pSnake->y,snakeColor); //变成食物的颜色
Delayms(50);
pSnake=pSnake->next;
}
Score+=10;
RandomFood();
}
else
{
nexthead->x = ((nexthead->BlockNumber) % X_MAP) *(SNAKEBLOCK + SNAKEBLANK);
nexthead->y = ((nexthead->BlockNumber) / X_MAP) *(SNAKEBLOCK + SNAKEBLANK);
nexthead->next = head;
head = nexthead;
pSnake = head; //准备遍历
while(pSnake->next->next !=NULL)
{
SnakeElement(pSnake->x,pSnake->y,snakeColor);
pSnake=pSnake->next;
}
SnakeElement(pSnake->next->x,pSnake->next->y,DEEPBLUE); //消除,即变成背景色
FreePool(pSnake->next); //释放内存
pSnake->next=NULL;
}
if(BiteSelf() == 1)
{
EndGameFlag = BITESELF;
return 1;
}
return 0;
}
2.6 游戏运行与控制
由于考虑到Option ROM中有可能对Event支持得不好,程序的框架中并没有引入Event机制。程序中通过检查键盘是否按下,获取相应的键码,控制蛇的转向。实现代码如下:
/**
运行游戏
@param VOID
@retval VOID
**/
VOID GameRun(VOID)
{
EFI_INPUT_KEY key={0,0};
SnakeStatus = SnakeRIGHT;
while(1)
{
CheckKey(&key);
if((key.ScanCode==0x01) && (SnakeStatus!=SnakeDOWN)) //UP key
{
SnakeStatus=SnakeUP;
}
else if((key.ScanCode==0x02) && (SnakeStatus!=SnakeUP)) //DOWN key
{
SnakeStatus=SnakeDOWN;
}
else if((key.ScanCode==0x03) && (SnakeStatus!=SnakeLEFT)) //RIGHT key
{
SnakeStatus=SnakeRIGHT;
}
else if((key.ScanCode==0x04) && (SnakeStatus!=SnakeRIGHT)) //LEFT key
{
SnakeStatus=SnakeLEFT;
}
else if(key.ScanCode==0x17) //ESC
{
EndGameFlag = USEREXIT;
break;
}
Delayms(SleepTime);
if(SnakeMove())
break;
}
EndGame();
}
EndGame()函数用来收尾,显示用户的得分。具体实现就不贴出了,博客最后给出了代码的下载地址。
3 测试
代码编译:
C:\UEFIWorkspace>build -t VS2015x86 -p RobinPkg\RobinPkg.dsc \
-m RobinPkg\Applications\Snake\Snake.inf -a IA32
在UEFI Shell下测试,效果如下:
Gitee地址:https://gitee.com/luobing4365/uefi-explorer
项目代码位于:/FF RobinPkg/ RobinPkg /Applications/Snake
1,164 total views, 2 views today