声明:这篇文章中出现的代码并非凑字数,与叙述内容无关的均已用文字代替,还请耐心阅读


(没错俺偷懒直接把作业报告改改(其实没怎么改)就发出来了)

(虽然说是小游戏,但不知为什么压缩后都超50MB了555)

首先上源码链接(内含食用方法)

发现用百度网盘发太费事了,所以直接传到github公开了,欢迎直接运行

GitHub - robin-bird-go/fake_piano_blocks: 钢琴块的低仿拉胯版

(以及总代码在链接里面,文中的基本上是半伪代码)

(先上个图镇楼,整了个简单gif)

一、开发环境

  1. 基本软件:vs2022和easy_x插件

  2. 使用已有库

#include<windows.h>
#include<stdio.h>
#include <stdlib.h>
#include<time.h>
#include <conio.h>
#include <graphics.h>
#include <time.h>
#include <math.h>

二、基本原理

  1. 菜单界面展示:利用<windows.h>库中的system函数进行窗口格式、显示内容操作(比如清屏"cls",暂停"pause"),COORD结构体来实现光标的移动

  2. 实时对输入的读取:利用<coino.h>库的_getch()函数来获取输入的按键值、_kbhit()来判断是否有按键值输入

  3. 游戏界面的绘制:利用<graphics.h>库的相关绘制操作、绘图(这里主要用到了线条、矩形绘制)的函数来形成基本画面和动态图像

  4. 游戏元素、变量的存储:该游戏的核心元素是钢琴块,其产生,消除的先后顺序正好是FIFO,符合队列特点,故用该数据结构实现;游戏数据记录是通过读取本地txt文件,但是展示时可能面临一页界面不足的问题,所以采用单向链表存储每一页的数据

  5. 游戏动态展示思维:游戏的画面也是类似于视频的”逐帧播放“,需要不停进行清屏、绘制,easy_x的库函数中有一系列批量绘制的函数可以实现双缓冲作用防止游戏画面闪烁

三、需求说明

作为一款轻量级的简单小游戏,在以下方面具有相应特点

  1. 在界面设计上,简单,功能清晰明了

  2. 在与用户交互上,尽量做到反馈及时准确,画面流畅舒适等

  3. 在游戏内容规则上,将相关设置和游戏玩法用简明扼要的方式呈现,能让体验者快速上手

  4. 在游戏条件上,应尽量从简单化,不需要过多的配置即可打开、运行游戏

四、成果展示

(1)基本功能

  1. 界面显示

    • 菜单界面

      先利用system("mode con cols=120 lines=40 ");将命令行窗口设置为120字符宽,40行高度

      在配合gotoxy()函数,将光标自由移动到指定位置,进行输出,例如星标的打出

      //星标的打出,n表示打在第几行,num表示一共多少选项
      void menu_select(int num, int n)
      {int i;for (i = 0; i < num; i++){if (i == n){gotoxy(35, 18 + i * 5);printf("-★-");gotoxy(64, 18 + i * 5);printf("-★-");}else{gotoxy(35, 18 + i * 5);printf("    ");gotoxy(64, 18 + i * 5);printf("    ");}}
      }
    • 游戏界面

      initgraph(100 * key_num + 120, 650);开始,这里宽度受到打击键个数的影响,高度恒为650;以closegraph();结束,关闭绘图(游戏)窗口

      绘制背景、图形,先set_sth_color(RGB()),set_sth_style(),再绘制相应图形如直线line(左端点x,左端点y,右端点x,右端点y),矩形fillrectangle(l,t,r,d)

  2. 钢琴块移动

    每次将blocks结构体的y值增加,并配合清屏、绘制即可实现

    while (tmp)
    {display_b(tmp);tmp->y += 3 * speed;tmp = tmp->next;
    }
  3. 击中判定以及发声

    在检测到外部输入时,根据blocks结构体的y(也就是钢琴块的下端)与打击区域的关系来给出相应处理,在区间外视为miss,在区间内,根据其距离打击重心的远近来给出不同的分数;发声是对应有bgm的版本的模式,利用一个简单的蜂鸣函数Beep(fre,time)实现,击中的处理均为使当前节点出队(具体结构体运用会在后面详细说明)

  4. 游戏属性的设置

    设置了一些可调整大小的参数,来改变游戏的属性,从而增加游戏的多样性

  5. 游戏数据的存储

    主要运用C语言的文件读取函数fopen(),fgets(),fscanf()来进行文件读、写,读到末尾用fscanf()== EOF来判定

