14. C语言从零实现推箱子游戏

1 C语言入门

学习编程之前,我们必须要明白一点:编程的过程,实际上就是人与计算机对话的过程

就和中文、英文一样,编程语言也是一门语言,只不过与这些自然语言不同,编程语言是一种有限制性的语言而已。

什么是限制性?

那就是一种语义有其固定的规则写法,而自然语言,你却可以任意组合近乎无穷的表达方式。

比如一个动作:吃饭

那么编程语言的规则可能就只有这一个(或少数几个)形式:吃饭,**你吃饭 **之类的。

而自然语言,你却可以随便说,只要别人能理解你的意思就行了,比如:吃中饭!吃晚饭 之类的。

1.1 C语言是什么?

正如自然语言有中文英文日语等等。

编程语言也有很多:C、C++、python、java、go、rust……

而C语言就是其中一门非常古老的语言。

虽然古老,其直到今天依旧生命力顽强,所以对于新人来说,我依旧建议你将它作为第一门编程语言进行学习。

因为它几乎是现代互联网的基础,你如今所能看到的一切:电脑、手机、智能手表、汽车……都有它的影子。

原因无它,就是因为它接近底层,效率足够高。

一般来说,越靠近硬件的就越底层,执行效率就越高。

它可以让你肆无忌惮、随心所欲的操控你想操控的一切,而代价就是,编程过程中充满着凶险,开发效率缓慢等。

但无论如何,我认为学习它都是有必要的,因为它深入底层的这个特性,你才能透过它真正理解一切。

学会了它,以后你再去学习其它更高级的语言,只会觉得轻松~

嗯……可能还得学习C的大哥C++之后才会有这种感觉,可以参考本站的其它C++系列教程。

因为如今的高级语言,大多数都是在C(或C++)上开发而来的。

1.2 如何学习?

我并不希望从0开始,硬给你讲述一些固有的规则,那实在是太无聊的,因为这些内容你在互联网上已经可以随处可见、任何不懂的知识点都可以询问ChatGPT、DeepSeek等AI。

为了能够让你感受到编程是一件很酷的事情,本文会以项目为主导带大家学习。

也就是说,我会从一个有趣的项目开始,从0带着大家开发,而从这个过程当中,我们一定会遇到很多难点、问题。

之后,我们就会想办法去解决它,从中学习知识。

死板的知识是无趣的,只有将学到的东西应用到实践中,从实践中学习到知识,才能让学习变得有趣!

2 项目介绍

为了让学习C语言的过程没那么无聊,本文会以一个经典游戏推箱子为主导来学习相关知识点。

这个游戏非常的经典有趣,代码量也算是中规中矩,最终大概有三百行左右的样子,再加上其中可以运用到很多各个编程方面的知识,拿来练手最合适不过了!

最终完成后呈现出来的效果如下:

image-20230822082721118

当然,如果你稍微了解过一点C语言,都是知道C语言本身是写不出这样好看的游戏界面的。

对,C语言本身确实写不出来,但它可以调用其它第三方库啊!

因此这个项目涉及到的知识点还是很多的。

基本的C语言语法就不说了,那肯定是会用到的。

不敢说全部用到,但一定会用到其中相当一部分知识点。至于没有用到的知识点,也不用慌,可以再去进阶学习本站《C/C++实战入门到精通》等系列文章。

除此之外还有:文件操作GUI编程第三方库的使用等等。

3 了解软件

无论要做什么,首先从一个较为宏观的角度去了解它都是一件非常有必要的事情。

不过初学者却很难站到一个更高的角度来审视,而这也就是本教程所存在的意义了。

本教程最终实现的目标是:推箱子小游戏。那么第一个问题就是,什么是游戏?

3.1 理解游戏

广义上来说,游戏当然不仅仅是指电脑、手机游戏,比如小时候玩过的捉迷藏,当然也可以叫做游戏。

但由于本教程是教编程相关的,所以这里指代的游戏,就可以狭义的称其为游戏软件

游戏软件,与你手机、电脑上用的其它软件,比如QQ、微信、支付宝等,并没有什么区别,都是软件。

它们都是运行在电脑、手机上的一个个程序而已,只不过游戏更侧重于娱乐,而其它软件则更侧重于实用,仅此而已。

3.2 什么是程序?

上面我提到了一个词:程序

广义上讲,就是规定的一系列步骤,比如现实世界中的司法程序

而狭义上来说,计算机所运行的一系列指令组合起来,实现某一个功能,便可称其为程序。

