2

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

 2 years ago
source link: http://yiiyee.cn/blog/2021/02/14/uefi%e5%bc%80%e5%8f%91%e6%8e%a2%e7%b4%a278-yie001pcie%e5%bc%80%e5%8f%91%e6%9d%bf%ef%bc%8811-%e8%b4%aa%e5%90%83%e8%9b%87%ef%bc%89/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

请保留-> 【原文:  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()函数用来收尾,显示用户的得分。具体实现就不贴出了,博客最后给出了代码的下载地址。

代码编译:

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-exolorer
项目代码位于:/FF RobinPkg/ RobinPkg /Applications/Snake

148 total views, 1 views today


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK