程序员入门:QBASIC经典教程系列——最吸引人之动画

 目录

这部是最精彩的,你将可以编3D动画了!

补充点,上次忘了说那个3D图形的公式中SQR函数是开平方,与x^.5的作用是相同的,但速度比后者快.而X*X这种用法也是为了提高速度而不用X^2。

上次我们讨论了QB的基本绘图命令,然而用那些命令要做动画是不成的.我们知道,动画的原理就是一幅幅反复显示出来,在每一时刻都只有一帧图象. 最简单的动画要属擦图动画了,看下面这段例子:

SCREEN 12
CONST r = 10
COLOR 14
LINE (0, 200 + r)-(640, 200 + r), 10
FOR i = 0 TO 320 STEP .5
  x = i * 2
  y = 200 - ABS(SIN(i * .2) * 200) 'ABS取绝对值
  CIRCLE (x, y), r
  PAINT (x, y)
  t1! = TIMER '延时开始
WHILE TIMER < t1! + .01
WEND '延时结束
  LINE (x - r, y - r)-(x + r, y + r), 0, BF
  IF INKEY$ = CHR$(27) THEN END
NEXT

不好意思,我用取绝对值的正弦函数代替自由落体的函数.似乎是动起来了,效果也不错.但因为这是一个简单的图形,画的时间比较短,现在的CPU时间比较快. 如果你把延时部分放到LINE的后面或者去掉你就知道问题所在了.如果是复杂的图形,当画的过程所费时间与延时时间相近时就会发生闪烁.事实上这个程序也有闪烁,只是速度比较快,眼睛反应不过来.你可以试试把半径R改大一些.QB提供了GET和PUT语句来提高绘图的速度.下面修改一下上面的程序:

SCREEN 12
CONST r = 20
COLOR 14
bpp = 1
Planes = 4
l = 4 + INT(((r * 2 + 1) * (bpp) + 7) / 8) * Planes * (r * 2 + 1)
l = l / 2 + 1
DIM cir(l) AS INTEGER
CIRCLE (200, 200), r
PAINT (200, 200)
GET (200 - r, 200 - r)-(200 + r, 200 + r), cir(0)
CLS '清屏语句
LINE (0, 300 + r)-(640, 300 + r), 10
FOR i = 20 TO 300 STEP .5
  x = i * 2
  y = 300 - ABS(SIN(i * .2) * 200)
  PUT (x - 20, y - 20), cir(0)
  t1! = TIMER
WHILE TIMER < t1! + .01
WEND
LINE (x - r, y - r)-(x + r, y + r), 0, BF
IF INKEY$ = CHR$(27) THEN END
NEXT

我们把复杂的图形事先画好,用GET语句把点阵数据保存在一个数组变量里,然后在动画循环中用PUT命令一次画出,效果好一些.但数组变量的大小应该是多少呢?多了浪费,少了要出错误,QB的HELP里有这样一个公式:

l = 4 + INT(((X2-X1+ 1) * (bpp) + 7) / 8) * Planes * (Y2-Y1 + 1)

X1,X2,Y1,Y2是图形块的左上和右下角坐标,bpp和planes与屏幕模式有关:

Screen Mode bpP Planes
9 1 4 (if > 64K of EGA memory)
12 1 4
13 8 1

计算出的L是字节长.而在QB里的数字变量没有单字节的,最短也是2字节的INTEGER.因此我们用L/2+1来表示INT数组长度.其实也可以用其他类型的数组的.

需要注意的是,与其他语句不同,所有坐标必须都在屏幕的物理范围之内,否则将会出错.

PUT语句默认是把GET语句记录的图象与要覆盖的背景进行XOR运算.我们再复习一下各种逻辑运算:

NOT 取非,就是NOT 1=0,NOT 0=1

这里的1是二进制位,不是数字,数字应是-1,因为二进制1111…等于-1

AND 与运算,如果x AND y,只有两个数字都是1才等于1,否则为0
OR 或运算,只要有一个是1,结果就是1
XOR 异或,当两边不相等为1,否则为0.