也许你已经听到过无数遍了,计算机的本质其实就是二进制的0和1。

无论是你看的视频、听的音乐、玩的游戏,在计算机眼中都是二进制的0和1,除此之外,计算机就什么都不认识了。

那计算机为什么能仅仅依靠0和1两个数字就能展现出我们所能看到的如何繁荣的互联网呢?

答案就是数量

虽然一位数字只能表达两个信号:0或1,即 212^1

但如果是两位,那就能表达四个信号:00、11、01、10,即:222^2

而如果是8位呢?那就能表达 282^8 个信号。

这样能表达的含义不就丰富起来了吗!

比如用 00001111 这个8位的二进制数字信号,人为的规定其是做乘法运算的,00001100 这个8位的二进制数字信号,人为的规定其是做加法运算的。

那么计算机便能只在认识0和1这两个数字的基础上,完成超越二进制的加法以及乘法运算了!

而这就是早期的机器语言,即二进制编程,面向机器编程。

这样的机器指令可能有数百个之多,人为记忆起来是很困难的。

那我们就可以用我们所知道的符号来代替它呀!

比如:用符号mul代替 00001111 这个数字,用符号add代替00001100 等。

这样我们写代码的时候,就不用写这些二进制数字了,而是可以直接用这些符号来写了。

写完之后呢,再通过一个提前写好的程序,来将这些符号重新转化为对应的二进制指令数字,交给计算机执行不就行了?

第一个将符号转换为二进制数字的程序肯定是用机器码写的,其开发的痛苦程度都不用多说了!

这便是汇编语言的由来,它的每一个符号都对应着一段二进制机器指令。

但汇编指令写起来仍然很痛苦啊!

举个例子,以下就是用汇编语言打印 hello world 的代码:

DATA SEGMENT  
    BUF DB 'HELLO WORLD!$'  
DATA ENDS  
CODE SEGMENT  
    ASSUME  CS:CODE,DS:DATA  
START:  
    MOV AX,DATA  
    MOV DS,AX  
    LEA DX,BUF  
    MOV AH,09H
    INT 21H  
    MOV AH,4CH  
    INT 21H  
CODE ENDS  
END START  

写好上面这串代码后,我们还需要找到对应的汇编程序将其翻译为二进制后,才能让计算机来执行。

是不是只是看着就很绝望?

为了简化程序员的负担,后面就出现了更加高级的语言,也就是我们本系列文章要讲解的C语言:

#include<stdio.h>
int main(){
	print("hello world");
	return 0;
}

同样是输出一个hello world,是不是比汇编要简单多了?

不过其原理同样不复杂,就是在汇编的基础上,又添加了一层而已。

当你写好了这段C语言代码,通过对应的C语言编译器,就会将其翻译为汇编代码,最终再通过汇编程序将其翻译为计算机能够理解的机器码,也就是二进制数字。

虽然这样做可能会损失一部分性能,但如今的编译器非常强大,与为我们提供的开发速度来说,这点损耗已经完全算不上什么了。

甚至可以说,就算你手写汇编,也不一定比用C语言编译器编译后的汇编性能更好。

而第一个C语言编译器程序不出意外的话,自然也就是用汇编语言写的了。

但后面的C语言编译器,便是用C语言本身写的了,毕竟开发效率提升太多了!

语言本身写自己的编译器,称为自举,并不是任何语言都有这种能力。

以上,当我们用C语言写好一段代码后,程序的执行流程:

  1. 将C语言代码翻译会汇编代码
  2. 将汇编代码翻译为机器码
  3. 计算机执行机器码

而作为程序员来说,程序,常常就是指代的我们所写的代码。

对于本系列来说,也就是C语言代码。

通过写代码,我们就能够让计算机做一些我们想让它做的事情。

4 认识VS

前面可能有点无聊,因为都是一些比较空洞的概念。

但总体来说,你只需要知道,当我们写好C语言代码后,是需要依靠一个叫做编译器的东西将其翻译为二进制的,就可以了。

而本节,就是来介绍这个编译器的。

4.1 编译器认识

编译器并不是只有一个,目前主流的操作系统主要分为两类:window系统、类unix系统。

其中window系统一般用的都是vc++编译器,而类unix等系统上用的则是GCC、Clang等编译器。

但比较烦人的是,这些不同的编译器实现的规格并不完全相同。

所有很多时候同一份C语言代码在某个编译器下能正常运行,可能在另一个编译器下就会报错。

因此在写C语言代码的时候,除了尽量用通用、标准的写法,也最好是提前知道自己的代码最终要使用哪个编译器上,就直接用该编译器进行开发。

