介绍
我口袋里有一些小型/微小的游戏开发项目,但我没有在我的 Bearblog 帐户中记录这些项目。第一个是仅使用curses 库的ASCII 贪吃蛇游戏。将会有另一篇关于使用 Raylib 制作贪吃蛇游戏的类似文章。
#1 图书馆
#include <curses.h> #include <stdbool.h> #include <stdlib.h> #include <time.h> #include <unistd.h> #include <string.h>
#2 常量、结构和全局变量
#define MAX_SCORE 256 #define FRAME_TIME 180000 typedef struct { int x; int y; } Vec2; int score = 0; char score_message[32]; bool skip = false; bool is_running = true; int screen_width = 25; int screen_height = 20; // initialize screen WINDOW *win; // snake Vec2 head = {0, 0}; Vec2 segments[MAX_SCORE + 1]; Vec2 dir = {1, 0}; // berry Vec2 berry;
#3 函数声明
void game_over(); void init(); void quit_game(); void restart_game(); bool collide(Vec2 a, Vec2 b); bool collide_snake_body(Vec2 point); Vec2 spawn_berry(); void process_input(); void draw_border(int y, int x, int width, int height); void draw(); void update(); void game_over();
#4 碰撞:碰撞函数
- 碰撞:检查两个点是否具有相同的坐标。
- collide_snake_body:检查点是否与蛇的身体碰撞。
bool collide(Vec2 a, Vec2 b) { if (ax == bx && ay == by) { return true; } else return false; } bool collide_snake_body(Vec2 point) { for (int i = 0; i < score; i++) { if (collide(point, segments[i])) { return true; } } return false; }
#5 产卵浆果
- 在屏幕上为“浆果”生成一个随机位置,确保它不在蛇的头部或身体上,并且从边缘开始有 1 像素的填充
Vec2 spawn_berry() { // spawn a new berry with 1 pixel padding from edges and not inside of the snake Vec2 berry = { 1 + rand() % (screen_width - 2), 1 + rand() % (screen_height - 2) }; while (collide(head, berry) || collide_snake_body(berry)) { berry.x = 1 + rand() % (screen_width - 2); berry.y = 1 + rand() % (screen_height - 2); } return berry; }
#6 游戏区:绘制边框
- 使用 ncurses 库函数,使用指定宽度和高度的 ASCII 字符在屏幕上绘制边框。
void draw_border(int y, int x, int width, int height) { // top row mvaddch(y, x, ACS_ULCORNER); mvaddch(y, x + width * 2 + 1, ACS_URCORNER); for (int i = 1; i < width * 2 + 1; i++) { mvaddch(y, x + i, ACS_HLINE); } // vertical lines for (int i = 1; i < height + 1; i++) { mvaddch(y + i, x, ACS_VLINE); mvaddch(y + i, x + width * 2 + 1, ACS_VLINE); } // bottom row mvaddch(y + height + 1, x, ACS_LLCORNER); mvaddch(y + height + 1, x + width * 2 + 1, ACS_LRCORNER); for (int i = 1; i < width * 2 + 1; i++) { mvaddch(y + height + 1, x + i, ACS_HLINE); } }
#7 退出并重新启动
- quit_game:彻底退出游戏,恢复终端状态。
- restart_game:将游戏状态重置为初始值。
void quit_game() { // exit cleanly from application endwin(); // clear screen, place cursor on top, and un-hide cursor printf("\e[1;1H\e[2J"); printf("\e[?25h"); exit(0); } void restart_game() { head.x = 0; head.y = 0; dir.x = 1; dir.y = 0; score = 0; sprintf(score_message, "[ Score: %d ]", score); is_running = true; }
#8 初始化函数
此代码通过以下方式初始化游戏:
- 为随机数生成器播种
- 使用 ncurses 设置终端窗口
- 初始化颜色和颜色对
- 随机将“浆果”放置在屏幕上
- 初始化分数消息
void init() { srand(time(NULL)); // initialize window win = initscr(); // take player input and hide cursor keypad(win, true); noecho(); nodelay(win, true); curs_set(0); // initialize color if (has_colors() == FALSE) { endwin(); fprintf(stderr, "Your terminal does not support color\n"); exit(1); } start_color(); use_default_colors(); init_pair(1, COLOR_BLUE, -1); init_pair(2, COLOR_GREEN, -1); init_pair(3, COLOR_YELLOW, -1); berry.x = rand() % screen_width; berry.y = rand() % screen_height; // update score message sprintf(score_message, "[ Score: %d ]", score); }
#9 游戏结束状态
game_over:显示“游戏结束”消息并等待用户输入以重新启动或退出。
void game_over() { while (is_running == false) { process_input(); mvaddstr(screen_height / 2, screen_width - 16, " Game Over "); mvaddstr(screen_height / 2 + 1, screen_width - 16, "[SPACE] to restart, [ESC] to quit "); attron(COLOR_PAIR(3)); draw_border(screen_height / 2 - 1, screen_width - 17, 17, 2); attroff(COLOR_PAIR(3)); usleep(FRAME_TIME); } }
#10 运动 – process_input
此代码通过以下方式处理用户输入:
- 从终端窗口读取按键
- 根据箭头键按下更新蛇的方向
- 防止蛇立即改变方向
- 按下空格键时重新启动游戏
- 按下退出键时退出游戏
void process_input() { int pressed = wgetch(win); if (pressed == KEY_LEFT) { if (dir.x == 1) { return; skip = true; } dir.x = -1; dir.y = 0; } if (pressed == KEY_RIGHT) { if (dir.x == -1) { return; skip = true; } dir.x = 1; dir.y = 0; } if (pressed == KEY_UP) { if (dir.y == 1) { return; skip = true; } dir.x = 0; dir.y = -1; } if (pressed == KEY_DOWN) { if (dir.y == -1) { return; skip = true; } dir.x = 0; dir.y = 1; } if (pressed == ' ') { if (!is_running) restart_game(); } if (pressed == '\e') { is_running = false; quit_game(); } }
#11 更新游戏状态和抽奖功能
更新:
- 更新蛇的分段和位置
- 检查与身体或墙壁的碰撞
- 处理吃浆果并更新分数
- 等待一小段时间来控制游戏的帧率
画:
- 清除屏幕并重新绘制游戏元素
- 使用不同的颜色绘制浆果、蛇和边框
- 显示分数消息
void update() { // update snake segments for (int i = score; i > 0; i--) { segments[i] = segments[i - 1]; } segments[0] = head; // move snake head.x += dir.x; head.y += dir.y; // collide with body or walls if (collide_snake_body(head) || head.x < 0 || head.y < 0 \ || head.x >= screen_width || head.y >= screen_height) { is_running = false; game_over(); } // eating a berry if (collide(head, berry)) { if (score < MAX_SCORE) { score += 1; sprintf(score_message, "[ Score: %d ]", score); } else { // WIN! printf("You Win!"); } berry = spawn_berry(); } usleep(FRAME_TIME); } void draw() { erase(); attron(COLOR_PAIR(1)); mvaddch(berry.y+1, berry.x * 2+1, '@'); attroff(COLOR_PAIR(1)); // draw snake attron(COLOR_PAIR(2)); for (int i = 0; i < score; i++) { mvaddch(segments[i].y+1, segments[i].x * 2 + 1, ACS_DIAMOND); } mvaddch(head.y+1, head.x * 2+1, 'O'); attroff(COLOR_PAIR(2)); attron(COLOR_PAIR(3)); draw_border(0, 0, screen_width, screen_height); attroff(COLOR_PAIR(3)); mvaddstr(0, screen_width - 5, score_message); }
#12 主要
此代码是游戏的主要入口点:
- 处理命令行参数以设置屏幕尺寸
- 初始化游戏状态
int main(int argc, char *argv[]) { // process user args if (argc == 1) {} else if (argc == 3) { if (!strcmp(argv[1], "-d")) { if (sscanf(argv[2], "%dx%d", &screen_width, &screen_height) != 2) { printf("Usage: snake [options]\nOptions:\n -d [width]x[height]" "\tdefine dimensions of the screen\n\nDefault dimensions are 25x20\n"); exit(1); } } } else { printf("Usage: snake [options]\nOptions:\n -d [width]x[height]" "\tdefine dimensions of the screen\n\nDefault dimensions are 25x20\n"); exit(1); } init(); while(true) { process_input(); if (skip == true) { skip = false; continue; } // ---------- update ---------- update(); // ------------ draw ------------ draw(); } quit_game(); return 0; }