XOR有一种非常有趣的性质,就是任意交换性,比如A XOR B=C,则A XOR C=B,C XOR B=A….任意两个数交换位置都可以.而且只要不是0,通常运算结果跟运算前不同.这种性质给加密等操作带来很大的方便.

PUT语句后面可以加上各种符号,表示原图与背景进行的运算方式.前面说QB模式是XOR方式,还有PSET方式(直接覆盖),PRESET方式(取非后覆盖),OR方式(与背景取或),AND方式(与背景取与)

看看下面这段程序,我们把前面的程序加上背景,事实上大多数动画都是要有背景的.

SCREEN 12
CONST r = 20
COLOR 14
bpp = 1
Planes = 4
l = 4 + INT(((r * 2 + 1) * (bpp) + 7) / 8) * Planes * (r * 2 + 1)
l = l / 2 + 1
DIM cir(l) AS INTEGER
CIRCLE (200, 200), r
PAINT (200, 200)
GET (200 - r, 200 - r)-(200 + r, 200 + r), cir
CLS
FOR i = 0 TO 640 STEP 10 '画背景
  LINE (i, 0)-(i, 479), 3
NEXT

LINE (0, 300 + r)-(640, 300 + r), 10

FOR i = 20 TO 300 STEP .5
  x = i * 2
  y = 300 - ABS(SIN(i * .2) * 200)
  PUT (x - 20, y - 20), cir, XOR '试把这里改成其他符号,如PSET等.
  t1! = TIMER
  WHILE TIMER < t1! + .01
  WEND
  PUT (x - 20, y - 20), cir '默认是XOR,可以在保留背景的同时擦图
  'LINE (x - r, y - r)-(x + r, y + r), 0, BF '原来的方法把背景也给擦了
  IF INKEY$ = CHR$(27) THEN END
NEXT

我们发现,XOR的缺点是只要不是黑色就会变色,跟以前不一样了.因此要做得好一点应该先把要被覆盖的背景用个数组存下来,当下一帧时再把那个"备份"覆盖回去,缺点是闪烁感很强.

其实擦图动画永远解决不了闪烁的问题的,因为屏幕上总有一段时间那个图形消失了,一亮一灭,这就造成了闪烁,无论速度多快,只要屏幕上显示了不该显示的东西,人眼就能反映出来.

真正的解决办法是用双缓冲技术.这种技术把要画在屏幕上的东西事先在内存中画出来,然后把整个页全部覆盖. 然而QB对内存的控制很严,给你操作内存的语句很少也很慢,根本不能完成这种技术,而且所有绘图语句也都不能用,要用位运算来完成.其实象JAVA语言一样,这些限制的好处是安全性提高了,但JAVA本身就提供了双缓冲. 🙁 替代的办法是屏幕页技术. 显示卡从硬件上来说,显示内存足够存放很多页.可由于编QB时内存还很贵,通常的显示卡只支持模式9的双屏幕页,这也是为什么我要讲模式9的原因.模式9从颜色属性来说跟模式12一样,但发色数和分辨率要低得多.由于分辨率是640X350,象素是长的点而不是方点,如果你画个正方形就会发现变长方形了,这需要注意. 但CIRCLE会自动调整纵横比,画出来总是圆的.

屏幕页的控制用SCREEN语句的第三,四个参数:

SCREEN ,,绘图页,显示页

注意前面那两个逗号少不得.默认两个页都是0.画图时在绘图页上画,在屏幕上显示不出来,屏幕上显示的是显示页,两页无关,这样用户就不会看见绘图过程.当绘图页画好后你当然可以用SCREEN语句把绘图页和显示页交换,但实际上交换时可能会发生闪烁,这是硬件的原因.因此我们用PCOPY语句把一页拷贝到另一页去,速度很快.再看下面这个例子:

SCREEN 9
CONST r = 40 '加大半径,如果你把以前的例子也加大半径就会发生严重的闪烁