当然,这种情况也是极少数的,大部分的C语言代码,在各个编译器中都是统一、可正常运行的。

4.2 安装编译器

那么首先我们第一步,就是来安装编译器了。

就目前来说,PC端的游戏一般都是运行在windows系统之上,大部分软件,第一适配的系统也必然是windows系统。

并且大部分新人使用的也应该是windows系统, 加上我个人比较熟悉windows系统。

因此本系列教程将以windows系统上的vc++编译器作为本系列教程所使用的编译器。

相比于其它编译器,vc++编译器是其中做的最好的(好在其有一个强大无比的集成开发环境),因此新人用来入门应该也是最容易的。

官网地址为:Visual Studio

点击跳转进入官网后,下载最新版的Visual Studio Community版本即可:

image.png

安装过程很简单,就一路点击即可,中间选项任意,唯一需要注意的点是下面这一步:

image-20230824144625557

这个软件为集成开发环境(Integrated Development Environment,简称IDE)。

它不是编译器,但它包含了编译器,它是一个极其强大的综合体,这一点我们以后会体会到的。

这可以从它的官网介绍中看出一二:

image-20230824163927115

而这里勾选的就是编译C语言代码的组件,你可以从它的内容中看到:MSVC、CLang等字眼。

其中MSVC便是前面提到的vc++编译器,Clang就是前面提到的clang编译器。

其默认使用的是msvc编译器,这个直接默认安装即可,不用理会。

如果你C盘容量不够,可以更改安装位置,我这里是直接安装到C盘的:

image-20230824145152963

选择完成后,右下角有个安装按钮,点击安装,等待即可。

完成后,你就能看到一个类似下面这样的界面:

image-20230824145354803

然后通过创建项目,我们就能够开始写代码了!

但为了让你能够更好的理解项目与我们所写的代码之间的关系,会在这里先暂缓,下文来介绍一下VS(Visual Studio的简称)中的各种逻辑、组织之间的关系。

5 hello world

有了编译器,我们就可以正式开始写代码了,而学习编程语言一个传统便是,第一步先尝试输出hello world。

为了输出hello world,我们就需要写程序。

由于VS是以项目为基本运行单位的,所以我们首先就需要创建一个项目:

image-20230826072557362

项目也是有很多类型的,所以VS提前给我们准备好了很多可以直接用的项目模板。

但既然我们是学习,那自然还是从0开始了,即:选择空项目

image-20230826072700974

最上方的红框可以用来筛选项目模板,选择好我们这里要创建的空项目,就可以点击下一步了。

然后在下一个配置页面,我们就用上了上一章介绍的知识点:

image-20230826072911906

位置不说了,就是存放这个项目的文件夹,可以自行选择。

其中项目名称、解决方案名称,就是前文提到的项目、解决方案。

解决方案是包含项目的,所以一般来说,最下方那个:将解决方案和项目放在同一目录中,最好不要勾选。

除非你觉得你这个解决方案只会有这一个项目,那勾选了倒也没啥。

两者可以分别命名,我这里就直接默认了,然后继续下一步,点击右下角的创建即可。

VS强大而复杂,所以布局很多,但不要慌,其它的暂时都不用管,首先第一步就是找到解决方案资源管理器(一般在右边且默认打开):

image-20230826073354023

如果你的界面没有这个,可以从左上角的视图菜单中调出来:

image-20230826073535284

现在解决方案里面就只有一个项目,而这个项目还是空的,所以我们还需要为其创建一个源文件,用来写代码:

最简单的方式就是点击这个源文件目录,然后按快捷键:Ctrl+Shift+A

image-20230826074044428

注意:前面的名字无所谓,但要将其默认后缀.cpp改为.c

因为实际上这个VC++编译器是C++语言的编译器,但C++完全兼容C语言,所以可以直接用,但为了让其能够识别出这是纯粹的C语言代码,就需要改一下这个后缀名。

这个快捷键哪来的?你可以直接右键这个目录也能一路找到添加文件的选项:

image-20230826073910578

然后,你就可以在这个文件中写下这段代码:

#include<stdio.h>
int main() {
	printf("hello world");
}

样式如下:

image-20230826074411237

写好后,点击上方的绿水箭头,就能编译运行了:

image-20230826074516437

结果:

image-20230826074548736

是不是非常方便!

在这个IDE中,我们只需要按下一个按钮就可以直接将代码编译完成之后,直接运行!

但还是要记住,它依旧执行了两个步骤:先编译,再运行。

