原创.转载请注明出处: 六十六(http://blog.csdn.net/fosly).
如果是转骂就不要注明出处了.
本篇文章中提到的资源和代码可以到这里下载:http://download.csdn.net/detail/fosly/4971639
2.3 游戏再造计划
2.3.1 我们都需要什么
古语云,欲事其必器先其利,看过《三国传》的都知道。
基本上,我们已经将《守》的图片声音等素材都准备妥当了,目前的情况下,直接开始制作我们的同人游戏也未尝不可。但是我想大多数厨师朋友们对编程和电子游戏的理解程度并不会像传说中的什么云风啊,刘柳啊那类妖孽一样,其实那些人都是杜撰出来的啦,大家不要相信。所以为了日后的开发不至于因为事先的准备不足而演变的越发的繁琐而最终夭折掉,我们还是预先将合适的开发工具,简洁的开发方式准备并设计妥当为好。
那我们都需要什么呢?来看看,通过玩这款游戏的经验判断,要想改造它(毁灭它),应该注意这样几个环节,抛开世界观,游戏性和构思不管……就没有什么了。
总之,先照猫画虎弄个雏形出来吧,我听人说,主要分为这些东西:第一是片头动画,然后是游戏界面,这两个可以找美工MM帮忙撸。而贯穿于整个游戏,我们无处不见的一些东西,应该是角色的帧动画,过场演示,地图场景和战斗场景这些大元素。或许我们需要这几样工具,动画编辑打包器,脚本编辑器,地图编辑器和战斗编辑器。这似乎有点令人不知所措啊,毕竟大家都是炒菜出身的,不过没关系,人走人道,鬼走鬼道,反正CSDN上高手那么多,到后面这个东西写不下去的时候,天天去专家的博客上跪求就好了。
2.3.2 动画打包器
如果想让游戏的雏形出现在我们面前,首先我觉得先制作出一个最简单的游戏实现比较好:一个角色在一个场景中自由走动。
CSDN上应该没有吐槽功能,那我想……来,我们就趁着刚刚拆包搞元素的经验,先想办法弄个动画编辑打包的工具吧,下一步我们要把这些元素再打包封回去(靠了,当我们反应过来“既然人家的包格式你都知道了,为啥还要做打包工具——可以直接用原来游戏打好的包啊”的时候,下面那个编辑器的代码已经TMD写完了)。
在提取《守护者之剑》这款游戏的素材时,发现风雷小组给地球人留下了一个很给力的彩蛋,我们可以找到CD2\bto\Ani3.arj,这是一个压缩包,解压后发现,这应该是开发者当时使用过的动画编辑器……
图2.3.2-1 怀疑是动画风雷小组使用过的编辑器
看着界面上那些奇怪的汉字,这款软件貌似是英文版的。我们现在很需要这样一款动画编辑器将游戏的声音和动画重新打包,不过遗憾的是,这款诡异的软件太坑爹了,我鼓捣了很久,结果还是弄不明白应该如何使用(大家不妨试试看,可能是少什么文件)。看来,我们只好动手DIY一个了。
……此处略去两千字,好,就这样,我们的动画编辑器终于做出来了:
图2.3.2-2 自制的动画编辑打包工具
图2.3-2是专门针对《守》制作的多功能动画编辑器,在这款游戏的同人开发过程中,动画编辑器是我们最常使用的工具(靠!靠!靠!想想就崩溃,你为啥不早提醒我,编这个破玩意!靠……现在还在继续看博客的人,如果有兴趣弄这个,并且觉得没必要再次打包的,可以直接跳过这一节)。它可以帮我们以所见即所得的方式编辑人物的显示(和声音)行为,并且生成数据包文件,方便在其他程序中调用。虽然前期开发工具会比较麻烦,但是也只有这样做,我们才能在日后的开发过程中少一些困扰,少一些纠结(靠了)。
当然,我们也完全可以选用一些源自网络或其它途径的成品编辑软件来达到同样的效果,但是在学习和使用工具软件上耗费精力并不是很爽。这个我们可以酌情选择,择其善者。对于本系列提供的这款工具,玩家可以直接使用,也可以自行修改做他用。下面我会先把之前两个自然段那里略去的开发过程写一写,把下面的内容糊弄上,大家愿意看就看看,懒得看就直接跳到后面的章节,不会影响阅读。
好了,让我们打开来历不明的VisualC/C++6.0。
最后还是决定使用VisualC++2008开发,这套比VS6强大多了,还支持竖排选择呢!
……
正在读这篇文章的米娜,你们知道么,这四个自然段之间经历了漫长的三年时间,这个编辑器有个VC6版的,有个VS08版的,还有个VB6版的,三个都是半成品,每次都是当我在决定继续写下去的时候,就发现以前写的代码都看不下去了,还好,这次终于赶上能有时间,于是就一口气弄完了。
在制作工具之前,我们需要动动脑子设计一种存储结构,用来描述一组角色动画。仔细推敲一下会发现,角色动画包中可能会出现的一些元素如下:
源图数据,图像剪裁信息,图层透明度,翻转,缩放等杂碎效果,声音等等,一个动画包中可能包含很多帧,每一帧又包含很多图,而这些图中可能有重复的,我们没必要把重复的东西储存多次。而且,既然有图,肯定就有每个图在每一帧中的相对坐标,以及整个动画包的原点坐标等等。
现在的PC(手机也是)存储盘和内存都很便宜,这给我们带来了很多福利,至少在这种非商用的开发中,我们不用过多的考虑数据压缩,合理的内存规划什么的,当然,这不是个好现象。有那么一天,公司里需要开发一款3D软件,我发现用来存3D模型的OBJ格式是个很科学的东西,所以这里就模仿一下,我们的导出器也这么来整吧,只不过最终导出的是二进制文件,并且没有3D的顶点什么的而已。下面直接上代码吧。
(哦,忘了说明一下,这款导出器也是用VB6做的,这应该是本系列文章中最后一次使用VB6了,有点可惜)
动画包的存储结构如下:
Type F_PACKAGE
nOffX As Long '相对于背景的 X 坐标, 包内所有其他所有 X 坐标以此为参考
nOffY As Long '相对于背景的 Y 坐标, 包内所有其他所有 Y 坐标以此为参考
nFrameCount As Long '包包含的帧数
Frame() As F_FRAME '帧数组
nImgCount As Long '包内载入的图片总数(用作包资源)
img() As F_IMAGE32 '载入包的图片信息
nSndCount As Long '包内载入的音效总数(用作包资源)
snd() As F_SOUND '载入包的音效信息
End Type
Type F_IMAGE32
nWidth As Long '图片宽
nHeight As Long '图片高
Pixels() As Long '像素信息
End Type
Type F_SOUND
nSize As Long '声音文件大小
SndFile() As Byte '声音文件字节信息
End Type
Type F_FRAME
nLayerCount As Long '此帧包含的层数(每一层就是一个图片)
layer() As F_LAYER '层数组
nSoundId As Long '此帧的音效 编号,-1 表示此帧没有插入音效
End Type
Type F_LAYER
nFlip As Long '0-没有翻转 1-水平翻转 2-垂直翻转 (按位与, 3-水平翻转+垂直翻转)
nStretchW As Long '层图片放大/缩小后的实际显示宽度
nStretchH As Long '层图片放大/缩小后的实际显示高度
nOffX As Long '层相对 F_PACKAGE.nOffX 的坐标
nOffY As Long '层相对 F_PACKAGE.nOffY 的坐标
nLeft As Long '层图片剪辑起始坐标
nTop As Long '层图片剪辑起始坐标
nWidth As Long '层图片剪辑宽度
nHeight As Long '层图片剪辑高度
nAlpha As Long '层透明度
imgId As Long '层图片(编号)
End Type
img数组记录了每一张图片的实际像素信息,以每点32位的方式存储(BGRX),nImgCount记录了该数组的元素个数。snd数组记录了每一个音效的实际声音数据,以wav(RIFF)的方式存储,事实上读取wav文件什么的最讨厌了,我只是把wav文件的数据原封不动的放在这里而已,nSndCount记录了该数组的元素个数。Frame数组中则记录了各个图片的排列方式。
这里要注意的有两处,一个是类型问题,VB6中的long类型相当于VC下的32位整型;另一个就是这里的“层”,这种存储方式,每个层实际上就是一个逻辑的图像信息,它只是指定了图像的索引号和宽度,高度等信息,并不实际存储图像的像素点信息。
这样就有了一套还算比较合理的存储方式,我给它定义了一个相当酷的扩展名——“.pkg”,这个扩展名是从日本语“パッケージ(π开鸡)”直译过来的,并且为了显得特别,把第三个字母j换成了g。然后就可以根据这种存储格式开始制作编辑器了。
首先,要有载入图片的功能,为了省事儿,这个软件只支持bmp格式的图片(貌似我可以自己写代码读出来的图也只有BMP格式的了),图片载入后是存到一块儿临时内存空间中的,并且它被归类到“元件库”这个结构中,“元件库”是我在做公司的一个Flash的东西时发现的一个很有趣的东西,在这里它就像个IP连接池一样,负责给全程操作发放ID号,每个ID号都对应了一个图像,大家有兴趣可以通过任何途径获取flash软件瞅一眼。
下面的代码是将一个BMP文件读入并储存为我们定义的F_IMAGE32存储形式:
Sub LoadBmpFileToArray(ByVal bmpPath As String, ByRef img As F_IMAGE32)
Dim bmpFileHeader As BITMAPFILEHEADER
Dim bmpInfoHeader As BITMAPINFOHEADER
Dim nBit As Integer
With img
' 打开文件
Open bmpPath For Binary As #1
' 读取位图文件头
Get #1, , bmpFileHeader
Get #1, , bmpInfoHeader
' 层的宽度和高度(图片宽,高)
.nWidth = bmpInfoHeader.biWidth
.nHeight = bmpInfoHeader.biHeight
' 为颜色数组申请内存空间
ReDim img.Pixels(.nWidth * .nHeight - CLng(1)) As Long
Select Case bmpInfoHeader.biBitCount
Case 32
' 读32位图像
Get #1, , img.Pixels
Case 24
' 处理24位图像
Dim i As Long, j As Long, k As Long: k = 0
Dim r, g, b
Dim nScanLineLenth As Long
nScanLineLenth = .nWidth * 3 + Get24BitScanLineZeroCount(.nWidth)
ReDim scanLineBytes(nScanLineLenth - 1) As Byte
For i = 0 To .nHeight - 1
Get #1, , scanLineBytes
For j = 0 To .nWidth * 3 - 1 Step 3
'r = scanLineBytes(j)
'g = scanLineBytes(j + 1)
'b = scanLineBytes(j + 2)
'layer.Img(k) = b * 65536 + g * 256 + r '这种写法注意避免r,g,b溢出
img.Pixels(k) = RGB(scanLineBytes(j), scanLineBytes(j + 1), scanLineBytes(j + 2))
k = k + 1
Next j
Next i
Case Else
Call MsgBox("本程序只能处理24位和32位标准BMP格式!", vbOKOnly, "警告")
End Select
' 关闭文件
Close #1
End With
End Sub
BMP图像的文件头格式VB版会像下面这样:
''''''''''''''''''''''''''''''''''''
' 结构: BITMAPFILEHEADER
' 信息: BMP文件头第一段(共两段), BMP文件读写用
'
''''''''''''''''''''''''''''''''''''
Type BITMAPFILEHEADER
bfType(0 To 1) As Byte
bfSize As Long
bfReserved1 As Integer
bfReserved2 As Integer
bfOffBits As Long
End Type
''''''''''''''''''''''''''''''''''''
' 结构: BITMAPINFOHEADER
' 信息: BMP文件头第二段(共两段), BMP文件读写用
'
''''''''''''''''''''''''''''''''''''
Type BITMAPINFOHEADER
biSize As Long
biWidth As Long
biHeight As Long
biPlanes As Integer
biBitCount As Integer
biCompression As Long
biSizeImage As Long
biXPelsPerMeter As Long
biYPelsPerMeter As Long
biClrUsed As Long
biClrImportant As Long
End Type
格式和读取都很简单,就是读取24位图像的时候别忘了有个扫描线补齐(扫描线就是一个像素行,行的字节数模4为0时就算补齐了)的小操作,如果看得感觉奇怪,可以上百度找度娘帮自己撸一撸,那样会舒服一些。
还有一个小问题,就是读入的图像数据中,来自文件的图像宽度和高度不一定是正确的,加之本程序没有操作回溯功能,一般情况下如果读图时出错了,内存申请就会出错,就可能内存溢出,从而它就崩溃了……为了不出岔子,而且碰巧即使文件中存储的长度和宽度不对,强大的Win32API仍能正确读取图片,我尝试使用GetPixel()函数来确定图像的实际宽高,代码如下:
Function GetImgWidthFromDC(ByVal hdc As Long) As Integer
Dim iWidth As Integer: iWidth = 0
'循环,直到读入颜色失败
While (GetPixel(hdc, iWidth, 0) <> -1)
iWidth = iWidth + 1
Wend
GetImgWidthFromDC = iWidth
End Function
然后意外地发现,这样真的不出错了,于是就这么凑合用了。
读取后所有的图像缩略图都会显示在主界面的右侧元件库栏目,并且单击后在ClipsImg窗体可以对其进行逻辑修改,所谓的逻辑修改就是只改动对应的帧的参数,而不是真的去改动源图,因为源图是所有帧共享的。
图2.3.2-3 逻辑修改
软件最下面那一排小缩略图,是场景的快照图片,每当场景内容改变时,快照图片都会实时变化,为了使工程下次打开时可以恢复到上一次编辑完成时的状态,我们又不得不自定义一种“工程”文件格式,用以记录软件操作的中间状态,工程文件在概念上相当于pkg包的源文件。
Type F_PACKAGEINFO
nCurrActiveFrame As Long '当前活动帧
nCurrActiveLayer As Long '当前活动层(0-8), 第8层是包坐标图例
nCurrChoseLayer As Long '当前选中层
isMouseOnDragging As Boolean '鼠标是否正在拖动某一对象
thumbnail() As Long '缩略图数据
strIdName() As String '记录音效Id对应的音效名称
End Type
在用户点击保存按钮时,F_PACKAGEINFO结构用来记录软件当前的状态,比如当前正在编辑的是哪一帧,当前正在编辑的图层是第几层等等,而thumbinail数组存储的就是刚才提到的快照图片信息。thumbinail是一个二维数组,它的第一维用来存储图像的像素信息,第二维表示对应帧的编号。
最后的strIdName用来记录音效Id对应的音效名称(音效文件必须和它服务的.pproj工程文件放在一起)。这里有一个奇怪的现象,使用VB6中的string类型存储字符串是与其它主流语言不一样的,string类型的字符串不带'\0'结尾,所以我觉得它不适合直接保存到文件中,因此本程序在保存它时将它转换成宽字符Byte数组,两个Bytes构成一个字符,并且在每句话之前用四个字节记录后面的字节数长度(NND,我也不用'\0',而且下面两段代码也不是我写的=_=~)。
''''''''''''''''''''''''''''''''''''
' 函数: StringToByteArray
' 参数:
' ByteArray: 用作返回的Byte数组(注意需要自主申请空间)
' str: 要转换的字符串
' size: 要转换的字符串的长度
'
' 返回: 无
' 功能: String转换成Byte数组
'
Function StringToByteArray(ByRef byteArray() As Byte, ByRef str As String, ByVal size As Long)
Dim i As Long, c As String, icode As Integer
For i = 0 To size - 1
byteArray(i) = AscB(MidB(str, i + 1, 1))
Next i
End Function
''''''''''''''''''''''''''''''''''''
' 函数: ByteArrayToString
' 参数:
' str: 用作返回的字符串
' size: 要转换的字符串的长度
' ByteArray: 要转换的Byte数组
'
' 返回: 无
' 功能: Byte数组转String
'
Function ByteArrayToString(ByRef str As String, ByRef byteArray() As Byte, ByVal size As Long)
Dim i As Long, c As Byte
str = ""
For i = 0 To size - 1
str = str & ChrB(byteArray(i))
Next i
End Function
既然有层的概念,那么必然会出现上移、下移、插入、删除和复制层等操作。本程序的设计方法非常单纯,每一帧的显示方法都是,先画舞台背景(与包文件无关),完后画最底层,完后画最底层之上的另外基层,最后画选框什么的,后来为了使用方便,又加入了类似于flash中那种洋葱皮功能,在当前帧可以显示上一帧的半透明图像,但是不能编辑,这其实就是在除了舞台背景外所有绘制的对象之上又填了一层而已。所以层的各种操作无非就是改变一下各层的参数,最后再重绘这一帧。
不过令人挠头的事情在于,VB6在图像处理方面,如果用标准控件的话,速度上实在是不敢恭维,据说是用DX、GDI+或是其它的第三方图像显示插件处理会好些,但是我一时找不到能让人看下去的资料,所以暂时就使用内存块的方式先把图像剪裁啊,翻转啊或是alpha混和什么的在数组里处理完之后,再直接调用Win32API中的BitBlt函数,将内存块Blit上去。后来发现,这样做的话,处理图像的速度其实也不是很慢,勉强能够接受。
''''''''''''''''''''''''''''''''''''
' 过程: AlphaBlendBlit
' 参数:
' desDC: 目标DC
' nDesX: 目标区域绘制的开始位置坐标 X
' nDesY: 目标区域绘制的开始位置坐标 Y
' nDesW: 目标区域要绘制的宽度
' nDesH: 目标区域要绘制的高度
' srcDC: 源DC
' nSrcW: 源区域实际宽度
' nSrcH: 源区域实际高度
' alpha: 混合值 [0..255] 0为完全透明
' nKeyColor: 透明色键 (参数可选)
' nFlip: 翻转图像 0-不翻转 1-水平翻转 2-垂直翻转 3-水平翻转+垂直翻转 (参数可选)
'
' 返回: 无
' 功能: 在目标区域绘制一块带透明混合的图像, 带有stretchBlt的功能
'
Sub AlphaBlendBlit(ByVal desDC As Long, ByVal nDesX As Long, ByVal nDesY As Long, ByVal nDesW As Long, ByVal nDesH As Long, _
ByVal srcDC As Long, ByVal nSrcW As Long, ByVal nSrcH As Long, ByVal alpha As Byte, _
Optional ByVal nKeyColor As Long = -1, Optional ByVal nFlip As Long = 0)
Dim tmpDesDC As Long, tmpDesBmp As Long
Dim tmpSrcDC As Long, tmpSrcBmp As Long
Dim desData() As Long, srcData() As Long
Dim info As BITMAPINFO
Dim k As Long
Dim i As Long, j As Long, tmpColor As Long
'完全透明 或者 没有显示的数据时,直接返回
If alpha = 0 Or nDesW = 0 Or nDesH = 0 Then Exit Sub
'创建与源 DC 相匹配的 DC
tmpDesDC = CreateCompatibleDC(srcDC)
tmpSrcDC = CreateCompatibleDC(srcDC)
'创建与目的 DC 相匹配的 位图对象
tmpDesBmp = CreateCompatibleBitmap(desDC, nDesW, nDesH)
tmpSrcBmp = CreateCompatibleBitmap(desDC, nDesW, nDesH)
'设置关联
SelectObject tmpDesDC, tmpDesBmp
SelectObject tmpSrcDC, tmpSrcBmp
'申请与图片相关的内存空间
ReDim desData(nDesW * nDesH * 4 - 1)
ReDim srcData(nDesW * nDesH * 4 - 1)
'设置DIB头
With info.bmiHeader
.biSize = Len(info.bmiHeader)
.biWidth = nDesW
.biHeight = nDesH
.biPlanes = 1
.biBitCount = 32
.biCompression = 0
End With
' 初始化DC位图信息
BitBlt tmpDesDC, 0, 0, nDesW, nDesH, desDC, nDesX, nDesY, vbSrcCopy
SetStretchBltMode tmpSrcDC, STRETCH_HALFTONE '设置缩放图片的模式, 避免失真(第一个参数是目标DC)
StretchBlt tmpSrcDC, 0, 0, nDesW, nDesH, srcDC, 0, 0, nSrcW, nSrcH, vbSrcCopy
' 取得位图中的像素块到数组
GetDIBits tmpDesDC, tmpDesBmp, 0, nDesH, desData(0), info, 0 '0==DIB_RGB_COLORS
GetDIBits tmpSrcDC, tmpSrcBmp, 0, nDesH, srcData(0), info, 0 '0==DIB_RGB_COLORS
' 水平翻转
If (nFlip And CLng(&H1)) <> 0 Then
For j = 0 To nDesH - 1
For i = 0 To (nDesW - (nDesW Mod 2)) \ 2 - 1
tmpColor = srcData((j * nDesW) + (nDesW - i - 1))
srcData((j * nDesW) + (nDesW - i - 1)) = srcData(j * nDesW + i)
srcData((j * nDesW) + i) = tmpColor
Next i
Next j
End If
' 垂直翻转
If (nFlip And CLng(&H2)) <> 0 Then
For i = 0 To nDesW - 1
For j = 0 To (nDesH - (nDesH Mod 2)) \ 2 - 1
tmpColor = srcData((nDesH - j - 1) * nDesW + i)
srcData((nDesH - j - 1) * nDesW + i) = srcData(j * nDesW + i)
srcData(j * nDesW + i) = tmpColor
Next j
Next i
End If
' 开始混合
For k = 0 To nDesH * nDesW - 1
' IsMissing() 函数判断是否忽略了参数, 只能用于Variant变量
If (-1 <> nKeyColor) And (srcData(k) = nKeyColor) Then
' 当有透明色键参数时, 并且当前点的颜色==色键, 完全透明
' ......(对目标绘制区域没有影响, 什么也不做)
Else
If alpha = 255 Then
'完全不透明
desData(k) = srcData(k)
ElseIf alpha = 0 Then
' 完全透明
' ......(对目标绘制区域没有影响, 什么也不做)
Else
'按照透明色键的值, 和alpha值 进行混合
desData(k) = (srcData(k) And &HFF) * alpha / 255 + (desData(k) And &HFF) * (255 - alpha) / 255 Or _
((srcData(k) And &HFF00&) * alpha / 255 + (desData(k) And &HFF00&) * (255 - alpha) / 255) And &HFF00& Or _
((srcData(k) And &HFF0000) * (alpha / 255) + (desData(k) And &HFF0000) * ((255 - alpha) / 255)) And &HFF0000
End If
End If
Next k
' 将数组中的颜色信息写入目标DC
SetDIBitsToDevice desDC, nDesX, nDesY, nDesW, nDesH, 0, 0, 0, nDesH, desData(0), info, 0
' 清空数组和DC相关对象
Erase desData
Erase srcData
DeleteObject tmpDesBmp
DeleteObject tmpSrcBmp
DeleteDC tmpDesDC
DeleteDC tmpSrcDC
End Sub
用这种显示方法,经过测试,在我的机器上更新显示场景的代码DrawStage()无耻的耗费了172毫秒,这样导致在全大图情况下,鼠标拖动图层的时候会显得很卡,这是因为每次鼠标移动的时候,程序都会得到系统发来的一连串鼠标移动的消息,导致这一帧不断被重绘,通过跳帧的代码可以调整鼠标消息的处理次数,从而缓解这一症状,不过不是非常理想:
Static tmLastTime As Long
If GetTickCount() - tmLastTime < 1000 \ 60 Then Exit Sub
tmLastTime = GetTickCount()
再有,程序中所有控制图层或帧显示的参数输入框的OnChange事件几乎都关联着一次场景的更新显示(DrawStage()),我们还要通过设置标识量来区分,究竟是用户手动改变的数值,还是程序内部对其进行设置的。对于所有来自程序内部的设置,我们都可以忽略掉相应的OnChange()事件。
基本上就是这样,软件还是有很多缺陷和不尽人意的地方,比如不能撤销操作,生成的包太大,啥啥的。为了方便调试,也没有做过多的优化,大家可以根据自己喜好自行修改或者……重新做一个。
一款坑爹的动画打包器就这样诞生了。
本篇文章中提到的资源和代码可以到这里下载:http://download.csdn.net/detail/fosly/4971639
<<十万个冷笑话>>更新啦~~