在linux環境下,利用對framebuffer機制 和 tcgetattr與tcsetattr函數控制終端 來實現貪喫蛇遊戲。
對於framebuffer和tcgetattr與tcsetattr函數的具體原理機制可以參考網上的其他資料。這裏利用framebuffer來實現遊戲的圖像顯示,通過tcgetattr與tcsetattr函數來實現鍵盤終端的控制,從而實時讀取鍵盤的按鍵。
在開始編碼之前要先有一個整體的思路,設計一個遊戲的框架,然後再去對每一部分進行實現。
首先要做的是設計一個遊戲區域,其實就是一個二維空間。這裏我規劃了一個20×20的空間,蛇和食物都是以空間的每一個節點爲單位來構造的,利用對應二維空間的座標來記錄蛇和食物的位置。
//食物數據
typedef struct Food
{
int x;
int y;
}F;
//蛇的數據
typedef struct Snake
{
int x;
int y;
struct Snake *next;
}S;
以上是用來存儲蛇和食物信息的結構體,食物同時只會存在一個,而蛇是由多個節點組成的,所以用鏈表來存儲。畫圖時再通過座標對應到相應的像素點上就可以了。
那麼下面來具體說下畫圖的實現方法。這裏要利用對framebuffer的操作,來設置每一個像素點的信息。在主函數利用下面的代碼。
//framebuffer初始化
struct fb_var_screeninfo vinfo;
int fd = open("/dev/fb0", O_RDWR);
if(fd < 0)
{
perror("open err. \n");
exit(EXIT_FAILURE);
}
int fret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
if(fret < 0)
{
perror("ioctl err. \n");
exit(EXIT_FAILURE);
}
//用來知道自己屏幕的分辨率
printf("vinfo.xres: %d\n", vinfo.xres);
printf("vinfo.yres: %d\n", vinfo.yres);
printf("vinfo.bits_per_pixel: %d\n", vinfo.bits_per_pixel);
//定義指針來操作framebuffer
unsigned long* addr = mmap(NULL, (vinfo.xres*vinfo.yres*vinfo.bits_per_pixel)>>3,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
下面是需要的頭文件和宏定義。
#include <stdio.h>
#include <stdlib.h>
//framebuffer相關頭文件
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
//宏定義設置RGB,以便通過設定 r,g,b參數來設置顏色
#define RGB888(r, g, b) ((0xff & r) << 16 | (0xff & g) << 8 | (b & 0xff))
通過上面的方法得到vinfo.xres和vinfo.yres,就可以知道自己屏幕的分辨率。我的電腦是1376×768的,然後就可以通過addr來設定每一個像素的點的顏色了。下面以畫一個矩形爲例。r,g,b分別是紅綠藍三個顏色,根據值的不同可以調成不同的顏色,我畫的是藍色的。雖然測得屏幕分辨率是1376×768,但實際設定像素的時候發現是688×768。不同的電腦會有所不同,根據測得的分辨率畫的不對的話,要在測的數值的基礎上乘以或除以2去找到合適的。
void draw_side(unsigned long* addr)
{
int i , j;
//i是縱座標,j是橫座標,屏幕左上角是座標原點。以(200,200)到(400,600)爲對角線畫一個藍色矩形。
for (i = 200; i<600; i++)
for (j = 200; j<400; j++)
*(addr + i * 688 + j) = RGB888(0, 0, 0xff);
}
要注意的是對framebuffer進行操作的畫在圖形界面下是無法顯示的,通過ctrl + alt + F2 切換到命令行模式,在root下運行程序纔可以。
還有一點就是貌似得在物理機上才能畫,我在我的虛擬機上就沒辦法知道屏幕是個什麼分辨率,怎麼都畫不出一個矩形。
不過弄清屏幕的分辨率,成功的畫出一個矩形的話,那剩下畫其他的東西也就簡單了。我是以10×20區域爲單位作爲遊戲中的一個點,遊戲中的區域是20×20的 ,對應到像素上就是200×400。以像素點的(200,200)爲原點到(400,600)的矩形區域就是遊戲區域映射到屏幕上以後的區域。
畫圖的時候只要先實現一個根據遊戲中座標畫點的函數,畫蛇和食物就可利用這個函數一個一個點來畫了。
//以200*200像素位置爲座標原點,屏幕正下方爲y正半軸,屏幕正右方爲x正半軸。根據參數中的xx,yy是有意中的座標,畫圖時對應到像素的x,y上作爲矩形左上角頂點,畫一個10*20像素的矩形,參數r,g,b用於顏色設定。addr爲操作幀緩衝的指針,之後再利用該函數來畫蛇和食物。
void draw_point(unsigned long* addr,int xx,int yy,int r,int g,int b)
{
int i,j,x,y;
x = xx*10+200;
y = yy*20+200;
for(i=y; i<y+20; i++)
for(j=x; j<x+10; j++)
*(addr + i*688 + j) = RGB888(r,g,b);
}
//根據蛇每個節點的座標畫蛇,蛇頭爲綠色,蛇身爲紅色
void draw_snake(unsigned long* addr,S *head)
{
S *find = head;
//畫綠色的頭
draw_point(addr,find->x,find->y,0,0xff,0);
find = find->next;
//身體畫紅色
while(find!=NULL)
{
draw_point(addr,find->x,find->y,0xff,0,0);
find = find->next;
}
}
//根據食物座標畫一個綠色的食物
void draw_food(unsigned long* addr,F *f)
{
draw_point(addr,f->x,f->y,0,0xff,0);
}
//遊戲邊框,屏幕坐上爲座標原點,向右爲x正半軸,向下爲y正半軸
//屏幕的像素範圍爲(0,0)到(688,,768)
//遊戲邊框顏色爲藍色,內側爲(200,200)到(600,400)矩形區域
void draw_side(unsigned long* addr)
{
int i , j;
//邊框的上下邊
for(i=180; i<200; i++)
for(j=190; j<410; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
for(i=600; i<620; i++)
for(j=190; j<410; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
//邊框的左右邊
for(i=200; i<600; i++)
for(j=190; j<200; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
for(i=200; i<600; i++)
for(j=400; j<410; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
}
//清空邊框內側區域,全部刷黑,這樣就不用每次清屏重畫邊框了
void myclear(unsigned long *addr)
{
int i, j;
for (i = 200; i<600; i++)
for (j = 200; j<400; j++)
*(addr + i * 688 + j) = RGB888(0, 0, 0);
}
可以畫圖之後就是遊戲內容的具體實現了。即如何創建蛇,創建食物,讓蛇移動,喫食物,以及gameover的判斷。
下面先說如何創建和食物。蛇開始起碼得有個頭和尾巴,所以就創建了兩個節點。蛇頭位置隨機,尾巴爲蛇頭下面的節點。考慮到頭如果在邊上的話尾巴會出界,所以讓頭在邊框向中間縮3格的範圍內隨機生成。
生成食物的算法是優化過的,開始的思路是每次在遊戲區域內隨機一個節點作爲食物位置,遍歷蛇判斷食物是否在蛇身上,在的話重新生成食物,再判斷,直到食物不在蛇身上。這樣做的話隨機生成食物非常簡單,但是存在一個問題,就是當玩到蛇很長的時候,隨機生成的食物很大概率會在蛇身上,就要反覆的重新生成食物,並遍歷蛇看食物是否在蛇身上,遍歷是很費時間的,而且這樣生成食物就要費很長時間,假設最後只剩一個不在蛇身上的點,要食物正好隨機生成在那個點上的話,結果可以想象。
所以這種創建食物的方法是不可行的,那麼就需要每次只遍歷一次蛇,知道蛇的位置後在剩餘的節點中隨機選一個作爲新的食物位置。這個過程需要開闢一個二維空間來記錄可用節點,將可用節點值置0,蛇的位置置1。然後從0的節點裏隨機選一個作爲食物位置。雖然過程比開始的方法麻煩些,但是實際每一次執行起來的時間是很短的,可以估量的,即使在蛇很長的時候,遊戲也是很流暢的。
以下是具體的函數實現。
//初始化蛇2個節點,蛇頭隨機出現在邊框以內三格區域內,蛇尾在蛇頭下方,參數head爲蛇信息
void cre_snake(S **head)
{
*head = malloc(sizeof(S));
S *end = malloc(sizeof(S));
srand(time(0));
(*head)->x = rand() % 15 + 3;
(*head)->y = rand() % 15 + 3;
(*head)->next = end;
end->x = (*head)->x;
end->y = ((*head)->y) + 1;
end->next = NULL;
}
//改進後的創建食物函數,在蛇位置以外區域隨機生成一個新的食物,參數f爲食物信息,head爲蛇信息
void cre_food(F *f, S *head)
{
int abl_use[20][20]={0};
S *find = head;
int len = 1;//記錄蛇長度
int n;
int i, j, count = 0;//count用來記錄遍歷了的0節點個數。
while (find != NULL)
{
abl_use[find->y][find->x] = 1;
find = find->next;
len++;
}
srand(time(0));
n = rand() % (399 - len);//n爲可用節點數範圍內的一個隨機值
//找到第n個0節點的座標
for (i = 0; (i<20) && (count<n); i++)
for (j = 0; (j<20) && (count<n); j++)
{
if (abl_use[i][j] == 0)
count++;
}
f->x = j - 1;
f->y = i - 1;
}
接下來是蛇的移動和喫食物。
//蛇向指定方向走一步,參數toward爲指定方向,head爲蛇信息
void move(char toward, S *head)
{
S *find = head->next;
int sx, sy, tx, ty;
sx = find->x;
sy = find->y;
find->x = head->x;
find->y = head->y;
switch (toward)
{
case 'w':
head->y -= 1;
break;
case 's':
head->y += 1;
break;
case 'a':
head->x -= 1;
break;
case 'd':
head->x += 1;
break;
}
while (find->next != NULL)
{
tx = find->next->x;
ty = find->next->y;
find->next->x = sx;
find->next->y = sy;
sx = tx;
sy = ty;
find = find->next;
}
}
//避免移動中出現直接掉頭的情況,參數now爲新指定方向,fro當前方向
int if_move(char now,char fro)
{
if(now=='w'&&fro=='s'||now=='s'&&fro=='w'||now=='a'&&fro=='d'||now=='d'&&fro=='a')
return 0;
else
return 1;
}
//喫到食物時用來加長蛇的函數,在蛇尾方向上新增加一個節點,參數head爲蛇信息
void l_snake(S *head)
{
S *find = head;
S *tmp = NULL;
S *new = malloc(sizeof(S));
new->next = NULL;
while (find->next->next != NULL)
{
find = find->next;
}
tmp = find->next;
tmp->next = new;
new->x = tmp->x + tmp->x - find->x;
new->y = tmp->y + tmp->y - find->y;
}
//判斷蛇是否喫到食物,喫掉則調用l_snake加長蛇,並刷新食物位置,參數head爲蛇信息,f爲食物信息
void eat(S *head, F *f)
{
if (head->x == f->x&&head->y == f->y)
{
l_snake(head);
cre_food(f, head);
//改進前喫掉食物後生成新的食物的方法
// while(if_foodinsnake(head,f))
// cre_food(f);
}
}
//改進之前用於判斷新生成的食物是否在蛇身上
/*
int if_foodinsnake(S *head,F *f)
{
S *find = head;
while(find->next!=NULL)
{
if(find->x!=f->x||find->y!=f->y)
find = find->next;
else
return 1;
}
return 0;
}*/
Gameover的具體實現。
//判斷蛇是否越界,參數head爲蛇信息
int outside(S *head)
{
if(head->x<0||head->y<0||head->x>19||head->y>19)
return 1;
else
return 0;
}
//判斷蛇是否碰到自己,參數head爲蛇信息
int headisbody(S *head)
{
S *find = head->next;
while(find!=NULL)
{
if(find->x==head->x&&find->y==head->y)
return 1;
find = find->next;
}
return 0;
}
//通過outside,headisbody判斷是否gameover,並執行結束程序的相關處理
void gameover(S* head)
{
if(headisbody(head)||outside(head))
{
system("clear");
printf("Game Over!\n");
recover_keyboard();//要注意恢復終端的設置
exit(EXIT_SUCCESS);
}
}
以上是遊戲功能的函數實現,此外還有一個很重要的部分就是利用tcgetattr與tcsetattr函數控制終端 ,實時讀取鍵盤的按鍵。
需要的頭文件和宏定義。
#include <stdio.h>
#include <stdlib.h>
//讀鍵盤相關頭文件
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>
//根據鍵盤按鍵的鍵值進行宏定義
#define UP 0x415b1b
#define DOWN 0x425b1b
#define LEFT 0x445b1b
#define RIGHT 0x435b1b
#define ENTER 0xa
#define ESC 0x1b
#define SPACE 0x20
需要的函數以及使用的框架。
//讀鍵盤按鍵
int init_keyboard(void)
{
int ret;
struct termios tc;
ret = tcgetattr(0, &tcsave);
if(ret < 0)
return -1;
tc = tcsave;
tc.c_lflag &= ~(ECHO|ICANON);
ret = tcsetattr(0, TCSANOW, &tc);
if(ret < 0)
return -1;
flsave = fcntl(0, F_GETFL);
fcntl(0, F_SETFL, flsave|O_NONBLOCK);
return 0;
}
void recover_keyboard(void)
{
tcsetattr(0, TCSANOW, &tcsave);
fcntl(0, F_SETFL, flsave);
}
int get_key(void)
{
unsigned char buf[3];
int ret = read(0, buf, sizeof(buf));
if(ret < 0)
return -1;
int i = 0, key = 0;
for(i=0; i<ret; i++){
key += (buf[i]<<(i*8));
}
return key;
}
int is_up(int key)
{
return key == UP;
}
int is_down(int key)
{
return key == DOWN;
}
int is_left(int key)
{
return key == LEFT;
}
int is_right(int key)
{
return key == RIGHT;
}
int is_enter(int key)
{
return key == ENTER;
}
int is_esc(int key)
{
return key == ESC;
}
int is_space(int key)
{
return key == SPACE;
}
int main()
{
int key, ret;
ret = init_keyboard();
if(ret < 0)
return -1;
while(1){
key = get_key();
if(key < 0)
continue;
//printf("key = %x\n", key);
if(is_left(key))
printf("left\n");
if(is_right(key))
printf("right\n");
if(is_up(key))
printf("up\n");
if(is_down(key))
printf("down\n");
if(is_enter(key))
printf("enter\n");
if(is_space(key))
printf("space\n");
if(is_esc(key)){
printf("esc\n");
break;
}
if(key == 'q')
break;
}
recover_keyboard();
return 0;
}
利用這個框架就可以在按鍵時,顯示按的是那個鍵,實際上就是執行了對應鍵值調用的函數。再把這個框架利用到自己的程序中,將已經實現的遊戲函數融入進來。
下面就是遊戲的主函數部分。
int main()
{
system("clear");
//framebuffer初始化
struct fb_var_screeninfo vinfo;
int fd = open("/dev/fb0", O_RDWR);
if(fd < 0)
{
perror("open err. \n");
exit(EXIT_FAILURE);
}
int fret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
if(fret < 0)
{
perror("ioctl err. \n");
exit(EXIT_FAILURE);
}
//用來知道自己屏幕的分辨率
/*
printf("vinfo.xres: %d\n", vinfo.xres);
printf("vinfo.yres: %d\n", vinfo.yres);
printf("vinfo.bits_per_pixel: %d\n", vinfo.bits_per_pixel);
*/
//定義指針來操作framebuffer
unsigned long* addr = mmap(NULL, (vinfo.xres*vinfo.yres*vinfo.bits_per_pixel)>>3,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
//遊戲初始化
S *head = NULL;
F *f = malloc(sizeof(F));
draw_side(addr);
cre_snake(&head);
cre_food(f,head);
draw_snake(addr,head);
draw_food(addr,f);
//初始化鍵盤設置
int key, ret;
char choose='d';//初始蛇的運動方向,d爲右,a爲左,w爲上,s爲下
int speed = 1;//速度檔1爲0.4s走一步,0爲0.2s走一步
ret = init_keyboard();//接入鍵盤
if(ret < 0)
return -1;
//遊戲循環體,上下左右控制方向,空格變速,esc和q退出
while(1)
{
key = get_key();
//printf("key = %x\n", key);
//接收鍵盤信息改變蛇的運動方向,調用if_move來防止直接掉頭
if(is_left(key))
{
if(if_move('a',choose))
choose = 'a';
}
if(is_right(key))
{
if(if_move('d',choose))
choose = 'd';
}
if(is_up(key))
{
if(if_move('w',choose))
choose = 'w';
}
if(is_down(key))
{
if(if_move('s',choose))
choose = 's';
}
//可以爲回車添加其他功能
// if(is_enter(key))
// printf("enter\n");
//空格用來調速
if(is_space(key))
{
if(speed)
speed = 0;
else
speed = 1;
}
//退出遊戲
if(is_esc(key)){
printf("esc\n");
break;
}
if(key == 'q')
break;
//每次循環讀取鍵盤信息後,先判斷當前位置蛇是否喫到食物,再進行移動,然後判斷是否gameover
eat(head,f);
move(choose,head);
gameover(head);
//刷新圖像信息
myclear(addr);
draw_snake(addr,head);
draw_food(addr,f);
//可用於清除多餘的按鍵操作,經過測試認爲不清除比較好
// int c;
// while((c=getchar())!='\n'&&c!=EOF);
//通過sleep來控制遊戲循環的頻率,從而控制蛇的速度
if(speed)
usleep(400000);
else
usleep(200000);
}
//恢復鍵盤
recover_keyboard();
return 0;
}
通過以上的方法就可以實現簡單的貪喫蛇遊戲了,雖然看起來是一氣呵成,實際上首先設計好程序的框架,以及每一部分所要實現的功能是十分重要的。然後再將問題抽象,細分成一個一個小功能再逐步實現。並且每一部分實現後都要單獨設計用例進行檢測,沒有問題以後再進行下一步實現。整個過程從最初的畫邊框蛇,以及食物,到讓蛇移動喫食,再到最後加入Gameover,都是每完成一部分功能後進行測試,再來實現下一部分功能的。一直到實現所有的功能。
此外還可以做以下改進:
- 蛇的移動實現,這裏是將頭的位置改變後,之後的每一個節點都繼承前一個節點的座標,這裏每次移動都需要遍歷一遍蛇,蛇越來越長的話遍歷的代價就會越來越大。所以可以做以下優化,除了蛇的頭節點外再記錄下蛇尾節點的前一個,每次移動時將尾節點換到頭結點前面並記錄新的頭位置,再將更新頭節點和新的尾節點,並將新的尾節點的next賦空。這樣只是把尾巴換到頭上來就實現了移動,而不需要遍歷整個蛇了。
- 蛇喫食物,這裏也需要遍歷蛇,找到尾節點的前一個節點後再根據尾巴方向添加的新的節點,如果記錄了尾節點的前一個節點的話就不用每次再遍歷了。
- Gameover時,判斷越界和蛇是否喫到自己是分別遍歷兩次蛇來實現的,可以放到一起,在一次遍歷中實現。
- 這裏如果更換設備分辨率改變的話,畫圖部分就要根據像素重新編寫了。可以利用在操作framebuffer的時候得到的分辨率,作爲畫圖時需要的分辨率參數,這樣即使更換設備也可以兼容了。
- 遊戲功能上還可以豐富些,加入暫停,分數以及遊戲時間記錄還有最高分排行等元素。
- 由於對framebuffer的具體機理不是十分熟悉,遊戲只能在命令行模式,root權限下才能運行。還有待進一步學習,使得程序可以在視圖模式,普通用戶權限下就可以運行。
程序源碼:
- list.h
#ifndef _LIST_H
#define _LIST_H
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//framebuffer相關頭文件
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
//讀鍵盤相關頭文件
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>
//宏定義設置RGB,以便通過設定 r,g,b參數來設置顏色
#define RGB888(r, g, b) ((0xff & r) << 16 | (0xff & g) << 8 | (b & 0xff))
struct termios tcsave;
int flsave;
//根據鍵盤按鍵的鍵值進行宏定義
#define UP 0x415b1b
#define DOWN 0x425b1b
#define LEFT 0x445b1b
#define RIGHT 0x435b1b
#define ENTER 0xa
#define ESC 0x1b
#define SPACE 0x20
//食物數據
typedef struct Food
{
int x;
int y;
}F;
//蛇的數據
typedef struct Snake
{
int x;
int y;
struct Snake *next;
}S;
//遊戲邏輯功能函數
void cre_snake(S**);//初始化蛇
void cre_food(F*, S*);//創建食物,使食物隨機生成在身以外的位置
void move(char, S*);//蛇向指定方向走一步
int if_move(char,char);//避免移動中出現直接掉頭的情況
void eat(S*, F*);//蛇喫食物,調用l_snake加長蛇,並刷新食物位置
void l_snake(S*);//喫到食物時用來加長蛇的函數
//gameover相關函數
int outside(S*);//判斷蛇是否越界
int headisbody(S*);//判斷蛇是否碰到自己
void gameover(S*);//通過outside,headisbody判斷是否gameover,並執行結束程序的相關處理
//int if_foodinsnake(S*,F*);//原判斷食物是否在射身上的函數
//畫圖函數,將座標信息對應到屏幕像素位置
void draw_point(unsigned long*,int,int,int,int,int);//在規劃好的區域內,根據左邊,畫一個指定的單元點,以及設定點的顏色
void draw_snake(unsigned long*,S*);//利用draw_point和蛇的數據畫蛇
void draw_food(unsigned long*,F*);//利用draw_point和食物數據畫食物
void draw_side(unsigned long*);//根據規劃好的像素位置畫,遊戲的邊框
void myclear(unsigned long*);//用於清除方框內顏色,以便重新畫圖
//獲取鍵盤相關函數
int init_keyboard(void);//初始化調用鍵盤
void recover_keyboard(void);//釋放鍵盤調用
int get_key(void);//獲取鍵值
//根據鍵值判斷鍵位
int is_up(int);
int is_down(int);
int is_left(int);
int is_right(int);
int is_enter(int);
int is_esc(int);
int is_space(int);
#endif
- fun_snake.c
#include"list.h"
//函數實現
//遊戲邏輯功能函數
//初始化蛇2個節點,蛇頭隨機出現在邊框以內三格區域內,蛇尾在蛇頭下方,參數head爲蛇信息
void cre_snake(S **head)
{
*head = malloc(sizeof(S));
S *end = malloc(sizeof(S));
srand(time(0));
(*head)->x = rand() % 15 + 3;
(*head)->y = rand() % 15 + 3;
(*head)->next = end;
end->x = (*head)->x;
end->y = ((*head)->y) + 1;
end->next = NULL;
}
//改進後的創建食物函數,在蛇位置以外區域隨機生成一個新的食物,參數f爲食物信息,head爲蛇信息
void cre_food(F *f, S *head)
{
int abl_use[20][20];
S *find = head;
int len = 1;
int n;
int i, j, key = 0;
for (i = 0; i<20; i++)
for (j = 0; j<20; j++)
abl_use[i][j] = 0;
while (find != NULL)
{
abl_use[find->y][find->x] = 1;
find = find->next;
len++;
}
srand(time(0));
n = rand() % (399 - len);
for (i = 0; (i<20) && (key<n); i++)
for (j = 0; (j<20) && (key<n); j++)
{
if (abl_use[i][j] == 0)
key++;
}
f->x = j - 1;
f->y = i - 1;
}
//蛇向指定方向走一步,參數toward爲指定方向,head爲蛇信息
void move(char toward, S *head)
{
S *find = head->next;
int sx, sy, tx, ty;
sx = find->x;
sy = find->y;
find->x = head->x;
find->y = head->y;
switch (toward)
{
case 'w':
head->y -= 1;
break;
case 's':
head->y += 1;
break;
case 'a':
head->x -= 1;
break;
case 'd':
head->x += 1;
break;
}
while (find->next != NULL)
{
tx = find->next->x;
ty = find->next->y;
find->next->x = sx;
find->next->y = sy;
sx = tx;
sy = ty;
find = find->next;
}
}
//避免移動中出現直接掉頭的情況,參數now爲新指定方向,fro當前方向
int if_move(char now,char fro)
{
if(now=='w'&&fro=='s'||now=='s'&&fro=='w'||now=='a'&&fro=='d'||now=='d'&&fro=='a')
return 0;
else
return 1;
}
//喫到食物時用來加長蛇的函數,在蛇尾方向上新增加一個節點,參數head爲蛇信息
void l_snake(S *head)
{
S *find = head;
S *tmp = NULL;
S *new = malloc(sizeof(S));
new->next = NULL;
while (find->next->next != NULL)
{
find = find->next;
}
tmp = find->next;
tmp->next = new;
new->x = tmp->x + tmp->x - find->x;
new->y = tmp->y + tmp->y - find->y;
}
//判斷蛇是否喫到食物,喫掉則調用l_snake加長蛇,並刷新食物位置,參數head爲蛇信息,f爲食物信息
void eat(S *head, F *f)
{
if (head->x == f->x&&head->y == f->y)
{
l_snake(head);
cre_food(f, head);
// while(if_foodinsnake(head,f))
// cre_food(f);
}
}
//GAMEOVER部分
//判斷蛇是否越界,參數head爲蛇信息
int outside(S *head)
{
if(head->x<0||head->y<0||head->x>19||head->y>19)
return 1;
else
return 0;
}
//判斷蛇是否碰到自己,參數head爲蛇信息
int headisbody(S *head)
{
S *find = head->next;
while(find != NULL)
{
if(find->x==head->x&&find->y==head->y)
return 1;
find = find->next;
}
return 0;
}
//通過outside,headisbody判斷是否gameover,並執行結束程序的相關處理
void gameover(S* head)
{
if(headisbody(head)||outside(head))
{
system("clear");
printf("Game Over!\n");
recover_keyboard();
exit(EXIT_SUCCESS);
}
}
//改進之前用於判斷新生成的食物是否在蛇身上
/*
int if_foodinsnake(S *head,F *f)
{
S *find = head;
while(find->next!=NULL)
{
if(find->x!=f->x||find->y!=f->y)
find = find->next;
else
return 1;
}
return 0;
}*/
//畫圖相關函數
//以200 * 200像素位置爲座標原點, 屏幕正下方爲y正半軸,屏幕正右方爲x正半軸。
//根據參數中的xx, yy是有意中的座標,畫圖時對應到像素的x, y上作爲矩形左上角頂點,畫一個10 * 20像素的矩形,參數r, g, b用於顏色設定。
//addr爲操作幀緩衝的指針,之後再利用該函數來畫蛇和食物。
void draw_point(unsigned long* addr,int xx,int yy,int r,int g,int b)
{
int i,j,x,y;
x = xx*10+200;
y = yy*20+200;
for(i=y; i<y+20; i++)
for(j=x; j<x+10; j++)
*(addr + i*688 + j) = RGB888(r,g,b);
}
//根據蛇每個節點的座標畫蛇,蛇頭爲綠色,蛇身爲紅色
void draw_snake(unsigned long* addr,S *head)
{
S *find = head;
draw_point(addr,find->x,find->y,0,0xff,0);
find = find->next;
while(find!=NULL)
{
draw_point(addr,find->x,find->y,0xff,0,0);
find = find->next;
}
}
//根據食物座標畫一個綠色的食物
void draw_food(unsigned long* addr,F *f)
{
draw_point(addr,f->x,f->y,0,0xff,0);
}
//遊戲邊框,屏幕坐上爲座標原點,向右爲x正半軸,向下爲y正半軸
//屏幕的像素範圍爲(0,0)到(688,,768)
//遊戲邊框顏色爲藍色,內側爲(200,200)到(600,400)矩形區域
void draw_side(unsigned long* addr)
{
int i , j;
//邊框的上下邊
for(i=180; i<200; i++)
for(j=190; j<410; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
for(i=600; i<620; i++)
for(j=190; j<410; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
//邊框的左右邊
for(i=200; i<600; i++)
for(j=190; j<200; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
for(i=200; i<600; i++)
for(j=400; j<410; j++)
*(addr + i*688 + j) = RGB888(0,0,0xff);
}
//清空邊框內側區域,全部刷黑
void myclear(unsigned long *addr)
{
int i, j;
for (i = 200; i<600; i++)
for (j = 200; j<400; j++)
*(addr + i * 688 + j) = RGB888(0, 0, 0);
}
//讀鍵盤按鍵
int init_keyboard(void)
{
int ret;
struct termios tc;
ret = tcgetattr(0, &tcsave);
if(ret < 0)
return -1;
tc = tcsave;
tc.c_lflag &= ~(ECHO|ICANON);
ret = tcsetattr(0, TCSANOW, &tc);
if(ret < 0)
return -1;
flsave = fcntl(0, F_GETFL);
fcntl(0, F_SETFL, flsave|O_NONBLOCK);
return 0;
}
void recover_keyboard(void)
{
tcsetattr(0, TCSANOW, &tcsave);
fcntl(0, F_SETFL, flsave);
}
int get_key(void)
{
unsigned char buf[3];
int ret = read(0, buf, sizeof(buf));
if(ret < 0)
return -1;
int i = 0, key = 0;
for(i=0; i<ret; i++){
key += (buf[i]<<(i*8));
}
return key;
}
int is_up(int key)
{
return key == UP;
}
int is_down(int key)
{
return key == DOWN;
}
int is_left(int key)
{
return key == LEFT;
}
int is_right(int key)
{
return key == RIGHT;
}
int is_enter(int key)
{
return key == ENTER;
}
int is_esc(int key)
{
return key == ESC;
}
int is_space(int key)
{
return key == SPACE;
}
- main.c
#include"list.h"
int main()
{
system("clear");
//framebuffer初始化
struct fb_var_screeninfo vinfo;
int fd = open("/dev/fb0", O_RDWR);
if(fd < 0)
{
perror("open err. \n");
exit(EXIT_FAILURE);
}
int fret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
if(fret < 0)
{
perror("ioctl err. \n");
exit(EXIT_FAILURE);
}
//用來知道自己屏幕的分辨率
/*
printf("vinfo.xres: %d\n", vinfo.xres);
printf("vinfo.yres: %d\n", vinfo.yres);
printf("vinfo.bits_per_pixel: %d\n", vinfo.bits_per_pixel);
*/
//定義指針來操作framebuffer
unsigned long* addr = mmap(NULL, (vinfo.xres*vinfo.yres*vinfo.bits_per_pixel)>>3,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
//遊戲初始化
S *head = NULL;
F *f = malloc(sizeof(F));
draw_side(addr);
cre_snake(&head);
cre_food(f,head);
draw_snake(addr,head);
draw_food(addr,f);
//初始化鍵盤設置
int key, ret;
char choose='d';//初始蛇的運動方向,d爲右,a爲左,w爲上,s爲下
int speed = 1;//速度檔1爲0.4s走一步,0爲0.2s走一步
ret = init_keyboard();//接入鍵盤
if(ret < 0)
return -1;
//遊戲循環體,上下左右控制方向,空格變速,esc和q退出
while(1)
{
key = get_key();
//printf("key = %x\n", key);
//接收鍵盤信息改變蛇的運動方向,調用if_move來防止直接掉頭
if(is_left(key))
{
if(if_move('a',choose))
choose = 'a';
}
if(is_right(key))
{
if(if_move('d',choose))
choose = 'd';
}
if(is_up(key))
{
if(if_move('w',choose))
choose = 'w';
}
if(is_down(key))
{
if(if_move('s',choose))
choose = 's';
}
//可以爲回車添加其他功能
// if(is_enter(key))
// printf("enter\n");
//空格用來調速
if(is_space(key))
{
if(speed)
speed = 0;
else
speed = 1;
}
//退出遊戲
if(is_esc(key)){
printf("esc\n");
break;
}
if(key == 'q')
break;
//每次循環讀取鍵盤信息後,先判斷當前位置蛇是否喫到食物,再進行移動,然後判斷是否gameover
eat(head,f);
move(choose,head);
gameover(head);
//刷新圖像信息
myclear(addr);
draw_snake(addr,head);
draw_food(addr,f);
//可用於清除多餘的按鍵操作,經過測試認爲不清除比較好
// int c;
// while((c=getchar())!='\n'&&c!=EOF);
//通過sleep來控制遊戲循環的頻率,從而控制蛇的速度
if(speed)
usleep(400000);
else
usleep(200000);
}
//恢復鍵盤
recover_keyboard();
return 0;
}
- Makefile
CC=gcc
FLAG=-c
OUTPUT=-o
OBJ=exe
ALL=fun_snake.o main.o
$(OBJ):$(ALL)
$(CC) $^ $(OUTPUT) $@
%.o:%.c
$(CC) $(FLAG) $< $(OUTPUT) $@
.PHONY:clean
clean:
@rm -rf $(ALL) $(OBJ)
@echo "del ok!"