那么编译后的二进制可执行文件呢?

它就存放在这个项目文件夹中,你也可以从上面的输出中看到它的位置:

image-20230826074820903

在文件浏览器中也能找到:

image-20230826074925583

但你现在直接点击运行它是看不出效果的,你大概只能看到一个黑色窗口一闪而逝。

这是因为我们的代码目前只输出了一个hello world就结束了,程序结束了,窗口就自然被销毁了。

后面我们会了解到如何让这个窗口保留下来。

6 了解内存

虽然可能有点啰嗦,但我发现有些新人即使已经写了几个月的代码了,却依旧不知道代码是怎么运行的。

所以还是决定稍微啰嗦一下。

因为受早期很多手机厂商宣传的影响,很多人都会把内存当作设备的存储空间大小。

但实际上,至少在编程领域,内存一般都指的是计算机的运行内存

以前面编译生成的那个存放在文件夹中的二进制可执行文件(.exe)为例,它存放的位置是在硬盘中,而不是在内存中。

一般C盘、D盘的大小,指的是硬盘大小,用于长期存放数据的,即使你电脑关机,之后打开电脑,硬盘上的数据依旧存在。

而内存,则必须要时时刻刻通电才能保存数据,只要你关机,那么数据就会立刻丢失。

而想要运行一个软件,就必须要将这个软件从硬盘加载到内存中,才能运行。

因为离CPU近等等原因,内存存取数据的速度比硬盘要快得多。

所以,当你双击一个应用程序时,实际上其本质就是在将这个可执行文件的数据,加载到内存上去,这样计算机才能够运行它。

那么如何才能看到内存呢?

这可以从windows系统自带的任务管理器中查看。

快捷键为:win+X,其中win是键盘上画着窗口图案的那个键。一般在alt键旁边。

然后你就能从调出来的菜单中看到这个任务管理器,打开它即可:

image-20230827102702084

然后就能在性能页面,看到内存:

image.png

比如我这里,就是32G的内存,已经用了14.8G了。

后面编程过程中,我们所有分配的空间,实际上都是在分配这个内存。

计算机上所有的应用程序使用的内存,都是在共用这个内存。

当这个内存占用量过大时,电脑就容易出现卡顿,此时你就可以选择去关闭一些占用内存大的应用来解决。

7 C语言基本语法

前面讲了这么多,终于来到了我们的正题:编程。

但光讨论编程细节、语法,是很没意思的,因为这种东西网上随便搜一搜都有一大堆。

所以本文会结合游戏推箱子,来探讨我们实现它具体需要哪些东西,而C语言又有哪些语法刚好可以使用。

如果你真的零基础,那么最好在我提到一个知识点后,就尽量将其理解透彻,比如去浏览器搜一搜相关的关键字、问问AI大模型,否则后面你可能难以跟下去。

7.1 基本代码结构

但在正式分析游戏前,了解一下C语言的基本结构是很有必要的,不然分析起来就会有种空中楼阁的感觉。

正如前面所提到的,一个C语言输出hello world,就需要以下几行代码:

#include<stdio.h>
int main() {
	printf("hello world");
   return 0;
}

如果我们不需要输出字符串,那么上面的结构还可以进行简化:

int main() {
    return 0;
}

也就是说,一个最简单的C语言程序,就是上面这个形式,这是每个C语言程序所必须的。

这被称为主函数,又称main函数,因为它的名字是main

函数的内容我们后面再提,因为它还有点小复杂,目前你只需要记住这个格式即可。

其中的return 0;,这条语句的作用是返回这个函数的结果,在VS中就算你不写也不会报错,因为编译器会自动帮我们添加上。

并且我们需要知道的是,当将上面这份代码编译执行后,计算机就会从这个main函数的内部开始,遇到一句,执行一句。

C语言的每条语句都是用英文分号;作为结束,这就和中文的文章中,用句号作为一句话结束是一个道理。

有了这个结构,那么编译器就已经能够将其编译运行了。

但如果我们想要让它做点事,那就需要写语句了,即代码。

7.2 分析游戏逻辑

将推箱子游戏抽象一下,其实就是一个二维表格,

image-20230827133228976

用数字1,代表墙壁,数字5,代表箱子,数字2,代表金蛋,而数字9,代表我们的人物角色。

那么我们的规则就有了:

  • 数字9代表人物,可以在表格中移动,但不能越过墙壁1,且可以推动箱子5。
  • 游戏的目标就是将箱子5需要推到金蛋2的位置。