(2)游戏模式

游戏模式有三种,分别为:

  1. 有bgm版本/无bgm原版

前者主要多了一个击中某些特定音符的发声效果,界面是一样的

  1. 双向魔改版

这里主要是将游戏界面分为了两部分,左边往下移动,右边往上移动,打击线的位置对称,保证每次只会在两边中的一边进行击打钢琴块

具体界面可以在后面运行结果展示以及附带游戏体验视频中观看

(3)核心代码/思路分析

[1]总体的编程过程

  1. 参数化设置,便于不断调试修改

    对于一些经常被使用到,但无需更改值的量,被定义为宏后,放于文件顶端,方便集中修改,因为窗口的尺寸、文字输出的位置,大小等都是十分需要进行实际运行,观察输出来一步步调整的,参数化设置提供了极大的便利

  2. 模块化编程(多文件)

    (i)内容:该项目包含3个源文件(main.cpp,menu.cpp,game.cpp),2个头文件(menu.h,game.h),也就是一个主程序,和两个模块(一个是菜单,一个是游戏)

    (ii)过程:

    • 先从菜单界面显示开始,通过学习网络上的文章,在main.c中先编写出相应的函数,引用对应的库函数,使之编译,运行通过后,再开始代码转移;

    • //main.c文件中
      #include <windows.h>
      //#include ....
      ​
      //清屏
      void clr(void)
      {system("cls");
      }
      ​
      //int main()....
    • 创建/寻找你想放入的模块对应的.cpp.h文件,并参考下面格式进行补充

    • //menu.h
      /*
      头文件放入以下内容(一般不在这里引用头文件)
      1.宏定义(带值)     #define
      2.结构体声明  typedef struct ...;
      3.常量定义(带值)  const data_type <x> = ...;
      4.变量声明  ▲容易被报错重定义,如果想用全项目均可访问的,建议用extern▲extern data_type <x>;
      5.函数声明   void fun(void);
      */
      #pragma once
      #define main_menu_choice 4
      typedef struct _page//每一页的数据
      {int n;//这个表示存了多少行int data[9][7];char time[9][20];struct _page* last;struct _page* next;
      }p, * pp;
      extern pp head;//此时它还没有值,只是被声明了
      const int choose[3][3] = { {1,2,3},{1,2,3},{4,5,6} };
      void clr(void);//函数声明
    • //menu.cpp
      /*
      1.该文件下需要的头文件引用  #include...
      (特别提醒,一定要在非主程序.cpp文件下引用同名的.h文件)
      2.变量定义(如果需要初值,可以在这里设置)  data_type <x> = value;
      3.函数定义  void fun(){...}
      */
      #include "menu.h"
      //#include <stdio.h> ...
      ​
      //清屏,函数定义被移动到这里了
      void clr(void)
      {system("cls");
      }
    • 最后记得主程序也要引用自己定义的.h头文件,用#include "head.h"格式,是双引号,否则可能会引用失败

    • 甚至,对于一些比较复杂的、关联性大,需要一次性写出所有函数再debug的内容,可以重新开一个新的项目,单独调试,成功后再移植过来

    • 总之,对于一个较为复杂,功能较多的项目,应分而治之,逐级调试后再整合

  3. 游戏的运行模式处理

    (i)循环性:由于游戏需要实时与用户交互,随时监测输入的信息并及时给出输出,所以整个程序的主体是在while(1)的循环里的,而大多数功能模块,也在while(1)循环中,只在特定的条件下被break,跳转到其他的代码块中,从此次经验看,整体代码有如下框架:

    //主菜单
    int main()
    {while(1){清屏根据输入值调整屏幕显示内容,输出主菜单while(1){监测有无外部输入if(有)break;            }获取外部输入if(输入 == value_1){清屏         给出对应显示while(1){清屏获取外部输入if(输入 == ord_1)对应显示..else if(...)break;}}else if(输入 == value_2)....                            }           return 0;
    }

    而游戏主体部分更是如此

    //游戏部分
    void game()
    {初始化界面,内存while(1){while(未超出打击区域极限){移动钢琴块清屏,显示检测外部输入有无if(有)break;}获取输入if(表示暂停){while(1)遇到对应条件,退出}else if(到达打击区域)  击中与否进行判断以及处理else if(超出打击区域)对应处理    } 输出游戏结束的结果
    }

    (ii)界面动态性:其实是与循环性相成的,因为画面会因为玩家操作而改变,这涉及清屏重绘的反复性,但是绘制总会有先后性,如果仅仅是在while循环里清屏,逐个绘制,容易使画面闪烁,而在网上看到的很多解决办法即是使用双缓冲机制,而强大的easy_x库提供了3个有力的工具,可以直接实现这样的作用,即

    void BeginBatchDraw();
    // 结束批量绘制,并执行指定区域内未完成的绘制任务
    void EndBatchDraw(int left,int top,int right,int bottom
    );
    // 执行指定区域内未完成的绘制任务
    void FlushBatchDraw(int left,int top,int right,int bottom
    ); 

    后两者没有太大的区别,使用方法为

    BeginBatchDraw();
    绘制操作
    FlushBatchDraw();

    这样就可以让绘制的内容一次性被输出在画面上,而不分先后,从而解决闪烁问题。