COLOR 14
bpp = 1
Planes = 4
l = 4 + INT(((r * 2 + 1) * (bpp) + 7) / 8) * Planes * (r * 2 + 1)
l = l / 2 + 1
DIM cir(l) AS INTEGER, backup(l) AS INTEGER
CIRCLE (200, 200), r
PAINT (200, 200)
GET (200 - r, 200 - r)-(200 + r, 200 + r), cir
CLS
FOR i = 0 TO 640 STEP 10
  LINE (i, 0)-(i, 479), 3
NEXT

LINE (0, 300 + r)-(640, 300 + r), 10
PCOPY 0, 1
SCREEN , , 0, 1 ' 显示第0页绘图,第1页显示
FOR i = 20 TO 300 STEP .5
  x = i * 2
  y = 300 - ABS(SIN(i * .2) * 200)
  GET (x - r, y - r)-(x + r, y + r), backup '先把要覆盖的背景备份
  PUT (x - r, y - r), cir '你可以用任何方法贴图.
  PCOPY 0, 1
  t1! = TIMER
  WHILE TIMER < t1! + .01
  WEND
  PUT (x - r, y - r), backup, PSET '恢复背景
  'LINE (x - r, y - r)-(x + r, y + r), 0, BF
  IF INKEY$ = CHR$(27) THEN END
NEXT

不错吧?一点闪烁都没有.但QB做动画还是不成,因为没有透明色方式. 在动画中动的物体通常被称为"精灵",精灵的背景通常是黑色,但如果背景不是黑色,用PSET就会很难看,如果用XOR精灵的"身体"就会变色.这时就需要一种"通明色",贴上去只显示身体,不显示背景. 很不幸的,DOS下的程序都很难完成这种操作,WIN下恐怕也要DIRECTX技术的帮助,我原来曾想用汇编语言编函数,可后来一想,都WIN95时代了,DOS就凑合用吧.

动画技术基本讲完了,一般的书上还有"字符动画",我觉得意义不大,字符模式可以有4到8页,但不能用PCOPY,闪烁好象更厉害. 😛 可以参考我的BATTOOL3.2版,我想很多站的DOS工具区都有这个软件.里面有用字符动画做的一只小猪的动画,还是用批处理做的,改成QB程序太容易了.

但是绘图技术不仅这些,它需要很多的数学知识. 比如我那本最早的BASIC书里讲述了各种用矩阵变换计算各种二维三维图形,各种三维图形的做法等.<电脑爱好者>几年前也连载过用QB做动画的教程,似乎是能做出来三国志的片头动画(但效果可是….嘿嘿,能看出来是人就不错了)希望在实际应用时多运用数学工具,先想出数学模型,在转换成QB语句.

下面我们要讲模块化编程了.主要有两个语句:SUB 和 FUNCTIONSUB的作用是自己定义一个过程,以后在QB里可以象调用语句那样调用它,或者说就是"自定义语句"或是"子程序".FUNCTION的作用就是"自定义函数",可以象调用函数那样调用FUNCTION过程.使用过程除了简化一段反复使用的程序外,它可以使一个复杂的工程问题简单化,先解决"主要矛盾",把一些难解决的细节问题放到过程里.在过程的内部的变量和其他过程,主程序间是没有关系的.也就是说你可以在过程外边用for i=….循环,循环里面有个过程,过程里面也有个for i=…循环,但这两个变量i是没有关系的.这就是过程的优点,当程序很大的时候,变量很多,闹不好就用重复的名字,很容易出错,可过程间变量名相同也没关系.当然你也可以让某些变量成为"全局变量",比如在主程序里输入:

DIM SHARED I AS INTEGER

那么无论是那个SUB或者FUNCTION,只要有I,都是指的同一个I.QB会对过程进行"隔离"处理,使每个模块看起来都很小,心理上似乎觉得容易了. 切换各模块是用F2键.

首先我们来看FUNCTION,其实就是"自定义函数",但这个名称在老式BASIC里已经用了,我也不知道FUNCTION中文是什么. 😛 我们知道函数的特点是有几个参数,一个返回值.比如SIN(X),X是参数,返回一个数是计算结果.我们也来定义一个函数:

DECLARE FUNCTION a% (x AS INTEGER) '这行是QB自动加的
WHILE (INKEY$ = "")
  PRINT myfunc(y%)
WEND

FUNCTION myfunc% (x AS INTEGER) '函数开始
  STATIC c AS INTEGER
  c = c + 1
  myfunc = c
END FUNCTION

当你输入FUNCTION时QB就会自动开个新的屏幕,好象是个新的程序,这就是模块化,模块间的关系只通过参数传递来联系.函数的返回值放到一个特殊的变量中,变量名跟函数名一样,myfunc=c的意思就是整个函数的结果等于c.需要注意的是这个变量只能赋值,不能用myfunc=myfunc+1,否则可能会出现很严重的后果,因为这种调用叫做递归调用,见后面的解释.STATIC表示静态变量,在第二行开头加上个"’"注释掉,看看会怎样?静态变量的意思就是这个变量在内存的位置始终不变,每次调用都保持上次的值.也可以在FUNCTION a(..)的后面加上STATIC表示这是个静态函数,所有变量都是静态的.你可以加上/去掉试试看有什么区别.默认情况下每一次调用都不同,值也不同. 这样才能进行递归调用.所谓递归就是自己调用自己的一种算法,在计算机编程里是一种很重要的算法,很多数学问题(比如著名的河内之塔问题)必须要用递归.为了安全,QB递归有次数限制的,超过要出错误提示,不会象其他语言那样崩溃.我们用QB编个计算阶乘的函数: 阶乘的定义是 a!=a * (a-1) * ...3 * 2 * 1 (叹号是阶乘符号) 由此可以知道 a! = a * (a-1)!, (a-1)!=(a-1) * (a-2)!... 看下面这个程序:

DECLARE FUNCTION jc% (x AS INTEGER)
PRINT jc(3)

FUNCTION jc% (x AS INTEGER)
  IF x <= 1 THEN
    jc = 1
    EXIT FUNCTION
  END IF
  jc = x * jc(x - 1)
END FUNCTION

a就是一个阶乘函数,主程序只有一句 print 语句,因此你完全可以在立即窗里输入 ?a(9) 立刻得到9的阶乘是多少.为了理解递归的过程,你可以在jc=x*…那行的前后各加一个PRINT X看看递归调用的顺序.注意千万不能 PRINT JC,因为那就成递归调用了.

有时我们要反复的使用一段程序,每次使用都相似,但某些变量稍有不同,我们就可以用SUB简化程序.当你输入SUB xxx(参数表)时QB会自动换一个新的屏幕,并替你添好END SUB,这是SUB结束命令.DECLARE命令是过程声明,表示你使用了哪个过程很不好拼写?没关系,你也不用写.但当混合语言编程时必须写DECLARE,不过我们暂时用不着.调用过程时直接写xxx 参数表 ,不要括号,就跟其他QB命令一样.也可以用CALL xxx(参数表) 调用下面的例子说明了SUB的作用,因为反复调用了两次。

看下面这个例子:

DECLARE SUB hos (xv!, yv!, zv!, z1!, z2!) '这行是系统自动输入的,不用输

OPTION BASE 1 '设置所有的数组的最低下标为1,而不是0
SCREEN 9, , 0, 1 '你可以试着把9后面的东西去掉,可以看出使用双屏幕页的优点

WINDOW SCREEN (-100, -100)-(200, 200)
FOR z = 400 TO 0 STEP -25
  COLOR 15 - (z MOD 14)
  hos -100, 80, -50, z, z + 20
NEXT
WINDOW
x = -300
Xstep = 15 '此处可调动画速度
LINE (0, 0)-(640, 170), 13,BF
COLOR 14
DO
  IF INKEY$ <> "" THEN END
  LINE (0, 70)-(370, 150), 0, BF
  hos x, 80, -50, 2, 24
  PCOPY 0, 1
  x = x + Xstep
  IF x < -300 OR x > 800 THEN Xstep = -Xstep '碰撞效果