只要我们把这个简单的游戏逻辑写好,之后再将这些数字替换为图片不就行了吗?

所以首先我们的第一个问题就是,如何表示这些数字,并让其可以移动。

7.3 二维数组、数据类型、变量

想要在C代码中表达一个二维表格很简单,可以像下面这样写:

int map[10][10];

这就是一个10*10的二维数组,你可以将其想象为上面看到的二维表格,并且我们还给它取了一个名字为map,即地图的意思。

这个名字称为变量名,它有一定的规则,这个可以自行网上查询,比如:C语言变量名命名规则

规则并不难,简单来说就是,可以用全字母组合(a-z,A-Z)的任意变量名,或者还可以添加下划线_,又或者添加数字(0-9),但数字不能作为变量名的开头。

而前面的int,标准说法就是数据类型,意思就是这个二维数组里面将用来存放整数。

int,即为整数单词integer的缩写,这是C语言的规定,不必想太多,记住即可。

C语言中的数据类型有很多,但本系列只会用到几个基本的数据类型,为了节约篇幅,可能不会细讲,具体内容可参考本站的其它文章。

再次提示,最后是用的英文分号作为结束的,英文分号,英文!很多新手写代码,就会敲上去中文分号,一运行就报错!

结合前一章的内容,这句话的作用就是,在内存中分配一块可以存放10*10大小的整数二维数组内存空间,用于后面我们使用。

7.4 循环

只是有这样一个二维数组可能还不够,因为这块内存是系统分配给我们的,那这块内存以前存放的什么数据,我们怎么知道呢?

前面说过,内存是电脑所有应用共享的,而你得到的这块内存,很有可能是刚被其它应用程序使用完后,就马上交给你用的,此时这块内存上的数据完全未知。

所以一般来说,我们第一步都会先将其清0,

用最笨的方式,就是一个一个的去赋值,就像这样:

map[0][0]=0;
map[0][1]=0;

10*10=100,所以你需要这样写100次,才能将其全部赋值为0。

数字0的含义前面没有提,我们可以认为将其赋予为空,即作为游戏草地背景。

二维数组的使用方式就是像上面那样,第一个[]中写你想要访问第行,第二个[]中写你想要访问第几列。

无论行还是列,都是需要从0开始计数,也就是说,长度为10代表0-9可用。

那么这个时候,我们就需要用到循环了,它可以简化这种重复的步骤:

for(int i=0;i<10;i++){
	for(int j=0;j<10;j++){
		map[i][j]=0;
	}
}

循环对于新人来说,可能也有点复杂,但不要紧,我们可以从里到外刨析。

上面的代码完成了一件事情:将map这个二维数组全部清零了。

并且我们并没有写一百次map,而是只写了一次map[i][j]=0

i代表二维数组的第几行,j代表二维数组的第几列,它们也叫变量:可变的量。

显然,这两个数字要分别能从0变化到9,这个赋值操作才能完成,而这就是循环的目的。

一个最简单的循环写法为:

for(;;){
}

也就是用单词for来写循环,后面紧跟小括号(),然后是大括号{}。

其中,小括号中还有两个分号;,将其分为了三个区:

  • 第一个区:用于初始化变量,比如上面的int i=0int j=0,这就是在初始化两个int类型的变量,名字为i和j的,初始值为0。
  • 第二个区:用于判断是否继续循环,比如上面两个都是i<10,j<10,即,只有这两个变量小于10,这个循环才会持续下去,否则,这个循环就结束了。
  • 第三个区:用于存放每次循环执行的代码,比如这里就是,每次循环,就会执行i++j++,它们等价于i=i+1,j=j+1,即自身加1,每次循环都会执行一次。

而后面的大括号,就是每次循环要执行的内容了。

因此,一个for循环:

for(int i=0;i<10;i++){
	
}

意思就是执行十次后面的大括号中的内容,而每次执行,i都会自增1,这个特性就可以用来作为二维数组的行。

然后在循环中再放置一个循环,也就是二重循环:

for(int i=0;i<10;i++){
	for(int j=0;j<10;j++){
	
	}
}

由于第二个循环在第一个循环的大括号中,所以其达成的效果就很明显了。

外层的i每次循环一次,内部的j就循环10次(0-9)。

由于外层i一共要循环10次(0-9),因此内部j就循环的100次,分十次循环(0-9),这不就正好和二维数组对应上了吗?

第一行有10列,第二行也有10列,以此类推,共100个。

这个如果想不明白的,最好再去网上搜一搜、问问AI,理解明白再往下看,对于游戏来说,数组很重要。

