我的 C 语言学习历程
2011年12月04日 · 12469 字 · 阅读
前言
这篇文章整理自作者 2011 年 12 月在 CSDN 论坛发表的《我的C语言学习历程》一贴,由于当时作者写作水平有限,想到什么就写什么,内容并未经过认真的组织,因此内容质量比较差,现在看这内容时还是挺让人尴尬的,比较庆幸的是当时看完帖子的人都没有喷作者的帖子写得烂。
入门
高二时我有了一部学习机,它搭载的是 linux 系统,除了学习功能外还自带了“编程天地”功能:
学习机里的“趣味导学”的开头有这样一段内容:
你玩过电脑游戏吗?
你喜欢电脑游戏吗?
电脑游戏是你生活中的一部分吗?
小时候,游戏是一台黑白电视机——现实在手柄的这头,梦境在手柄的那头.
有的游戏改变过人的一生……
长大后,游戏是一枚小小的铜币——现实在摇杆的这头,梦境在摇杆的那头。
现在啊,游戏是一台电脑——现实在鼠标这头,梦境在鼠标那头……
当计算机技术给游戏提供了强有力的支持后,一个陌生而又似曾相识的新奇世界展示在人们面前。
这里有逝去的童年梦想,有心头压抑已久的情感;有疯狂、神秘、也有脑力和技巧的挑战;有轻松获得实实在在的知识,也有用“虚拟”成就一个别样的人生。这不是一个神奇的世界吗?下面,我们就用编游戏来学习C语言吧!
总是从HELLO WORLD开始。
学习编程的第一个程序,一般就是打印一个亲切的词语——“Hello,World!”
……
(后面的内容省略)
打开“编程天地”后,主菜单里面有个 Noah-IDE,之前还不知道 IDE 是什么,有空百度了一下,原来 IDE 指的是集成开发环境。
这个 Noah-IDE 很强大,虽然它的编辑器只是个普通的文本编辑器,没有现代编辑器的语法高亮、自动缩进等功能,但也够用,可以新建 C/C++ 工程、编译工程时会自动生成 Makefile 文件并执行 make 命令,系统环境预置了 gcc、g++ 编译器和标准库头文件。
一开始是照着“趣味导学”里的给的 Helloworld! 示例源码写,经测试可以显示 Hello World!。之后,上面说要加个 getch() 函数让程序在按任意键后退出,我就改了一下源码,加上头文件 conio.h,在 printf() 后加上 getch(),改完后编译,结果编译报错,原因是学习机的 linux 系统环境中并不包含 conio.h 这种非标准库的头文件,当时没管这么多就继续找下个示例程序来测试了,然而后面的“在屏幕中移动的笑脸”和“推箱子游戏”这两个示例源码也编译报错。
“编程天地”里还有C语言教程,包括入门篇、初级篇、中级篇、高级篇,虽然看过好几遍,但写起来还是挺费劲的,折腾了很久才会用 printf() 函数,for、while、switch–case 在那时都还不会用。
第一个项目
某天,像往常一样开始折腾学习机,用 ls 命令查看了 bin 文件夹里的文件,随便选了一个,选到了 gzip,加上 --help
参数输出帮助内容,可那时并不能看懂这堆英文,在网上搜索相关信息后得知 gzip 是个压缩软件,它能将文件压缩成 gz 格式的压缩文件,在了解了这个后又发现了 tar 和 bzip2,经过测试,学习机里有这些软件并且可用,但我觉得手动敲命令行太麻烦,如果有个软件能简化操作就好了,于是就有了用 C 语言编写压缩管理程序的想法,从这个时候开始,“压缩管理“便成为我的第一个正式的 C 语言项目,也是我学 C 语言的第一个小目标。
当时只会基本的函数:用 printf() 函数显示字符菜单,scanf() 函数接受按键输入,if 语句判断输入内容,system() 函数调用命令进行操作,rename() 函数修改文件名,打印的字符界面还是黑白的。在校期间接触电脑的机会很少,大部分的 C 语言代码是用学习机写的,有时要写的代码比较多,会去网吧用电脑写代码,复制粘贴一把梭,编码效率比学习机高很多。
之前在学习机的论坛上看到某位高人的帖子,他的程序能在终端打印彩色字符,于是在 QQ 上问了编程相关人士,可没问出什么结果,只好搜索关键词 “linux 彩色 字符“,经过一番搜索后才找到答案,然后花了点时间将它应用到我的程序,最终实现了彩色字符界面:
这时的“压缩管理“,功能很少,只支持指定位置里的文件的压缩及解压,想解压/压缩文件就必须把文件放到指定的文件夹下,然后运行程序来操作。
按键输入功能有问题,不能一次性输入多个字符,否则会跳过菜单显示,例如:主菜单有 abcdef 六个选项,每个选项都有子菜单,如果我输入ab,那么就会跳过a选项的子菜单的显示,直接执行子菜单中的b选项操作。为了解决这个问题,我用了这么个办法:scanf(“%s”,&str);
,if 语句判断,只判断 str[0]
的值,每次输入完后,清空 str
里的全部内容。
防盗版
“压缩管理“完成后想在论坛上发布与其他机友分享成果,但又不想让他们轻易获得这个程序(毕竟是个人的劳动成果,有点舍不得),于是就想到了一个办法:添加试用期,正版激活。试用期为 7 天。
如何获取时间?
毫无疑问,这种问题要到程序员云集的 CSDN 论坛上问,帖子:http://topic.csdn.net/u/20101016/09/13d6c4c1-8099-4fad-bc3f-7d80dc898117.html。
如何知道程序安装时间?
我的想法是, 程序第一次运行时创建一个文件,记录安装时间,以后运行就读取这个文件。要实现这个功能,需要用到 C 语言的文件操作类的函数,重新复习了一遍学习机里自带的教程,又在网上搜索了一番,我用了 fprintf() 函数写入数据至文件,fscanf() 函数读取文件内容。这时,我有了这么一个想法:printf() 和 fprintf() 都是输出数据,用法基本一样,前者是将内容输出至屏幕,后者是输出至文件,fprintf() 前面的 f 是指 file 吗?我觉得应该是的,那么,是不是应该有个能输出至字符串变量的函数?字符串是 string,string + printf = sprintf
, 会有 sprintf() 函数吗?搜索了一下,的确有,而且用法和 printf()、fprintf() 基本一样,这时,我便学会用 sprintf() 函数改变字符串变量的内容了,对以后的字符串处理的帮助很大。
如何判断时间?
安装时间的保存和读取的功能已经基本实现,那如何知道还剩几天呢?如何判断是否超过了试用期?这问题比较复杂,平常有空就用笔在草稿纸画程序运行流程图,先干什么,后干什么,判断什么,画着画着还是觉得直接敲代码好一些。
判断时间,我用了一堆 if 语句,先判断年份,后判断月份、天、小时、分钟、秒;考虑到1月、3月、5月、7月、8月、10月、12月有31天,4月、6月、9月、11月有30天,而2月在平年有28天,闰年有29天,为了准确度,只好全部判断了。(现在觉得这样有点蠢)
如何实现正版激活?
试用期的天数的计算倒是勉强完成了,可是程序的正版激活如何实现?像电脑上的软件那样用 CD-Key 激活?CD-Key 如何生成?为了防止多个人共用一个 CD-Key,如何实现一个学习机上只能用专有的 CD-Key 激活程序?
我是这样想的:先实现独一无二的产品 ID 的生成,之后,通过算法算出对应的 CD-Key,想激活程序的机友可将产品 ID 以回帖方式发给我,我再给他 CD-Key,这样能增加帖子的回复数和人气,但是,这个功能由于本人当时技术水平有限,终究还是没实现,关于这个问题,在论坛上提过:
- http://topic.csdn.net/u/20101010/13/51fb341e-05c8-46f3-919c-6b17d3955d80.html
- http://topic.csdn.net/u/20101210/09/1b49f8b8-213d-4c40-8b1d-c6e3015d3ea7.html
UUID
某一天在看 include 文件夹的头文件时发现了 uuid,很想知道这个是干什么的,搜索找到的结果是:
UUID 含义是通用唯一识别码 (Universally Unique Identifier),这 是一个软件建构的标准,也是被开源软件基金会 (Open Software Foundation, OSF) 的组织在分布式计算环境 (Distributed Computing Environment, DCE) 领域的一部份。
研究了uuid.h 头文件中的函数的用法,写了个测试程序,经过多次修改,生成的数字可以以这种格式显示:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx
这不正是我要找的能生成独一无二的产品ID的功能吗?但是,发现这个是很久以后的事了,那时可没想过再添加这个功能。
如何写自定义函数?
当时,我不会写自定义的函数,所有代码都写在 main() 函数内,有尝试过把部分功能的代码拆分到自定义函数内,可是不知到该如何和 main() 函数共享数据,例如:main() 函数将一个变量 a 作为参数传给自定义的 func() 函数,在 func() 函数中可以随意修改传进来的变量的值,可退出后,main() 函数的变量 a 还是没变。这问题在经过一段时间的思考后终于解决了,比如 func() 函数原型是:func(char a[]);
在 a 前面加个 *,再去掉 [] 就可以了:func(char *a);
,func() 函数中的 a 变成了 char 型的指针,指向传入的参数,对指针的内容进行修改,就相当于对调用 func() 函数的上一级函数中的的变量进行修改。
在网上搜索的过程中还了解到了局部变量和全局变量,全局变量可以多个函数共用,局部变量只在声明它的函数内起作用。
代码可以分离成多个函数,但还是在一个 c 文件内,考虑到一个C文件内源代码量很多的话会影响编译耗时,于是就想将代码分离至多个文件,这样,编译耗时就不会那么长了,只重新编译被修改过的C文件中的源代码。
调用 7-zip
压缩管理的第二个版本,也就是 0.84 版(之前的版本是测试版本),加入了很多新功能。那时由于本人发现了一个强大的压缩软件 7-zip,就决定基于它实现压缩管理功能,命令行版的 7-zip 支持很多压缩格式,可自定义参数也较多,包括了系统自带的 tar、gzip、bzip2 这三个压缩程序的功能。
为了更好的利用 7-zip,我还特意研究了 7-zip 的命令行用法,花费了一些时间实现了压缩参数设置功能,具体如图所示:
图中所示的是 7z 格式的压缩参数,更新后,又添加了几个选项:
Zip 格式的压缩参数:
单词大小、词典大小、压缩等级、压缩算法等参数均可自定义,7z 和 zip 这两种格式的压缩参数最多,输入对应字母即可切换参数,这些自定义的参数都保存在文件内,实现思路如下:
首先,需要读取每一行字符串,这个,我在文件操作类的函数中找到了 fgets() 函数,它只能读取一行内容,要让它读取每一行内容,我就用了 while 循环,每读一行就进行处理。那么,如何搜索已读取的字符串是否包含指定的字符串?我用的是 strstr() 函数,这个函数是通过提问得到的:http://topic.csdn.net/u/20101111/09/927dccb8-0797-4e51-af0e-60dabe14f771.html
用 strstr() 函数查找是否有对应选项,有的话,保存该选项后面的内容,例如:
有一行内容是这样的:“switch = off“,程序搜索时,就用strstr函数搜索该行是否有 “switch =“,有的话则判断后面的内容是否正确,考虑到字符串首尾会有空格,在网上找了个去除字符串首尾空格的代码,这个代码是仿照 trim() 函数写的。又考虑到对比字符不需要区分大小写,例如 switch 和 SWITCH 等同,于是就找到了 strcasecmp() 函数。
调用 7-zip 进行压缩和解压文件主要是通过 system() 函数实现的,命令行的内容是通过 sprintf() 函数处理生成的,先定义一个 char 型的字符串变量 command 用于存储命令行内容,每个参数都有各自的变量,当这个参数有效时就赋值给相应变量,无效则清空相应变量里的内容,最终用类似于 sprintf(command,”7z %s %s %s%s %s %s %s”a,b,c,d,e,f,g)
的代码生成命令行,用 system(command)
运行命令行。
定制 7-zip 的源码
看到 7-zip 在解压和压缩文件时输出的文字是英文的,想把它汉化,于是就去看 7-zip 的源代码,源代码是 C++ 语言编写的,有些地方和 C 相似,还是能看明白一部分,汉化很简单,找到需要汉化的字符然后改成中文即可。黑白太单调,加上了彩色字符,改完后重新编译。这个编译是在学习机下完成的,先运行 ./configure
,然后运行 make
,这是在学习机论坛上得知的 linux 系统下编译源码的方法,受益匪浅。
以下是汉化后的效果图,由于当时的学习机出毛病送去修了,只能在电脑上测试,这图是用 PC 端 ubuntu 系统的输出结果合成的。
这个版本的压缩管理只能解压任意位置的压缩文件,还不能将任意位置的文件压缩成压缩包。
添加文件关联界面
研究了学习机自带的”资源管理“的关联功能, 所有格式的文件关联信息都保存在 resourcemanager.app 文件中,为了更好的修改文件内容,我又用 C 语言编写了一个”关联管理“的程序,可以添加/编辑/删除关联数据,具体见下图,在此不做过多的说明。
为了更好的查看“压缩管理“与支持的压缩文件格式的关联状态,我把”关联管理“集成到了“压缩管理“中:
主菜单中显示已关联的格式数量,新增的关联管理功能菜单中可以显示具体状态,红色背景的表示为已关联,黄色字体表示未关联。
由于压缩 7z 和 zip 格式可以设置压缩密码,为了避免忘记密码,我加了密码查询功能,可以查询之前设置的密码。在操作完成时,程序会播放提示音,这个功能是靠调用 madplay 程序播放 mp3 格式的音频实现的。
学习机的总内存为 64MB,平常的可用内存为 5MB 左右, 有时调用 7zip 压缩文件和解压文件时会提示内存不够,为了腾出足够的内存空间供当前程序使用,需要一个 SWAP 分区来保存暂时没有用的内存数据,于是就加了 SWAP 开关功能,这个 SWAP 也是在学习机论坛上得知的。
添加文件操作界面
建立文件关联后,就能在学习机里的“资源管理”里打开压缩文件了,“资源管理”在打开文件时会调用“压缩管理”,并将文件的绝对路径作为参数传给它,int main(int argc,char *argv[])
中的 argc 和 argv 在这时候就起到作用了,通过判断 argc 的值得出是否有参数传入,有参数传入就会保存在 argv 里。
有参数时会进入文件操作菜单,这个菜单中的“位置”和“名称”内容是变化的,内容过长、过短都会影响字符界面,于是,写了个字符串处理函数,指定字符的长度,低于这个长度则用空格填补,大于这个长度则截取内容,并将末尾3个字符改为”.”。
7-zip 对压缩文件的操作不止这些,后续我又追加了一些功能:
传入的参数是文件的绝对路径, 如何将文件名和位置从绝对路径中分离出来?当时采用的方法是计算绝对路径中的“/”的个数,以最后一个”/”为字符串截断点,把字符串分成两个。
以下是每个设置项的说明:
- 解压后自动关闭终端,返回“资源管理”。
- 自定义压缩文件的释放目录:当前目录、工作目录、自定义目录。
- 查看文件列表是使用了“l”参数查看压缩包内的文件,把 7z 输出的文件列表重定向至文件中。
压缩文件还可以再次进行压缩,考虑到 tar 格式的存在,就添加了这个功能, 将 tar 格式的文件压缩成 gzip 、bzip2、zip、7z 格式,可以看下图,在资源管理里面,源文件是 terminfo.tar,7z 和 zip 都是使用 lzma 算法压缩的。
添加路径选择界面
解压和压缩文件时少不了文件路径的选择,直接输入字符很麻烦,于是我新增了路径选择功能,选择后会将文件输出到“本地磁盘”或“存储卡”中固定的目录中。
存储卡的空间信息显示有误,后来知道是块大小的问题,我复制的源代码中的块大小是直接用 4,改成用 stat 结构体中的变量 st_blksize 后总空间大小才计算正常。
清除编译警告
刚开始写 C 代码时,听说 warning 可以忽略,只需重视 error,然而一次偶然的机会,在网上发现了 gcc 编译器的一个参数,-Wall 开启所有警告信息,于是就试着用上这个参数,结果发现我的程序原来有这么多 warning。由于本人喜欢追求完美,想消灭掉所有 warning,于是花费了大量时间在处理 warning 代码上。从此以后,编译代码前都会给 gcc 编译器加个 -Wall 参数,看看程序有什么 warning 和 error。
改进按键操作体验
早期版本的“压缩管理”的操作方式是输入字符并按回车,一个操作需要按两次键,体验比较差,那么有没有能在按下键时立即响应的方法呢?Windows 中的 getch() 函数正好满足需求,然而它并不是标准库的函数,不适用于 linux 系统,只能想办法模拟实现 getch() 函数的功能,用关键词“linux getch 模拟实现”搜索了一番,网上给的源代码差强人意,按下方向键时会产生多个键值,当时没打算改进它,勉强的先用着了。
图形化启动画面
“压缩管理”的 0.86 版中加入了程序启动画面,这个界面只是一张图片,图片显示功能是通过调用 mgaview 程序得。
mgaview 程序是我在逛学习机论坛时上发现的,它可以打开一张图片并绘制到屏幕上,但它只绘制一次,如果屏幕内容有变动,这图片也就消失了。为了寻找解决方法,我便开始研究 mgaview 的源码,一开始研究得并不深,只是把部分代码改了一下,例如将它的 main() 函数改成 int view_image(char *filepath, int sec)
,以实现自定义文件路径和显示时长。后来想把它编译成一个库,在了解到动态库和静态库相关概念后,我选择了静态库,静态库其实是多个 o 文件经过打包而成的,可通过调用 ar 命令将 o 文件打包成静态库。
这个时候的“压缩管理”的功能已经比较完整,能添加的功能也尽量添加了,例如:IPK 的安装、IPK 安装包信息的读取、IPK 安装包的制作、IPK 安装信息的读取等。IPK 是学习机的的安装包文件格式,实质上也是个压缩包,这让“压缩管理”也有发挥的余地。
进阶
到了 0.87 版,程序的名字已经改成“文件管理器”,因为那时我觉得 PC 版的 7-zip 都有文件管理器的功能,同是压缩软件的“压缩管理”也应该具备文件管理器的功能。
这个版本中,主界面直接显示存储空间信息,源代码修改自之前的“压缩管理”中的路径选择菜单。“本地磁盘”的背景为红色,表示当前选中的是“本地磁盘”,按方向键 ↓ 键可以选中“存储卡”,存储卡的插入/拔出状态判断是通过读取根目录下的 proc 文件夹中的某个文件实现的,当存储卡处于移除状态时,该文件内容为 REMOVE,处于插入状态时内容就会是 INSIDE。
为了改进 getch() 函数的方向键支持,我采用了这种做法:接受第一个按键键值后,创建一个线程,用于再次接受按键键值,等 1000 毫秒后终止这个线程,之后,将子线程获取的键值与刚刚得到的键值相加,就得到了一个新的键值。
按 F 键即可显示字符下拉菜单,菜单中的选项也有对应的快捷键,但在后来的更新中已经去掉了。
这些字符界面是用转义序列实现,使用 printf() 函数打印 \e[y;xH
可将终端的光标定位到 y 行 x 列,例如:printf("\e[1;1H");
将光标定位到第 1 行第 1 列,这时,后面打印的内容就会从这个位置开始显示。
读取文件目录
要实现文件管理器的文件浏览功能,首先要能读取文件目录信息,以“linux 遍历文件目录 C”为关键词在网上搜索,找到个合适的代码就复制过来了。
统计文件数量
windows 7 系统中的资源管理器可以显示当前文件夹下有多少文件夹和文件,我也模仿实现了这个功能,修改遍历文件目录的源代码,加入文件夹和文件计数功能。
获取文件大小
文件列表中应该要显示文件大小,于是就用 stat() 函数在获取文件列表后获取每个文件的大小。
文件列表排序
文件列表中应该要按顺序显示,文件夹排前面,文件排后面,我在网上搜索了一下,发现scandir函数生成的文件列表是自动经过排序的,而这个文件列表是用malloc分配的内存存储的,需要free掉;生成的文件列表是文件和文件夹混合的,需要将文件夹和文件分离出来,用for循环判断文件列表中的文件的属性,这个时候我用的是二维数组存储的,静态内存,编译出来的文件也很大。
使用动态内存
考虑到浏览的文件目录内的文件和文件夹会很多,用静态内存存储文件列表的话,存储的数量是有限的,听说有动态内存,网上搜索了一下,可用malloc() 函数分配内存,free() 函数释放内存,就这样,我改用二维指针来存储文件列表及各个文件大小,文件总数、文件夹数、文件数、每个文件的文件名、每个文件的大小都是已知的,分配内存比较容易,用完后,用 free() 函数释放掉,free() 函数需要和 malloc() 函数成对使用。
文件批量操作
前期版本只支持单文件操作,在后续的更新中,已经实现了多文件操作。
用户可以标记多个文件,标记的文件存储在二维数组中,同时用一个变量保存已标记的文件的总数。
字符界面重绘优化
文件管理器的界面中的左下角显示的内容是变化的,标记文件时会显示“已标记N个文件”,移动/复制文件时会显示”将XXX文件(或者N个项目)移动/复制至…“,没有任何操作时,显示“N个文件夹,M个文件”,实现这个字符界面主要用到了字符串处理函数,用 sprintf() 函数生成界面每一行的内容,之后再用 printf() 函数输出,由于 clear 命令清屏闪烁效果会影响视觉体验,我改用了转义序列的光标定位功能来覆盖更新屏幕内容。
文件名的长度有长有短,为了不让字符界面被文件名打乱,在输出文件名前会做些处理,当字符串长度少于指定长度则用空格填补,多余则截取字符串。
有的时候截断的字符串的末尾会乱码,原因是本地编码下的汉字占用两个字节,少了一个字节会使这个字符无效,而无效字符则会显示成框框。考虑到汉字每个字节的值都是负数,并且这个负数都是成对存在,那么在截取字符串时可以顺便统计汉字的占用字节数,当为偶数时可截断。
显示文件操作进度
windows 7 的资源管理器在复制/移动文件时会显示进度条以及相关信息:
这个功能实现起来比较容易,最初的效果是这样的:
经过完善后:
在复制/移动过程中,会碰到同名文件,win7 的资源管理器会显示这样的界面:
实现起来也很简单,用stat函数获取文件大小和修改日期,之后对比,体积大的就显示“(较大)”,最近修改的就显示“(较新)”,效果如下图所示:
显示文件详情
windows 7 的文件详情窗口中展示了文件名、类型、位置、大小、包含的文件数、创建时间属性:
模仿效果:
使用这些功能有时会遇到段错误(Segmentation Fault),解决这些问题也用了大量时间。
添加文件关联管理
经过多次更新后,添加了文件关联功能,这样就可以自定义文件打开方式了。
为了方便查找,我用了一个从网上找到的排序函数对这个列表做了排序。
添加压缩管理
文件管理器中的压缩管理功能可以将多个文件压缩成压缩文件,在解压文件时,可以选择任意位置作为解压目录。
将文件压缩成压缩文件时,可以设置压缩文件的名称和压缩格式:
打开压缩文件后,有4个选项:
- 打开:调用 7zip 显示文件列表
- 解压:进入当前目录,可以正常浏览文件目录,按 V 键确定当前位置为解压位置
- 更新:选择其他文件,然后将它们添加至压缩包内
- 测试:调用 7zip 自带的测试功能
改进配置界面
通过按 ↑ 键和 ↓ 键移动光标 “»” 来选中选项,按确定键可选中这个选项。
改进 IPK 安装功能
学习机的 IPK 包安装程序是 ipkg-cl,可调用它安装 IPK 包,在安装后,它会将 IPK 包信息记录在 status 文件内,“文件管理器”可以通过读取这个文件来获取安装信息。
在文件管理器中打开 ipk 安装包时,会先读取 ipk 包的信息,之后提示是否安装该安装包。
如果已经安装过,会提示是替换还是先卸载再安装。
安装包的信息保存在 control 文件内,可调用 7z 命令从安装包中解压该文件,然后读取内容,如果没有这个文件,则不显示安装包的信息。
在安装 ipk 包后再次在查看安装包信息时,会显示该安装包安装后占用空间。占用空间的计算方法比较简单,由于 ipk 包在安装后会自动生成包内的文件列表,因此可通过读取它来累计所有文件的大小作为占用空间大小。
图形编程
一直做同样的事情容易感到乏味,这时会想找点其它事情做,比如去研究 mgaview 的源码,搞清楚它是如何将图形绘制到屏幕上的,至于研究过程就不在此处做详细描述了,研究成果可在另一篇文章中找到。
mgaview 是通过往帧缓冲 (FrameBuffer) 写入数据来实现图形显示的,在参考相关代码和帧缓存的一些文章后,开始尝试编写一个测试程序。首先,将屏幕填充白色并在指定位置绘制一个红色矩形,然后让这个红色矩形在白色背景中移动,看上去没什么难度,只是简单的纯色填充,接下来是将这个红色矩形更换为小鸟图片,小鸟的图片数组是用 mgaview 中的图片解码功能解码出来的,第一次测试,小鸟的图片变形了,后来发现是图片尺寸的问题,不是实际尺寸,纠正后能正常显示。
在这之后,我又增加了一只小鸟,并把白色背景改成一半黑一半白的:
在屏幕坐标系中,原点(0,0)在左上角,由于像素数据是线性存储的,将二维坐标转换成一维的线性坐标的公式为:
数组中的位置 = y 轴坐标 * 屏幕宽度 + x 轴坐标
例如:屏幕上有个点的坐标为 (320, 240),而屏幕尺寸是宽为1280像素,高为800像素,那么,这个点在数组中的位置是:240 * 1280 + 320 = 307520。
以 RGBA32 格式存储像素数据的话,一个像素点占用四个字节,其中 Red、Green、Blue、Alpha 各占一个字节,一个字节能表达 0~255 范围的值,那么 RGB 占的三个字节能表达 16777216 种颜色,在当时能显示这么多颜色的屏幕是真彩屏幕。
第一个图形游戏
在掌握了这些图形知识之后,准备编写一个小游戏,游戏很简单,只有一张彩色背景和一个人物贴图,人物贴图素材是用手机运行游戏一帧帧截取的,截取完后还需要用图像编辑工具将人物动作图形抠出来并保存为 png。准备好图片素材后再将这些图片转换成数组,每张图一共四个数组,分别保存R、G、B、A通道,关于 alpha 通道混合图形的方法,我发了个帖子:http://topic.csdn.net/u/20110623/14/ac5fbbff-a3f9-469f-a362-ecde8aa32a3f.html
以下是最终运行效果:
可用按键控制人物行走,按 J 键使用攻击动作。
游戏人物的图形素材来自这个游戏:
图形化界面
经历了几个图形测试程序的开发,自认为对图形绘制方法掌握的得差不多了,于是就对文件管理器进行图形化改造,主界面是第一个实现图形化的:
之后又对主界面做了些改动,在界面下面增加显内存信息,并且会动态更新。
当然,这图形界面都是由一些预先编辑好的图片组成的,其中文字的绘制比较复杂,需要预先用图像编辑工具将数字、小数点、MB、GB、KB 分别保存至 png 图片中,然后在程序中将文字与这些图片建立映射关系,之后在绘制文字时遍历字符串将对应的图片粘贴至指定的位置,例如:59.16MB,先遍历这个字符串,计算所需矩形背景的长度和宽度,之后判断,第一个是 5,将 5 的位图贴到矩形背景中,第二个是 9,将 9 的位图贴到矩形背景中,以此类推,最后,矩形背景中就贴上了该字符串对应的位图。
容量条用到了三张图片:红色、蓝色的满格容量条的图片,一张容量条的空槽的图片。绘制时根据百分比将适合长度的满格容量条贴到空槽的图片中,当超过一定值时,就用红色容量条的图片。
在这之后,其它的功能也实现了图形化:
按钮是模拟实现的,虽然按下时有凹陷的效果,但都是用图片贴出来的,而窗口中的内容大部分是用图像处理工具预先编辑好的,剩下的由程序处理显示。
游戏编程
学会显示图形后,想做一个 2D 图形游戏,当时有了解到 flash 小游戏《死神vs火影》,网上也有相关图形素材下载,于是就决定在游戏中使用这些图形。
第一个测试程序,用的是黑崎一护的站立动画,全白背景:
在添加了移动动作的动画后,考虑到只有人物朝向右边的图形,没有朝向左边的,如果用图像编辑工具做个朝向左边的的版本的话比较耗时,于是就想要一个图形水平翻转算法。稍加思考后,水平翻转只是将左右像素点交换,实现起来不难。
将人物移动到屏幕边缘时显示不正常,后来发现是将屏幕范围外的像素写入到屏幕内了,解决方法是将有效区域裁剪出来再绘制。处理后的效果如下:
之后加上了首屏界面和主菜单:
这时界面中已经有了一些简单的动画:“按确定键继续”的闪烁背景、进入主菜单时的淡入淡出、菜单光标的平移。
选择开始游戏后,会进入对战画面:
在添加人物状态栏之前游戏画面刷新率还能接受,然而在添加后画面刷新率有明显下降,看来学习机的硬件性能已达极限,无法支撑更加复杂的游戏画面。
黑崎一护使用的月牙天冲,原始的动作动画是一次性释放一个,我觉得这个限制很大,就把人物动作和技能效果图分离出来,单独处理显示,在按住 U 键后,每隔一段时间会释放一次月牙天冲,在释放完技能后人物才可以移动。
这时的游戏还没有电脑玩家、攻击和受击判定,源代码写得也很烂,需要重构一遍。
以前玩游戏的时候都会有一些想法,觉得它有很多可改进的地方,有时还给官方反馈过意见,然而这并没有什么用,游戏官方可不会因为平民玩家的建议而对游戏做改动。现在的我,已经具备基本的游戏开发能力,可以尝试开发属于自己的游戏了。
结语
当时作者存在以下问题:
- 大部分时间都在写代码,技术成长缓慢
- 看过的技术书籍很少,理论基础薄弱,了解到的技术仅限于自己开发这几个程序
- 对行业内的包括技术社区、开源项目等在内的资源了解有限,没有研究过 SDL、Qt、DirectFB、GTK 等开发库
这种水平只能算个业余编程爱好者,对找工作没有优势,如果你是一个对编程感兴趣、想以此为职业的人,建议你制定类似于以下的成长规划:
- 在求职平台上找自己感兴趣的职位,了解自己需要具备哪些技能
- 阅读相关技术书籍,巩固理论基础
- 上技术成长平台刷算法题,提高算法功底
- 找一些自己感兴趣开源项目,研究其源代码并试着改进它
- 应用自己所学到的技术开发一个开源项目,并持续完善它
- 写博客记录学习历程
- 学习他人分享的面试经历,刷面试题
如果你这些都做到了,那么简历上就可以这样写:
- 熟悉 XXXX、 XXXX、 XXXX、 XXXX
- 熟悉常用的数据结构及算法,基础扎实
- 具备扎实的计算机科学基础知识,熟悉各种 XXXX 原理及相关知识
- 具备丰富的开源项目开发和维护经验,参与过 XXXX 知名开源项目并贡献过代码
- 熟练使用 Git 版本管理工具进行多人协作开发
- 良好的编码和文档习惯,对代码美感的追求孜孜不倦
- 热爱学习和分享,持续更新博客 N 年以上
有了这些内容,在应届毕业生中已经有很大优势了。
文章版权归作者所有,未经许可不得转载。