小游戏中的动画与音效

软件世界

游戏的多媒体输出是评价游戏的重要标准。在小游戏的制作中,以显示输出和音效输出为主。今天我们就通过打飞机这样一个例子来学习游戏中的动画和音效制作。

一、原理

用过Flash的朋友,一定知道关键帧之间的过渡有形状(Shape)和动作(Motion)过渡。一般来说,Shape过渡是图像本身发生变化,而Motion过渡是图像的位置发生变化。类似地,在VisualBasic中,实现动画的主要途径是使用PictureBox或Image控件,在Timer的控制下,用LoadPicture过程定时更换逐渐变化的图像实现类似于Shape的变化,其过程与电影相同;同时控件本身通过改变Left和Top属性发生位移。本例中,每种状态的飞机都有两幅图片,其差别在于螺旋桨角度略有不同。这两幅图片互相更替,就形成了螺旋桨不断旋转的动画效果。实现动画的另一种方式是使用特定的媒体播放器播放已有的动画。例如ShockWaveFlash控件可以用来播放Flash动画。
音效一般有两种:背景音效,贯穿于游戏始终。这里使用API函数mciexecute来播放背景midi文件。mciexecute函数只有一个参数lpstrcommand,所有要进行的操作都要连接成一个字符串传递给该参数,play+空格+文件名和stop+空格+文件名这两个命令分别是播放和停止音乐;事件触发音效,即只有特定的事件才能够发出预定音效。本例中,一旦游戏者击中了飞机,就使用sndplaysound函数来播放预设的wav文件。sndplaysound有两个参数lpszsoundname和uflags,分别是wav文件的文件名和播放命令如何执行的标志。特别提醒的是,事件触发音效使用的sndplaysound函数的uflags参数一定要等于SND_ASYNC(异步播放)而非SND_SYNC(同步播放)。这样,音效的播放才不会影响游戏的进程,否则一旦播放音效,游戏必须处于暂停状态直到播放音效完毕。(以上提到的文件名均包括路径)。
在这个游戏中,游戏者要在规定的时间内准确地用鼠标击中目标飞机,并尽可能地提高命中率。
在窗体上设置两个Label控件,两个Timer控件,其Interval属性分别为500和10,以及包含5个Image控件的控件数组I1,包含6个Image控件的控件数组I2,I2的Visible属性为False。编辑一个主菜单Operation(操作),下有mnunew(新局)、mnustartpause(开始/暂停)、mnuquit(退出)三个菜单项。准备类似(图1)所示6个位图并一一装入I2中,I2(0)~(5)依次是飞机从右向左、从左向右飞行和向下坠毁三个状态的,每个状态两张。控件的背景色一律为飞机图片的背景色,前景色用较亮的颜色。另外准备一个WAV文件用于在击中飞机时播放,一个MID文件作为背景音乐。

图1
图1

游戏中使用的变量意义如下:I是循环变量,Total记录射击次数,Targets记录命中次数,Secr储存剩余时间,Rtn在执行API函数时用于接收返回值,TmpTop用于存储随机产生的Top变化值,Drct,Up分别存储对应索引值I1的运动方向,Drct存储向左(表达为True)或向右(False),Up存储向上(True)或向下(False),Shooted存储是否被击中,Pic显示对应索引值I1的当前图片(True和False分别对应该种状态的两张图片),Running指示程序是否在运行中。游戏中两个初始化过程:PrivateInitialize仅仅初始化参数Idx对应索引值的单个I1,GeneralInitialize进行全局初始化。
I1从I2中读取图片,I1就是游戏者要去击中的对象。如果游戏者击中了I1,那么触发的是I1的MouseDown事件,反之,没有打中I1而是打在了窗体上,触发Form1的MouseDown事件。每隔500毫秒运行的Timer1_Timer事件通过改变Up的值决定下一个500毫秒内各个I1在纵坐标上是向上还是向下,并进行倒计时,与此同时其横坐标在Timer2_Timer的控制下匀速改变。Timer2_Timer还按照飞机的状态控制着I1交替载入该状态的两张图片以实现动画效果。

二、完整代码