只不过因为这个游戏是二维的,所以这里介绍的是二维数组,其实还有更常用的一维数组,也就是只有一个[]符号的。

同样的,循环也是,一般一重循环用的最多,但由于这里是二维数组,所以就用的更难的双重循环。

完整的代码如下:

int main(){
    int map[10][10];
    for(int i=0;i<10;i++){
        for(int j=0;j<10;j++){
            map[i][j]=0;
        }
    }
}

8 让游戏一直运行

前面我们通过数组、循环,就完成了游戏地图的建立:二维数组所对应的游戏地图。

但那显然还是不够的,所以本节继续在此基础之上,去完善它。

前面我们已经初始化好的游戏地图数据对吧!

但你会发现,程序一直都是从main函数内部开始,看到一句,就执行一句,执行结束后,程序就结束了!

可我们看到别人的游戏、软件怎么不是执行一会就自己结束呢?

难道是他们写的代码数量超级超级多。让系统都执行不完吗?

显然不是的,毕竟以如今计算机的运行速度,你就算写上十年的代码,估计也能很快给你跑完(可能只需要花费几十秒乃至几秒)。

而这就用到了上一章提到的循环了,它可以让同一块代码反复的执行,只要让它一直循环起来不就行了吗?

死循环的方式很简单,只要你不写for循环中三个区域中的中间那个判断条件区域,它就是死循环。

所谓死循环,意思就是一直循环执行代码,没有跳出循环的条件、可能性,就像这样:

for(;;){
}

只要什么都不写,那它就是个死循环,会一直执行后面大括号中的代码。

前面提到过,当我们找到编译后的可执行文件时,直接点击它运行只能看到它一闪而过,解决方式也很简单,在最后面添加一个死循环不就行了嘛!这样程序就永远不会执行结束了!

这样写着可能有的人觉得不太好看,所以也有另一个循环可以使用:

while(true){
}

while循环后面的小括号内,就只有一个判断条件,我这里直接让这个判断条件为true,那它自然就会永远执行下去!

它就相当于只剩下第二个判断区域的for循环,看自己喜好,用哪个都行,作用都是一样的。

8.1 判断语句

但你又发现了,如果写死循环,那里面的代码不就千篇一律了吗?

比如前面用到过的输出hello world的代码,如果将其放在死循环里面,那就会一直输出hello world,这有什么用?

所以这个时候就需要判断语句了,它可以让你即使是在同一段代码中,也能在不同的情况下使用不同的代码片段。

#include<stdio.h>
int main() {
	for (int i = 0;;i++) {
		if (i == 0) {
			printf("i==0");
		}
		else {
			printf("i!=0");
		}
	}
}

比如上面这段代码,由于for循环我没有写第二个判断区域,所以它就是一个死循环。

但我写了第一个区域,声明了一个整数int类型的i变量,并将其初始化为了值0。

并且每次循环执行结束后,都会执行一次i++操作,等价于i=i+1,即自增1。

然后我就在代码中写上了一个判断语句,就判断这个变量i是不是等于0的,根据这个条件,我就可以去执行不同的代码。

判断语句用到的关键字就是if,其后紧跟的小括号内填的是判断条件,这个和while语句后面的小括号功能是一样的。

唯一不同的是,if判断语句只判断一次,而while判断语句会一直循环的进行判断。

C语言中,乃至绝大部分编程语言中,一个等于(=)是赋值操作,意思是将右边的值赋值给左边,而两个等于=才是判断两边是否相等,而不等于则是用感叹号加等号表示(!=)

如果判断为真,那就会执行其后的大括号中的语句。

至于后面的else关键字,相当于是if判断语句的扩展,也就是分支,如果其判断语句为false,那就会执行后面else紧跟的大括号中的代码了。

if(){}
else{}

也正是因为其有这个特性,才具有根据不同情况去执行不同代码的能力。

但单一判断条件,在很多情况下依旧不够,还需要更多的判断条件,那就可以再在ifelse中间添加else if语句:

		if (i == 0) {
			printf("i==0");
		}
		else if (i == 1) {
			printf("i==1");
		}
		else if (i == 2) {
			printf("i==2");
		}
		else {
			printf("i!=0 且 i!=1 且 i!=2");
		}

else ifif语句本身使用起来基本是一样的。

其调用逻辑仍然是从上到下,挨个判断,只要有一个判断条件为真,那就不会再判断以及执行后面的了。

else if可以中间无限添加,而这就能赋予程序无穷的功能。

9 接收键盘输入