LOOP

DATA 50,100,50,150,150,150,150,100,50,100,100,75,150,100 '五边型坐标

SUB hos (xv, yv, zv, z1, z2)
  DIM x1(7) AS INTEGER, x2(7) AS INTEGER, y1(7) AS INTEGER, y2(7) AS INTEGER
  RESTORE
  p1 = -zv / (z1 - zv) '第一个五边形
  p2 = -zv / (z2 - zv) '第二个五边形
  FOR i = 1 TO 7
    READ x, y
    x1(i) = xv + (x - xv) * p1
    y1(i) = yv + (y - yv) * p1
    x2(i) = xv + (x - xv) * p2
    y2(i) = yv + (y - yv) * p2
  NEXT
  FOR i = 1 TO 6
    LINE (x1(i), y1(i))-(x1(i + 1), y1(i + 1))
    LINE (x2(i), y2(i))-(x2(i + 1), y2(i + 1))
    LINE (x1(i), y1(i))-(x2(i), y2(i))
  NEXT
  LINE (x1(7), y1(7))-(x2(7), y2(7))
END SUB

如何?是不是很兴奋?自己也能做出这样的三维动画! 你可以结合PALETTE等命令不断改变房子的颜色,让它更加奇幻一些.

OPTION BASE 语句后只能跟0或者1,当为1时数组定义a(n)相当于a(1 to n)房子的画法很简单,七个点是房子截面的五个角再加一条横线,画两个五边形再把对应顶点连起来就是个房子了.把这14个点分别用透视变换转成三维投影再用LINE连线就出房子了. 因为三维的直线的投影仍然是直线.第一个FOR循环画一串的房子,为了使前面的房子盖住后面的房子,是倒着画的. 房子很不好看,因为把应该被挡住的线也画出来了.三维遮掩技术很复杂,如果简单的点构成的图形可以用个数组存放每个点的Z轴投影,只把最前面(Z最小)的点显示出来,但对于复杂图形或者以面构成的图形,需要判断多边形(POLYGON)是前是后,这需要高等数学的向量矩阵变换,还有填充技术也很复杂,鉴于读者如果学过这些知识多半就不会看本文了,本文也就不细讨论.而且现在的三维动画通常都直接使用DIRECT3D技术,用不着考虑这么多数学问题.

后面的动画是不断移动视点位置,造成移动效果.由于刚讲完动画,就不多说了.

由于两次画房子的操作,而虽然画的房子不一样,但画房子的方法是一样的,于是我们就定义一个HOS过程,只要传递给过程房子两个五边形的Z坐标,眼睛的位置就能显示出来同样的房子模型,甚至可以用WINDOW改变房子的比例.

这个程序的主要思想是画动画和一串房子,画房子虽然很麻烦,但我们可以把它扔到SUB里去以后再想,专心琢磨如何编动画. 由于QB对过程的"封装",他们之间都是"隔离"的,每个模块都不大,用不着翻很多页去找另一段程序了,只要按一下F2就成了. 而且由于你的过程与具体数据间联系不大,这个模块以后的程序还可以再用,不用重新编.这种编程方法就叫模块化设计, 我们老师教的是要"由上至下",就是先编主程序,再编模块. 可我的习惯总是相反,因为有的模块可能不容易编出来,要用另外的方法去做,因此还要翻回去改主程序,这样就会影响到整个程序. 无论怎样编,采用模块化的思想将会使你编大程序的时候不会觉得枯燥,更容易调试,不容易出错.比如上面那个房子,我在主程序还没编好时就可以看看画出来的房子是否正确,如果所有程序都混在一起,很难挑出来错在哪里.而且编到一半就能看到效果,也解了一部分编程的枯燥,因为编程虽然枯燥无味,但成功的喜悦是非常美妙的. 🙂

… 还没完哦!还有事件与声音、游戏杆、键盘、MODEM控制,和错误与文件处理,系统功能调用和混合语言编程,高级QB编程专题(Mouse,声卡,EMS内存,目录处理…

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注