Option Explicit
Private Declare Function sndplaysound Lib "winmm.dll" Alias "sndplaysoundA" (Byval lpszsoundname as String, Byval uflags as Long) as Long
Private Declare Function mciexecute Lib "winmm.dll" (Byval lpstrcommand as String) As Long
Private Const SND_ASYNC = &H1
Dim I,Total,Targets,Secr,Rtn,TmpTop as Long
Dim Drct(5),Up(5),Shooted(5),Pic(5),Running as Boolean
Private Sub PrivateInitialize(Idx as Long)
If Rnd >= 0.5 Then '随机确定要初始化的I1(Idx)的运动方向(左/右)
Drct(Idx) = True '向左运动
I1(Idx).Picture = I2(0).Picture
I1(Idx).Left = Me.Width + I1(Idx).Width '初始位置置于窗体右界外
Else
Drct(Idx) = False '向右运动
I1(Idx).Picture = I2(2).Picture
I1(Idx).Left = -I1(Idx).Width'初始位置置于窗体左界外
End If
I1(Idx).Top = 1000 * Idx
I1(Idx).Enabled = True
Shooted(I) = False
Pic(I) = True
End Sub
Private Sub SetTitle()
Label1.Caption = "射击次数:" & LTrim(Str(Total)) & " 命中:" & LTrim(Str(Targets)) & " 命中率" & LTrim(Str(Targets / Total * 100)) & "% "
End Sub
Private Sub GeneralInitialize()
On Error Resume Next
Randomize
Me.MousePointer = 2 '把鼠标指针形状变为十字
For I = 0 To 4
PrivateInitialize (I) '逐个初始化I1
I1(I).Visible = True
Next I
Total = 0
Targets = 0
Running = True
SecR = 61
Label1.Caption = "射击次数:0" & " 命中:0"
Rtn = mciexecute("stop " & App.Path & "\mar.mid") '停止/播放背景音乐
Rtn = mciexecute("play " & App.Path & "\mar.mid")
Timer1.Enabled = True
Timer2.Enabled = True
mnustartpause.Enabled = True
End Sub
Private Sub Form_Load()
mnustartpause.Enabled = False
For I = 0 To 4
I1(I).Visible = False
Next I
Timer1.Enabled = False
Timer2.Enabled = False
Me.WindowState = 2
End Sub
Private Sub Form_MouseDown(Button as Integer, Shift as Integer, X as Single, Y as Single)
If Running = False Then Exit Sub '暂停时不予处理
Total = Total + 1
Settitle
End Sub
Private Sub Form_Unload(Cancel as Integer)
On Error Resume Next
Rtn = mciexecute("stop " & App.Path & "\mar.mid") '停止播放midi
End Sub
Private Sub i1_MouseDown(Index as Integer, Button As Integer, Shift As Integer, X As Single, Y As Single) '打中了I1
On Error Resume Next
If Running = False Or Shooted(Index) = True Then Exit Sub '如果是在暂停时击中或再次击中已击中过的飞机则不算
Shooted(Index) = True
Total = Total + 1
Targets = Targets + 1
I1(Index).Enabled = False
Rtn = sndplaysound(App.Path & "\shoot.wav", SND_ASYNC) '触发音效
SetTitle
End Sub
Private Sub mnunew_Click()
GeneralInitialize
End Sub
Private Sub mnuQuit_Click()
Unload Me
End Sub
Private Sub mnuStartPause_Click()
Running = Not Running
Timer1.Enabled = Running
Timer2.Enabled = Running
End Sub
Private Sub Timer1_Timer()
For I = 0 To 4 '随机决定下周期内飞机向上或向下运动
If Rnd >= 0.5 Then Up(I) = True Else Up(I) = False
Next I
SecR = SecR - 0.5
Label2.Caption = "剩余时间:" & LTrim(Str(Int(SecR)))
If SecR = 0 Then
mnuStartPause_Click
If (MsgBox("时间到,还要再玩么?",vbYesNo + vbInformation, Me.Caption))= vbYes Then GeneralInitialize Else End
End If
End Sub
Private Sub Timer2_Timer()
For I = 0 To 4 '逐个改变I1的位置和图片
Pic(I) = Not Pic(I) '改换图片标记值使True和False对应的图片轮流更替
TmpTop = Int(Rnd * 100 + 50) '随机产生Top将要变化的距离
If Shooted(I) = True Then '如果已经被击中
If Pic(I) = True Then I1(I).Picture = I2(4).Picture Else I1(I).Picture = I2(5).Picture '根据Pic的轮流显示坠机状态的两幅图片
I1(I).Top = I1(I).Top + 300 '向下坠落
If I1(I).Top > Me.Height Then PrivateInitialize (I) '如果已经坠到底部则将该I1初始化
GoTo 100
End If
If I1(I).Top > Me.Height - 2000 Then Up(I) = True '如果超越了下界则强制向上
If I1(I).Top < 200 Then Up(I) = False '如果超越上界则强制向下
If Up(I) = True Then I1(I).Top = I1(I).Top - TmpTop Else I1(I).Top = I1(I).Top + TmpTop '根据Up的值向上或向下移动TmpTop的距离
If Drct(I) = True Then '如果是向左运动的则Left减小,反之增大。50*I的作用是令各飞机的速度有所差别
I1(I).Left = I1(I).Left - 100 - 10 * I
If Pic(I) = True Then I1(I).Picture = I2(0).Picture Else I1(I).Picture = I2(1).Picture
If I1(I).Left < -I1(I).Width Then PrivateInitialize (I) '如果已经运动到最左则初始化
Else
If Pic(I) = True Then I1(I).Picture = I2(2).Picture Else I1(I).Picture = I2(3).Picture
I1(I).Left = I1(I).Left + 100 + 10 * I
If I1(I).Left > Me.Width Then PrivateInitialize (I) '如果已经运动到最右则初始化
End If
100 Next I
End Sub
运行界面如(图2)所示。程序当中的具体数值可以根据个人喜好适当更改。更改随机数TmpTop的上下限、Timer2_Timer中Left加减的数值或Timer2的时间间隔都可以调节飞机运动的速度。

图2
图2

注:本代码在Win2000+ServicePack2+VB6.0环境下调试通过,在Win98下也可顺利运行。