但显然,这仍然是不够的,毕竟现在的判断条件是我们自己写的,而游戏明明是玩家通过按键盘来执行对应的步骤啊!

对的!判断是基础,用户是可以输入很多按键的,所以我们就需要判断用户每一次输入的是什么内容,这样我们才能根据不同情况,让游戏做出不同的反应!

首先需要说明的是,用户输入的形式是可以多种多样的。

比如最常见的就是鼠标、键盘、屏幕滑动点击。

除此之外,游戏手柄、VR设备等,同样可以作为用户输入相关信号的通道。

而这些一般都统称为输入设备,本文要讲的就是输入设备中的一种:键盘。

9.1 库

但在正式介绍如何接受键盘输入的内容前,我们有必要来了解一下:

作为当代程序员来说,从某种程度上来看,我们是幸运的,因为很多东西都已经有前人为我们做好了。

也就是说,大多数情况下,我们都不必从零开始,自己去开发一个功能,而是可以直接使用别人写好的东西。

在编程语言中,称这种别人写好的代码为:包、库、箱等等称呼。

但名字不重要,你只需要知道其本质都是一个意思:使用别人写好的代码集合。

库的质量是良莠不齐的,新人写的库一般情况下都没有老手写的库好用。

为了保证库的质量,让程序员可以放心使用,就出现了一个叫做标准库的东西。

标准库,就是设计、规范该语言的官方团队所写的库,无论是质量,还是性能,都有保障,我们可以直接在代码中使用它们。

比如前面我们用到的打印函数:printf,这就是标准库中的东西,是官方提供的。

而它所在的地方,就是一个叫做stdio.h的头文件中,所以就有了这种代码:

#include<stdio.h>
int main() {
	printf("hello world");
}

前面的指令:#include<>,意思就是包含,即:将这个文件中的代码包含到当前文件中的这个位置。

而这个stdio.h中就实现了这个printf函数,因此你才可以在这里直接使用,并打印出字符串到控制台。

如果没有官方提供的这个函数,你就需要自己去寻找不同系统的底层函数说明文档,才能实现这个功能。

stdio.h这个名字也是有一定含义的,前面的stdStandard,标准的意思,后面的io即:input/output两个单词的首字母,即输入输出,最后的后缀.h,意为这个文件是C语言的头文件(header)。

所以一切关于输入输出相关功能的函数,基本都可以在这个文件中找到。

而这里的printf,就是输出功能的函数。而我们想要接受键盘输入的函数,同样也在这个库里面,这个后面马上就会提到。

9.2 函数

前文中我提到了很多次函数这个名词,在数学定义中,函数一般被写为下面这种形式:

y=2x+5

而更高级一点的写法,则是这种:

y=f(x)
f(x)=2x+5

这虽然只是做了一个替换,但实际上其中的思想却是很值得学习的。

比如很火的科幻作品三体,里面有三个太阳,我想要预测其行动轨迹。

虽然这被证明是不可预测的,但假设真的可以预测,其中的运算过程复杂度都是我们无法想象的。

而作为普通人来说,或者对于大人物来说,他们会在意中间的运算过程吗?不!他们只看重运算结果!

也就是这里的抽象化:y=f(x);

我想要结果y:太阳的运行轨迹,至于这个y是怎么算出来的,大部分人其实根本不关心,所以将其抽象为一个f(x)这样一个形式,而这里的x就是这个函数参数。

也就是要算出这个结果,它需要什么东西?比如要十年前的太阳位置、百年前的天文数据等等。

将这个思想放在代码中也是一样的,比如我们就想要将字符串打印到控制台上,至于里面是怎么实现的,大部分人根本就不关心。

控制台,也称黑窗口、终端等等,就是当你运行程序后弹出来的那个黑色窗口,没有任何图案,只能显示字符的窗口。

这便是函数的意义:封装。

在C语言中,写一个函数就和你写main函数是一样的:

//函数实现
int fun1() {
	return 0;
}
int main() {
	fun1(); //调用这个函数
}

但要注意,不同函数在C语言中是不能重名的!

函数分为实现与调用,调用很简单,就是写好函数名,后面紧跟的下括号中填入这个函数需要的参数就行了。

而实现就稍微复杂一些了,主要分为:返回类型函数名函数参数函数体这四大部分。

返回类型就是这里首先写的int,即整数,普通函数一般编译器就不会像main函数那样,为你填充返回值了,所以你需要自己写return将这个函数的结果返回回去。

而函数名,主要就是记住不能重名,其它的和变量名规则一样的。

