UEFI开发探索78- YIE001PCIe开发板(11 贪吃蛇)

请保留-> 【原文:  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所示:

图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为矩形块的外部。在实际的画图中,矩形块是这样绘制的:

图2 矩形块的绘制

计算的时候,是当作一个整体来看待的,数据结构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下测试,效果如下:

图3 运行贪吃蛇游戏

Gitee地址:https://gitee.com/luobing4365/uefi-explorer
项目代码位于:/FF RobinPkg/ RobinPkg /Applications/Snake

1,029 total views, 2 views today

发表评论

电子邮件地址不会被公开。