[2]运用的数据结构知识

  1. 钢琴块元素的存储->链式队列

    • (1)使用背景:由于游戏模式是不断产生新的元素,再消除已被击中的元素,符合FIFO的特点,故用队列的结构来存储方块,实现过程如下

    • (2)流程:先定义钢琴块结构体,存储其相关信息,再定义队列

    • typedef struct _block
      {int x;    //这个是左下角的坐标int y;int fre;  //对于有声音的方块,存其频率int time; //对于有声音的方块,存其发声时长struct _block* next; //用的链式队列,可以不用担心溢出问题
      }b, * pb;
      typedef struct _queue
      {pb head;pb tail;int num;  //存储当前队列一共多少元素
      }que, * qq;
    • 再定义入队、出队的操作函数,包含指针的改变,队列元素个数的增/减,特别之处在于需要注意,出队时及时free(p)释放空间

    • void in_queue(pb nb)
      {q->tail->next->next = nb;q->tail->next = nb;q->num++;
      }
      //出队操作,这里就是把方块消失,不能再显示了,而且记得及时释放空间
      void out_queue()
      {pb tmp = q->head->next;q->head->next = tmp->next;free(tmp);q->num--;
      }
    • 结合游戏背景,设置判定条件,即何时入队(在队列的元素个数小于6个时,无bgm模式是只要有一个钢琴块被消除,立刻出队,并新入队元素;在有bgm的模式麻烦一些,因为可能一次性会加得总数超过6个),何时出队(统一规定,只要在击打区间击中,即出队,对于有bgm的模式还允许在方块的下端超出击打范围,判定miss时出队)这里以无bgm简单模式为例子,同时为了方便阅读,重点关注队列操作的内容,省去一些非相关的函数,转用一些伪代码

    • void init_block2(int n)
      {//先初始化队列和队头尾结点和一个结点q = (qq)malloc(sizeof(que));初始化队列pb nb;nb = (pb)malloc(sizeof(b));队头尾结点初始化q->head->next = q->tail->next = nb;q->num = 1;//再对剩余钢琴块初始化int i = 5;while (i){nb = (pb)malloc(sizeof(b));nb->y = (4 - q->num) * 110;nb->x = (rand() % n) * 100;nb->next = NULL;in_queue(nb);i--;}
      }
      //成功击中方块
      void hit_b23()
      {out_queue();
      }
      //持续获取新的方块,在有方块出队且剩余时,保持有6个方块,当方块数目<=6,开始继续获取方块
      void get_block2(int n)
      {pb nb = (pb)malloc(sizeof(b));nb->y = q->tail->next->y - 110;nb->x = (rand() % n) * 100;nb->next = NULL;in_queue(nb);
      }
      void game2(int n, int ac)
      {//先把初始化界面整出来init_block2(n);pb tmp = q->head->next;int now = 1;while (tmp && now <= 6){display_b(tmp);//将钢琴块显示在界面上tmp = tmp->next;now++;}设置暂停界面提示system("pause");char in = 0;int c, flag = 0;//开始正式游戏while (1){输出提示if (第一个节点为空)break;while (第一个节点没有超出击打区域){in = 0;if (q->head->next->y >= 550 - ac * ac * ac_unit)in = _kbhit();if (in != 0)break;清屏,开始批量绘图pb tmp = q->head->next;创建画面while (tmp){display_b(tmp);//显示每个钢琴块tmp->y += 3 * speed;tmp = tmp->next;}输出提示批量绘图,显示画面Sleep(20);//系统暂停20ms,以控制钢琴块移动速度}in = _getch();if (输入空格,暂停){while (1){in = _getch();if (再次输入空格,退出暂停,继续游戏)break;else{结束并退出游戏,记录数据}}}if (超出打击区域){//超出打击区域极限,判定miss记录数据,结束并退出游戏}else if (in && q->head->next->y >= 550 - ac * ac * ac_unit){//在打击区域,进行打击正确性的判定c = q->head->next->x / 100;if (in == key[c]){计分hit_b23();}else{int j = 0;for (j = 0; j < n; j++){if (j != c){if (key[j] == in){记录数据并退出游戏}}}}}if (q->num < 6)get_block2(n);else if (q->num == 0)break;}结束批量绘图显示游戏结束信息记录该局游戏信息
      }
  2. 本地数据的存储->双向链表

    • (1)使用背景:玩家进行游戏的次数未免会很多,而相应的记录数据量也会增大,在进行本地游戏记录查询时,会想浏览任意页数、任意行的信息,涉及向前和向后访问的需求,所以使用双向链表来存储(将数据进行9行/页的分组后的)每页的信息

      • (2)流程:定义页数信息

  • typedef struct _page   //每一页的数据{int n;             //这个表示存了多少行int data[9][7];    //一页最多9行,每行有7个数据,分数,连击数等char time[9][20];  //这个存游戏时间struct _page* last;struct _page* next;
    }p, * pp;
  • 双向链表基本操作,在创建新链表(涉及到是否需要创建一个新的页来继续存数据的判断),尾部添加新结点,读取结点信息

  • //从尾部添加新链表void in_link(pp link, int i, int* list, char* tt)
    {int j;for (j = 0; j < 7; j++)link->data[i][j] = list[j];strcpy(link->time[i], tt);
    }//读取txt文件形成链表
    void store_data(void)
    {rewind(data_fp);//让文件被重读int i, j;pp tmp = head;if (未读到文件末)将临时读取的值存入节点while (未读到文件末){存临时读取值if (i < 9)//也就是当读取的行数没到9行时{值存入当前页节点i++;}else{//创建一个新的页节点,并构建前后关联pp new_page = (pp)malloc(sizeof(p));new_page->n = i = 0;in_link(new_page, i, info, ti);new_page->last = tmp;new_page->next = NULL;tmp->next = new_page;tmp = new_page;new_page->n = i = 1;}}
    }//读取一页的信息并显示在窗口
    void read_data_new(pp pa)
    {int i = 0, j;清屏输出while (i < pa->n){输出pa第i行的数据i++;}
    }
  • 应用到主程序中,仍部分使用伪代码,由于涉及指针,所以用完后记得释放空间

  • else if (选择浏览本地游戏记录){对头指针初始化store_data();read_data_new(tmp);      while (1){in = _getch();if (in == 13)//利用双指针进行迭代,逐个释放空间{tmp = head;pp l = head->next;while (l || tmp){free(tmp);tmp = l;if(tmp)l = tmp->next;}break;}else if (in == 'w')//读上一页{if (tmp->last){tmp = tmp->last;read_data_new(tmp);}}else if (in == 's')//读下一页{if (tmp->next){tmp = tmp->next;read_data_new(tmp);}}}}

(4)未实现功能/待解决的问题

  1. 关于含bgm的版本

    本意是想在击打特定钢琴块时发出声音的,但由于beep函数本身占用CPU主进程,会导致界面得等声音发出结束后再继续更新,使游戏过程卡顿,这个可以尝试用threads.h库函数进行进程的分支,多线程操作,但是由于配置问题,没能成功安装该库,还有改进的空间

  2. 关于输入读取

    游戏只单纯地用了读取输入的_getch()函数,但遇到用户在短时间内输入大量内容,会使得缓冲区字符堆积,影响后续对用户输入的及时体验,目前未能找到一个较好的解决方法

五、运行结果

六、总结

第一次尝试做游戏,能在写函数的过程中,体会到与以往C语言编程、做题不一样的编程风格,也能将自己所学的数据结构知识用于其中解决一些实际问题,收获颇丰,更感受到了做软件的不易和工程量之大,首次尝试在界面上、游戏体验上仍存在着很多的不足,以及改进的空间,希望未来能学得更多,在编程之路上越走越远!

七、参考文章

[1] C++如何定义多个文件使用全局变量_hugongda123的专栏-CSDN博客_多文件全局变量怎么定义

[2] 控制台光标(一): 隐藏光标_nocomment_84的博客-CSDN博客_隐藏控制台光标

[3] getch()功能与用法_YuJar的专栏-CSDN博客_getch()的功能

[4] 函数kbhit()是干什么的?_百度知道

在此鸣谢帮孩子内测的uu们,以及各位优秀的csdn博主们(还有隔壁软编助教23333,整的那个中国象棋的板子很有参考性)

C语言实现钢琴块小游戏(低仿拉胯版)相关推荐

  1. C语言可以敲哪些小游戏,C语言可以写哪些小游戏?

    C语言可以写哪些小游戏? C语言可以编手机游戏.你叫他去死不过我这有贪吃蛇的代码,你倒可以看看(用TC编译一定过( #包括 #包括 #包括 #包括 #包括 #定义输入7181 #定义ESC 283 # ...

  2. 安卓c语言自动补全软件吾爱,C语言实现贪吃蛇小游戏

    本文实例为大家分享了C语言实现贪吃蛇小游戏的具体代码,供大家参考,具体内容如下 一.程序实现的原理: 1.构造蛇身:定义一个坐标数组,存放的是蛇的每一节蛇身所在的坐标位置.这样就将移动蛇身的操作转换为 ...

  3. C语言实训——经典小游戏马里奥开发day 1

    C语言实训--经典小游戏马里奥开发day 1 卷轴式地图 一.关于卷轴式地图 ​ 在之前的开发尝试之中,我曾经使用的是地图移动但是马里奥保持在一个固定的点来实现马里奥的移动,但是我发现了几个问题: 1 ...

  4. c++ 小游戏_C/C++编程笔记:C语言写推箱子小游戏,大一学习C语言练手项目

    C语言,作为大多数人的第一门编程语言,重要性不言而喻,很多编程习惯,逻辑方式在此时就已经形成了.这个是我在大一学习 C语言 后写的推箱子小游戏,自己的逻辑能力得到了提升,在这里同大家分享这个推箱子小游 ...

  5. 原生html小游戏,原生JS实现别踩白块小游戏(一)

    对于前端开发人员来说,闲暇之余自己开发个小游戏打发时间,也是对自己基础技术的一种应用考验.那么别踩白块小游戏,相信大家并不陌生,这个小游戏我们可以通过原生js来实现,即便是前端初学者也可以轻松完成. ...

  6. c语言射击类小游戏任务书,(c语言课程设计报告小游戏“石头剪子布”.doc

    (c语言课程设计报告小游戏"石头剪子布" <C语言课程设计>报告 题目:石头剪子布 班级: 学号: 姓名: 指导教师: 成绩: 目 录: 一.选题背景- 3 - 二.设 ...

  7. R语言入门——猜数小游戏

    R语言入门--猜数小游戏 题目介绍 使用代码(2种方法) 运行结果 题目介绍 设计一个猜数字的游戏:计算机随机生成一个1~100 之间的整数,然后由用户猜测所产生的随机数.根据用户猜测的情况给出不同提 ...

  8. C语言开发打气球小游戏

    C语言开发打气球小游戏 首先我们写一下项目所需要的头文件 #include<stdio.h>//标准输入输出头文件 #include<math.h>//数学库头文件 #incl ...

  9. 基于C语言的信息管理系统和小游戏

    基于C语言的信息管理系统和小游戏 一.课设题 本次课程设计有以两类题可以选择: 信息管理系统: 即实现个基于控制台的信息管理系统,例如选课系统.图书馆系统.公司财务系统等等 ,除满增删查改等基本功能, ...

  10. c语言做搬山游戏,C语言实现搬山小游戏,适合新手的项目实战,超易上手!

    原标题:C语言实现搬山小游戏,适合新手的项目实战,超易上手! 问题描述 设有n座山,计算机与人作为比赛的双方,轮流搬山.规定每次搬山数不能超过k座,谁搬最后一座谁输. 游戏开始时,计算机请人输入山的总 ...

最新文章

  1. 百度编辑器(1.4.3—net版)上传图片路径及其他配置
  2. LINUX系统常用操作
  3. Weka学习五(ROC简介)
  4. 解决WSL2中Vmmem内存占用过大问题
  5. RabbitMQ(一):Hello World程序
  6. 转发和重定向和request域对象
  7. 自然语言处理简介及开发环境
  8. LeetCode刷题(29)
  9. 数据科学包4-pandas核心数据结构
  10. 2016/2/24 1,dotctype有几种? 2,了解html的发展历史
  11. CCS软件仿真 手把手教你 CCS 软件仿真 TMS320F2812
  12. win10窗口设置眼睛保护色
  13. JSMInd实现动态思维导图的保存和展示
  14. 夜晚网速变慢与网站服务器开机数量减少有关,网速变慢的原因及解决办法
  15. 2010-01-22 | 占豪收评:牛市继续还是进入熊市?
  16. os系统服务器防火墙怎么关闭,mac防火墙如何关闭
  17. NVIDIA Jetson之GPIO引脚设置
  18. Halcon 3D 常见算子描述
  19. 多模态(红外,可见光)目标检测
  20. android提取图片颜色代码,Android 中动态提取图片中颜色作为主题色

热门文章

  1. python中pillow是什么意思_Python-pillow
  2. TypeScript 之 More on Functions
  3. 软件测试工程师面试题之(计算机网络下)
  4. 记录一下unity3d资源加载Resources.Load资源加载的坑
  5. 最近在测FREEBSD平台下ISP邮件系统前的准备知识摘录
  6. python 中搞错工作路径的意思导致的相对路径产生bug:[Errno 2] No such file or directory:
  7. mac的 tr命令_tr命令 - Holy_Shit - 博客园
  8. mysql 未找到 WinSxS_window_win7系统如何使用WinSxS工具安全删除WinSxS文件夹垃圾?,WinSxS文件: WinSxS是系统文件Wi - phpStudy...
  9. 雅典娜暴利烹饪系列(下)
  10. 自下而上分析方法-算符优先,LR(0),SLR,LR(1),LALR大全