函数参数填入小括号中,只不过这里我们没写。

函数体就是后面大括号中的内容,就是当这个函数被调用的时候,具体要执行的代码。

比较重要两点是:

  • 无论你写多少个函数,程序运行后都是从main函数开始运行的,遇到一句代码,就执行一句代码,遇到一个函数,就跳转到这个函数内部去执行,执行结束后再跳回来继续向后执行。
  • 函数的实现(或者声明)要在调用之前。

函数真要讲起来,细节非常多,比如这里的函数声明、函数实现又是一个知识点。

但为了节约篇幅,所以这里点到为止,后面用到了咱再继续讲,你也可以自己去网上搜索相关的资料进行学习。

9.3 键盘输入

对于大多数新入C语言的同学,使用的都是scanf函数来接受键盘输入。

但在VS中直接使用这个函数,是会报错的,因为它不安全。

至于其原因,涉及的比较底层,这里先不深究。并且要说明的是,这是编译器的原因,其它编译器并不会报这个错误。

解决办法有两个:

  • 在代码最前面写上:#define _CRT_SECURE_NO_WARNINGS
  • 使用安全的函数:scanf_s

这个函数就可以用来接收键盘的输入,比如我想写一个接收w、s、a、d这四个按键的,作用是控制游戏角色方向。

那就可以这样写:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	for (;;) {
		char d;
		scanf("%c", &d);
		if (d == 'w') {
			printf("按下了上键\n");
		}
		else if (d == 's') {
			printf("按下了下键\n");
		}
		else if (d == 'a') {
			printf("按下了左键\n");
		}
		else if (d == 'd') {
			printf("按下了右键\n");
		}
	}
}

除却这四个键之外的按键我们不想管,所以这里没写else语句。

scanf函数可以接收很多个参数,多个参数用英文逗号隔开,其中最重要的是第一个参数:格式控制参数。

它是一个字符串,用于表示我们想要接收哪种格式的输入,这里我们想要接收的是按键字母,那就是char类型。

与int整数类型一样,char字符类型也是一种数据类型,用于存储一个字符。

比如这里填入的%c就是格式控制,告诉这个函数我们想要接受的是字符类型,如果是整数类型那就是%d,这个可以自己用到的时候去浏览器中查即可(或者问chatgpt)。

然后当程序运行到这个函数时,就会卡在这里,等待用户输入(要按下Enter键该函数才会开始接收)。

接收到的内容就会填入后面的参数中,比如这里的char d;

在接收用户输入时,需要在变量名前面添加一个&,为取址符号,意为拿到对应的变量在内存中的地址,然后将接收到的内容填入进去,新手很容易在这里栽跟头:不加&符号!

效果如下:

image-20230829142746517

这样确实可以做到接收用户输入,并按情况执行不同的代码了。

但很明显,这样一卡一卡的,每次都需要按下Enter键才能开始接收的效果,太差了!

遗憾的是,标准库中提供的函数功能很有限,不存在那种可以直接接收按键的函数。

好吧……非要细究的化,其实也是有的,只不过很不好用,因此我用的是第三方库提供的函数。

所谓第三方库,就是除却标准库外、由其它程序员开发的库,其中不乏比标准库还优秀的存在。

现在我们的目的是:完成整个游戏的基本框架逻辑!

10 游戏地图绘制

有了前面的基础,虽然你可能觉得到目前为止你学到的东西并不多,但其实这就已经足够让你完成一个简化版本的推箱子小游戏了。

本节将完成游戏的地图绘制。

完成了基础版本的之后,再根据这个版本的不足之处,后面学习其它的新知识点,来将它逐步完善!

10.1 输出

前面我们其实就用过了输出函数,也就是printf函数。

别看它是入门级的函数,但其复杂度却丝毫不弱,最简单的用法就是打印一个字符串:

printf("hello world");

什么是字符串呢?这同样也是一个数据类型,而且是各种数据类型中最复杂的一个。

所以这里并不打算对它进行深入的解释,对于使用来说,其实只需要知道其最简单的用法就可以了。

显然,你应该已经知道了它的用法,那就是用过两个英文引号包括起来,中间的那一部分就是字符串,可以用来输出的控制台上。

但其实际上的用法还不止于此,因为它还可以用来格式化字符串,比如,我想要按格式,打印年月日:

printf("2023");
printf("-");
printf("09");
printf("-");
printf("01");

上面的输入结果为:

2023-09-01

这显然有点复杂,不是吗?

所以就可以用格式化的写法,等同于下面一句话: