SpriteEditor 2019 - 代码使用说明 - 错误!






3.80/5 (4投票s)
通过一个例子来使用你的精灵,这将给有“bug”的代码赋予新的含义
- 下载 bug - 4 MB
- 下载 LadyBug - 7 MB
- 下载 BedBug - 8.8 MB
- 下载 Ant - 5.9 MB
- 下载 YellowBeetle - 2.1 MB
- 下载 Spider - 1.7 MB
- 下载 WhiteBug - 1.3 MB
- 下载 GiantWaterBug - 1.1 MB
引言
那么,你有机会看到一些 SpriteEditor2017 的代码吗?因为在这篇文章中,我将尝试解释 classSprite
中一些更重要的部分,以及其他参与制作精灵的类。如果你还记得,在 SpriteEditor2017 系列的第一篇文章中,我提到了一篇文章和我在十年前在 CodeProject 发表的另一个 SpriteEditor
。原创文章 Sprite Editor for .NET 获得了大量的点击,我愿意相信它是许多创意不眠之夜、错过讲座和病假工作时间的来源。所有人都玩得很开心。
我今天要谈论的一些事情
- 精灵制作器 - 加载、创建和保存精灵
- 精灵类 - 编译、旋转和定位精灵肢体以制作动画帧
- 如何将精灵包含到你的应用程序中 - 让你的梦想成真... (不包括 Nastia Liukin)
精灵如何工作
精灵,我相信你知道,是一系列由热情的开发人员精心定位的图像,以创建可以轻松合并到任何项目中的流畅动画。但它们是如何工作的呢?作为入门,你可以重新阅读上面提到的文章,并对 SpriteEditor2017 的工作原理有一个大致的了解(它们本质上非常相似),但我们还是要在这里介绍一些基本的东西。
每个精灵都有
- 一个肢体列表,每个肢体都有独特的名称
- 这些肢体有存储在 AlphaTree 中的指针,可以通过名称存储/检索它们
- 一个动画列表,每个动画也都有唯一的标识名称
这本质上就是所有主要组件。
classSprite_Limb
是精灵的基本组件之一,所以我们来看看这个。这些图像不包括你在 classSprite_Limb
中使用 MS Intellisense 时的所有内容,但它们显示了基本要素。这个类本身实际上只保存 classSprite
用于生成动画帧的信息。在肢体的图像旋转之后,肢体中没有进行太多的工作。所以这个类相对简单。
你有属性
bmpDraw
:此肢体最近使用的图像DisplayPosition
:由 SpriteEditor 的 UI 使用DrawAngle
:上次绘制的角度Hinge
:指向将此肢体绑定到其主肢体的classSprite_Hinge
的指针ptDrawCenter
:肢体中心相对于精灵在最近绘制的动画帧中的MasterLimb
的位置SetDrawPosition
:一个布尔变量,用于确定在classSprite
当前绘制的帧中是否应该计算此肢体的位置(可以节省许多不必要的三角函数和长除法计算,并加快动画的交付速度)
一些字段是
cAT_LimbImages
- AT 代表 AlphaTree,在这个项目中 cAT 随处可见。它们是三元搜索引擎,在每次搜索迭代中都有 26 个以上的分支节点,为字母表的每个字母使用不同的分支。它们非常快,可以容纳任何转换为通用 VisualStudioobject
类的各种数据。只要记住你在哪棵树中存储了什么,将树中的所有对象保持相同的数据类型,我稍后会再次提到的classAlphaTree
非常快速、可靠和通用。在这里,顾名思义,这个 cAT 包含此肢体类的特定实例所包含的各种图像。intImageList_Index
:用于绘制此肢体的图像列表的索引(同一图像每完整旋转 128 次)lstHinge_Slaves
:将此肢体作为主控绑定到其他从其分支出来的肢体的铰链列表。lstImages
- 包含与刚刚提到的 cAT 相同的数据,并且经常用于其.Count
方法,例如,当 SpriteEditor2017 必须将所有这些图像显示到屏幕上时。
/// <summary>
/// item in list of limb images.
/// one version of a limb's appearance
/// </summary>
public class classSprite_Limb_Images
{
public string Name = conDefaultLimbImageName;
public Bitmap[] bmpImages = null;
}
让我们看看 classSprite_Hinge
。
在这里,你可以看到一些属性
Limb_Master
-Limb_Slave
:是指向此铰链的主肢和从肢的指针Angle
指的是此铰链旋转的角度,将其从肢以相对于其主肢的角度定位。SlaveContact_MagnitudeFactor
:用于定位从肢相对于铰链及其主肢的弧度坐标的幅值乘数。最后两个变量(Angle
和SlaveContact_MagnitudeFactor
)在每个动画的每一帧中设置,让开发人员可以比早期版本的 Sprite Editor 更灵活地定位和摆放精灵。
这里显示的字段大部分是上面列出的属性来源的重复,除了
Contact
:包含弧度坐标Master
和Slave
,它们将铰链定位相对于从肢和主肢,因此从肢相对于其主肢。这些弧度坐标与上面提到的Angle
和SlaveContact_MagnitudeFactor
不同。这些是使用 SpriteEditor2017 的蓝色HingeContact_Graphical
面板设置的值,而Angle
和SlaveContact_MagnitudeFactor
是可以使用橙色FrameEditor
面板更改的值。Contact
值是半永久性的,也就是说,它们可以在 SpriteEditor 中更改,但是一旦精灵保存到文件并准备好在应用程序中使用,就没有方便的方法在运行时更改这些值。然而,Angle
和SlaveContact_MagnitudeFactor
变量在每个动画的每一帧中都会更改,这使得精灵编辑器在绘制 2D 动画方面非常出色。
当然,你可能已经意识到大部分工作发生在 classSprite
中。这是一个该类的完整 IntelliSense 列表,我们可以回顾一下这个重要类的内部工作原理。你不需要了解这些即可使用 SpriteEditor,但在需要将你的精灵包含到其他项目中时,其中大部分内容将派上用场。所以我们来了...
属性
DrawSizeFactor
:一个double
变量,用于调整输出图像的大小Name
:你可能已经猜到了NumImagesPerQuadrant
:这是一个肢体图像在每个四分之一旋转中旋转和绘制的次数。默认值为32
,但你可以将其设置为任何值。如果你这样做,我建议你在开始构建你的精灵之前这样做。我编写此方法是为了在设置此值时它将重新调整和重新生成所有肢体图像,但我从未测试过它,这就是 SpriteEditor 不提供你选择在中途更改所用图像数量的原因。如果你要更改此值,请更改常量默认值并保持不变。它可能工作正常,但我从未测试过,也不会为此担保。对我来说,每个四分之一旋转 32 张图像似乎没问题。UseRAMCache
:似乎不言自明,但可能需要稍微解释一下。RAMCache
是一个布尔变量,它决定在生成动画帧时是否使用精灵的帧缓存系统。draw_Animation_Frame(ref classSpriteData cSpriteData)
函数会询问自己所请求的帧图像是否与它存储在内存中用于该帧的图像相同。如果是,它就不会费心去做定位肢体并将适当旋转的肢体图像按照预定的绘制顺序绘制到新位图上的所有工作,而是直接返回上次请求此动画帧时它发送的相同图像。提供一个现成的图像比重新绘制所有内容要快得多,所以当你能够做到时,使用缓存非常方便。唯一的问题是有些帧是旋转的,有些是调整大小的...这是它做出的决定测试if (UseRamCache && cFrame.cCache.DrawAngle == cLimb_Master.DrawAngle && cFrame.cCache.DrawSizeFactor == DrawSizeFactor && string.Compare(cFrame.cCache.strLimbDrawIndex, cFrame.strLimbDrawIndex_CacheTest) == 0) { // cached image good cSpriteData.moveData(); cSpriteData.cDrawData_current = cFrame.cCache.cDrawData.Copy(); bmpRetVal = cFrame.cCache.bmp; goto cacheExit; }
接下来,我们有字段
cAT_Animations
- 是一个alphaTree
,与我之前提到的类完全相同。它们易于使用,可靠且多功能。这个树保存你精灵的所有动画,因此如果你没有方便的枚举类型告诉你使用哪个索引来访问给定的动画,你可以通过名称来请求它。第一个示例来自
classSpriteMaker
的Load()
函数,该函数从文件中加载动画,然后通过将classAnimation
实例转换为泛型对象类型的变量,然后使用该动画的名称将其插入树中,从而将指向它的指针存储在cAT_Animations
alphatree
中。object objAnimation = (object)cAnimation; cRetVal.cAT_Animations.Insert(ref objAnimation, cAnimation.Name);
下一个示例使用相同的
alphatree
,以便告知精灵类用户在 SpriteEditor 的Animation
选择combobox
中选择了哪个动画。cSpriteData.cAnimationData.cAnimation = (classAnimation)cSpriteData .cAnimationData .cSprite .cAT_Animations .Search(cmbAnimation.Text);
这是我第一次提到
classSpriteData
。你将熟悉它,因为它就是你用来告诉classSprite
你想要哪个帧以及你想要它的方式的工具。你也可以遍历这些
alphaTree
以获取其中对象的完整列表。在这里,进行遍历是为了重建树。请注意,这很容易通过使用列表来完成。我只是一个沉迷于将我的alphaTree
遍布城镇的“树痴”。public void ChangeName(string NewName) { classAnimation cMyReference = this; Name = NewName; List<classalphanumtree.classtraversalreport_record> lstTraversalReport = cSprite.cAT_Animations.TraverseTree_InOrder(); cSprite.cAT_Animations = new classAlphaNumTree(); for (int intAnimationCounter = 0; intAnimationCounter < lstTraversalReport.Count; intAnimationCounter++) { classAnimation cAnimation = (classAnimation)lstTraversalReport[intAnimationCounter].data; cSprite.cAT_Animations.Insert(ref lstTraversalReport[intAnimationCounter].data, cAnimation.Name); } }
lstAnimations
- 包含与alphatree
相同的所有动画。保留一个列表很方便,原因有几个。它们易于操作,索引比进行alphaTree
搜索更快,并且它们具有.Count
方法,可以立即告诉你你有多少个。cAT_Limbs
- 你可能已经注意到这些变量的命名方式,这也是一个alphaTree
。这些东西非常方便。它们的成本不高,因为从内存方面来说,你所做的只是保存指向你已经拥有的数据的指针,而指针对内存的负担不大。请注意,alpha 树的每个级别都必须携带 26 个null
指针到同一树的更多节点的 branches,所以使用更长的名称来标识你的数据会消耗一些内存,但与图形和精灵包含的所有这些旋转图像相比,这些指针和节点 branches 微不足道。如果你遇到内存问题,你可能需要寻找其他地方来解决,否则,为你的精灵和肢体使用更短的名称,这将减少alphatree
的开销。我怀疑你拥有的任何精灵会有如此多的肢体和动画,以至于alphatree
成为你的内存问题。减小精灵的大小并继续,或者构建几个具有不同动画的相同精灵,并加载用户当前环境所需的精灵。lstLimbs
- 这是你精灵所有肢体的完整列表。你可以为此目的使用枚举类型来找到它们。这里的肢体与上面cAT_Limbs
指向的肢体相同。cLimb_Master
- 不动的推力,肢体的大佬,被选中者。这就是我在上一篇文章中谈到的肢体。当你创建一个新精灵时,它就在那里。所有东西都围绕着它构建,当你改变drawangle
时,你正在旋转cLimb_Master
。它没有主肢体,也没有铰链。因此,在整个classSprite
和SpriteEditor
中都必须进行测试,以区分普通肢体和MasterLimb
,因为对肢体进行的工作有一半涉及引用它们的铰链和主肢体,而cLimb_Master
对此感到不满。lstLimbSlavesGetRetVal
- 是一个内部使用的列表,由一个递归函数使用,该函数查找给定肢体的所有从属肢体。semDrawAnimation_Frame
- 因为精灵使用classSpriteData
,它本质上是一个可移除的数据盒,你可以插入其中,告诉精灵要运行哪个动画和帧。需要“数据盒”是因为精灵在屏幕上的位置会随着精灵类根据给定肢体绘制而改变,并且从一个肢体到另一个肢体的过渡需要记住精灵上次绘制时的位置。当你只绘制给定精灵的一个实例时,这都很好,但是当你在屏幕上有十二个或二十个相同的精灵,穿着不同的服装并执行相同的动画时(在下一篇文章中,我将向你展示我的体操运动员克隆机,它使用肢体和动画列表相同的数十个精灵进行动画,但每个精灵都穿着不同的“制服”,并且必须拥有自己的classSpriteData
来记住她在你的桌面舞台上侧身移动和翻筋斗时的位置。所以,言归正传,需要一个信号量来确保精灵类实例正在使用正确的数据,并且这些正确的数据不会被其他穿着粉色或蓝色紧身衣的跳舞体操运动员损坏。Animation
:Alphabetize
、Delete
和New
- 不言自明 - 没有惊喜Limb
:New
、Delete
- 也相当简单明了Limb_Slaves_Get
- 使用一个递归函数,该函数追踪给定肢体的所有从属肢体,并将它们收集到一个全局列表变量中setDrawPoints_PrePass
- 遍历动画帧的DrawSequence
并跟踪每个需要绘制的肢体的从属肢体。每个从属肢体的布尔变量告诉draw_Animation_Frame()
函数在setDrawPoints()
函数期间需要定位和旋转哪些肢体。setDrawPoints
- 从MasterLimb
递归分支,通过所有在PrePass
期间设置了SetDrawPosition
布尔变量的从属肢体,并根据它们的主肢和它们自己的铰链angle
/SlaveMagnificationFactor
的状态定位这些肢体。Sprite
类可以定位所有肢体,跳过PrePass
并忽略SetDrawPosition
,但这样做可以让你拥有任意数量的肢体,而无需进行定位你未看到的肢体的数学计算。我当时写这个的目的是针对任何人形角色,特别是 Nastia Liukin。撰写本文时,Gymnast.sp3 精灵只有面向前方的体操运动员,但实际上,我打算花时间制作左侧和右侧轮廓,以及背面,并将它们都放在同一个精灵中。你可以使用这个SpriteEditor
来做到这一点,只需将整个LeftProfile
肢体列表滑到DrawSequence
中的 -do-not-draw- 标记下方,然后继续你的日常工作。通过setDrawPoints_PrePass
,你可以拥有任意数量的肢体,而不会被定位和倾斜不需要且未出现在屏幕上的肢体所涉及的所有长除法和三角函数所困扰。只需确保你的精灵的每个版本都从MasterLimb
分支出来,然后PrePass
将找到第一个,取消选中其布尔值并跳过,不再浪费时间处理它们。setFrameAnimationVariables
- 获取当前动画帧并复制将每个肢体的所有铰链转换为该精灵动画设计者设定的值的变量,以按预期姿态定位肢体setSpriteData_PT
- 使用classSpriteData
计算精灵在当前动画帧中绘制后的当前位置。draw_Animation_Frame
- 这个函数包揽了一切。它进行预处理和处理,然后定位和绘制。它甚至还支持 Windows。你以一个精心填充的classSpriteData
实例的形式发送你的位图请求,这个函数将为你准备好一切,耗时 30 微秒,然后将你想要的图像以你喜欢的大小和角度递给你。它甚至会进行按压和折叠,但工会代表对此有意见。
Animation
类除了作为一个列表帧的容器,通过 Add
、Sub
和 Capture
函数进行管理之外,实际上并没有做太多事情。
cSprite
- 只是一个指向其所属精灵的指针lstFrames
- 它的主要内容,就是它的全部作用。一个动画本质上真的只是一个帧的集合。Name
- 呃...我是睡着了吗...
这里的方法也没有多少...真正核心的是 Frames
类。
Add
、Capture
和Sub
- 这里没有什么特别的。DrawList_Add
- 将新创建的肢体添加到其帧列表的每个帧中
差不多就是这样了。
所以,这里是精灵绘制你想要的图像所需知道的一切。它一点也不复杂。请记住,每个肢体都需要像设计者使用 SpriteEditor2017 IDE 定位一样在屏幕上定位。为此,它必须知道所有东西的位置,它通过记住每个肢体的角度和 SlaveMagnitudeFactor
来做到这一点。“但是等等,”我能听到房间角落里一些更敏锐的头脑的热情灵魂说,上面图像提供的列表中既没有 Angle
也没有 SlaveMagnitudeFactor
字样,你是对的,因为这两个非常重要的变量包含在 classAnimation_Frame_LimbData
中,我很高兴稍后和你一起讨论。我们快到了。
Limb_DrawRelativeTo
- 是一个指向其他所有肢体都要围绕其绘制的肢体的指针。但是当精灵生成一个LimbDrawRelativeTo
不是MasterLimb
的帧时,实际上内部发生的事情与其他任何帧都相同。图像的绘制方式完全相同,唯一的区别是信息打包的方式。Limb
s 本身会告诉你它们相对于MasterLimb
的位置,但是classSpriteData
的cDrawData_Current
会告诉你每个肢体相对于帧位图的左上角的位置。既然你知道你在屏幕上放置位图的位置,这可能对你来说更容易处理,并且classSprite
肢体的值会随着下一次调用draw_Animation_Frame()
的下一帧而改变,这可能与你正在处理的值相同,也可能不同,具体取决于你的动画角色中有多少个使用相同的精灵。Horizontal
和Vertical Mirror
s - 是布尔变量,用于确定你获得的图像是否翻转,以及classSpriteData
的肢体位置是否反映其新外观cCache
是一个类,它跟踪此帧上次绘制时的参数和位图,使你可以通过直接从缓存中获取内存中已有的副本,而不是重新绘制所有内容来加快速度ptTL
和ptBR
- 是与MasterLimb
的距离,两者相减即可得到输出位图的大小lstData
- 是一个classAnimation_Frame_LimbData
类型的列表,它告诉帧以什么角度绘制每个肢体,相对于其masterLimb
移动多远,以及绘制肢体的LimbImage
的索引。分别对应:Angle
、SlaveMultiplicationFactor
和Limb
,如下图所示。我唯一遗漏的是,有肢体LimbImage
名称的副本,因此你可以按名称搜索它。它还复制了列表中所有LimbImage
名称,然后将此列表与请求的帧的列表进行比较,以确保缓存正常。在撰写本文时,我意识到我可以更改许多内容以提高质量。具体来说,此测试的原因是帧的图像列表不会在一分钟内更改,因此你可以安全地假设每个肢体请求的肢体图像将始终相同... -> 无用的测试,我会看看当我删除它时会发生什么。此外,draw_Animation_Frame()
函数在它也可以轻松地绘制相同的旧图像并根据需要调整大小、从缓存中翻转时,将所有内容乘以DrawSizeFactor
。我将对此进行调查,并在进行一些测试后可能会更新第一篇文章和classSprite
...或者因为它稳定,我可以把它留给你。
.
接下来,我们来到我们的大结局,classSpriteMaker
,其 Intellisense 如下所示
eDebugOutput
- 对于调试Save()
/Load()
函数至关重要。fs
- 是一个全局filestream
。将filestream
设为全局变量可能不被推荐或建议,但它帮助我解决了我在调试函数中使用它来跟踪流每一步的位置时的一些问题。你可能会从大量对调试器的调用中检测和推断出,我在编写这些函数时非常沮丧,为了测试我的文件流并为Load()
和Save()
函数中的每个读取/保存操作记录所有内容两次。随着我的进展不断添加内容,使得文件流难以保持稳定,如果没有这些调试工具,如果我最终没有因沮丧而放弃,我将花费更长的时间。所以现在,fs
变量是global
的,我不会改变它,以此纪念那些美好的旧时光。- 所有这些
Debug
字段都与加载和保存精灵到文件所涉及的调试有关。有一个rtx
文本框,它根据代码中的循环格式化数据并缩进所有内容,其中嵌入的循环比其他循环缩进得更深一些。
Bug
它不是你所想的那样。
bugs
应用程序是一个有趣的应用程序,其结果是制作一个新精灵的快速(两三个小时)动画是如此容易。我不得不使用 SpriteEditor
来测试它并随着我的进行发现 bug。现在它已经非常稳定了,这都要归功于这些 bug。对于大多数程序员、黑客和开发人员来说,bug 是一个令人头疼的词。这些小动物制作起来很有趣,看着它们移动让我感到非常满意。
所以,Bugs
应用程序是我将向你提供的第一个示例,说明如何在 SpriteEditor
之外使用 classSprite
。这是一个简单的应用程序,它在其工作目录中搜索精灵,然后加载它可以找到的每一个精灵,并运行它们动画列表中的第一个动画。由于这些动画考虑了移动它们的 DrawRelativeTo
肢体,它们有三种状态:TurnLeft
、TurnRight
和 Crawl
,但所有这三种状态都使用相同的 Crawl
动画绘制。即使 Ant
有一个钳子动画,Bugs
也会忽略它。所以它们会朝一个方向爬行一段时间,然后,它们会稍微转动几步,然后继续行走。
简单!
它们可以在你的桌面上爬来爬去,因为它们都使用了我从 CodeProject 下载的代码,名为 Per Pixel Alpha Blend,它绘制窗体,使得所有透明颜色都消失,你看到的只有绘制的图像,你的桌面看起来就像周围没有方形盒子状的窗体一样。差不多就是这样了。那么,让我们看看将精灵加载到应用程序中的代码。
它有两个窗体,也就是说,生成所有其他窗体的主窗体 formBugs.cs,然后它会生成多个第二种类型的窗体实例,这些窗体显示并采用它作为精灵加载的 bug 的形状和图像。这个窗体是用这行声明的
public partial class formCreepyCrawler : PerPixelForm.PerPixelAlphaForm
但首先,我们需要声明一个 classSpriteMaker
的实例,它负责所有精灵的加载/保存。在这里,我们看到 classSpriteMaker
的实例被声明为 formBugs.cs 代码顶部附近的 global
变量。
classSpriteMaker cSpriteMaker = new classSpriteMaker();
经过一番努力,在目录中搜索带有精灵扩展名 *.sp3 的文件列表后,它将精灵加载到它随手可用的精灵列表中。
for (int intSpriteCounter = 0; intSpriteCounter < strFileNames.Length; intSpriteCounter++)
{
classSprite cSprite = cSpriteMaker.Load(strFileNames[intSpriteCounter]);
cSprite.UseRamCache = true;
lstSprites.Add(cSprite);
}
此时,它已经搜索了目录并将所有带有 *.sp3 扩展名的文件存储在 string
数组 strFileNames
中,其长度用于限制 for
循环。for
循环内的第一行声明了要由上面声明的 classSpriteMaker
实例加载的新精灵,并将其命名为 cSprite
。由于这些虫子每个都只有一个外观,因此 UseRAMCache
设置为 true
,我们可以让 classSprite
决定是否利用这一点,这取决于你的每个虫子的几个实例的绘制角度。当你拥有相同虫子的多个实例,但以不同大小和不同角度绘制,并在屏幕上四处游荡时,UseRAMCache
可能不会经常使用,但是当你单击选项将虫子的数量限制为每种一个时,UseRAMCache
选项将更加明显,尽管你的电脑不应该因为找到的七个或更多虫子而负担过重。在撰写本文并思考如何向他人解释时,我产生了一些想法,可以通过消除 DrawSizeFactor
和现在不必要地使 UseRAMCache
失败的 LimbImage
测试来改进所有这些。但我可能不会立即这样做,如果你喜欢,你可以尝试一下。
所以,回到这个 bugs
东西。在这里,我们看到几行代码调用了 AddBugs()
函数,该函数将该特定精灵的几个实例添加到 formCreepyCrawler
列表中
for (int intSpriteCounter = 0; intSpriteCounter < lstSprites.Count; intSpriteCounter++)
{
classSprite cSprite = lstSprites[intSpriteCounter];
intMax = cRND.getInt(3, 5);
AddBugs(intMax, ref cSprite);
}
而 AddBugs()
函数看起来像这样
void AddBugs(int intNum, ref classSprite cSprite )
{
for (int intCounter = 0; intCounter < intNum; intCounter++)
{
formCreepyCrawler frmBug = new formCreepyCrawler(ref cSprite);
lstBugs.Add(frmBug);
}
updateInfo();
}
所以,这看起来足够简单。它创建了 formCreepyCrawler
的多个实例,然后将这些实例添加到名为 lstBugs
的列表中。在此之后,formBugs.cs 在一个计时器上运行,该计时器调用其列表中的每个 bug 并告诉它 Animate()
。
所以现在,是时候看看这个 formCreepyCrawler
了。这是 bugs
在我的屏幕上四处爬行时的屏幕截图。formBugs.cs 及其显示的菜单上下文在屏幕左上方可见。你会注意到 bugs 爬过了所有其他窗体,除了它们的父窗体 formBugs.cs 覆盖了它们。切换 TopMost
功能正是如此,它切换它们是否绘制在所有其他窗体的顶部。Add
和 Sub
非常基本,而 OneEach
选项会清除你当前拥有的 bugs,并为你的观看乐趣重新生成每种 bug 一个。可爱吧?我个人喜欢蜘蛛。静止的截图无法完全展现它们的美丽。Per Pixel Alpha 的东西太棒了!
那么,当你声明 formCreepyCrawler
的实例时会发生什么呢。
public partial class formCreepyCrawler : PerPixelForm.PerPixelAlphaForm
{
classMath3.classRandom cRND = new classMath3.classRandom();
public classSpriteData cData = new classSpriteData();
ContextMenu cmuContextMenu = new ContextMenu();
MenuItem mnuQuit = new MenuItem();
MenuItem mnuAddBugs = new MenuItem();
MenuItem mnuSubBugs = new MenuItem();
MenuItem mnuToggle_TopMost = new MenuItem();
Bitmap bmpImage = null;
public static bool bolTopMost = true;
enum enuMove { turnLeft, turnRIght, Crawl, _numMoves};
enuMove eMove = enuMove.Crawl;
这里重要的部分是
classSpriteData cData = new classSpriteData();
被声明。这是 sprite
类需要的信息,用于动画和移动这个特定的 bug。正如我们之前看到的,这包含了你的生物的 Frame
索引、动画指针、大小、位置和角度。在上下文菜单的初始声明之后,它声明了我们稍后会看到的 bmpImage
值和 static
变量 bolTopMost
,formBugs
用它来告诉它们所有它们的位置。但是看看枚举类型,很简单对吧。左、右或爬行。只是基本。这个 bugs
当前的移动状态记录在变量 eMove
中,并设置为 Crawl
。
MouseWheel += FormCreepyCrawler_MouseWheel;
int intY = cRnd.getInt(0, Screen.PrimaryScreen.WorkingArea.Height);
int intX_FromEdge = cRnd.getInt(0, Screen.PrimaryScreen.WorkingArea.Width);
Location = new Point(cRnd.getInt(intX_FromEdge,
Screen.PrimaryScreen.WorkingArea.Width - intX_FromEdge),
intY);
cData.cAnimationData.dblDrawAngle = cRnd.getDouble * classMath3.TwoPi;
cData.cAnimationData.intFrameIndex = cRnd.getInt(0,
cData.cAnimationData.cAnimation.lstFrames.Count - 1);
double dblRatioScreen = 0.08 + cRND.getDouble * 0.10;
cData.cAnimationData.dblDrawSizeFactor = 1.0;
Bitmap bmpSample = cData
.cAnimationData
.cSprite.draw_Animation_Frame(ref cData);
double dblBmpSize =(double)( bmpSample.Width > bmpSample.Height ? bmpSample.Width : bmpSample.Height);
cData.cAnimationData.dblDrawSizeFactor
= (dblRatioScreen / dblBmpSize) * (double)Screen.PrimaryScreen.WorkingArea.Height ;
FormBorderStyle = FormBorderStyle.None;
TopMost = true;
ShowInTaskbar = false;
Visible = true;
cData.pt = new Point(intX_FromEdge, intY);
变量 intX
和 intY
被随机设置,以将 bug 定位在屏幕上的某个位置,我并没有过多考虑,但重要的是要注意 cData.cAnimationData.intFrameIndex
和 cData.cAnimationData.dblDrawAngle
变量在精灵被放置到屏幕上之前就已设置,以及 cData.cAnimationData.dblDrawSizeFactor
,它被设置为一个随机值,作为精灵自然图像在自然高度(设置为 1.0)和屏幕大小的函数。然后将 dblDrawSizeFactor
设置为该随机值。其余部分确保窗体没有明显的边框,以表明这个生物不仅仅是另一个窗体。这里最重要的步骤是注意它使用 cData.pt
变量在屏幕上的定位方式。这非常重要,因为当精灵将它的小爬行爪钉在屏幕上并蹒跚而行时,sprite
类会改变这个变量来移动生物。
既然我们没什么可谈的了,这里是完整的 Animate()
函数。它将 TopMost
设置为 static
变量所持有的值,然后使用模数 %
运算符循环到下一个帧索引,以确保它不会超过动画中的帧数。
然后,它会查看枚举类型变量 eMove
,然后用它来决定是向左还是向右旋转 bug 几度。此时,我们已经准备好向 sprite
请求帧的 bitmap
。下面的那一行并不那么可怕。classSpriteData
包含你要求 classSprite
给你所需图像的所有信息,因此你可以在其他地方保留指向精灵的指针,但这里就有一个。这行代码的作用是使用此 bug 的精灵实例(此处称为 cData.cAnimationData.cSprite
)。有些人可能会觉得这丑陋而笨重,他们可以随意更改 classSpriteData
以反映自己的偏好。然而,精灵实例是你所需要的,无论你如何获取它,调用 draw_Animation_Frame()
函数并引用你的 classSpriteData
和你想要的帧的所有参数,然后将它返回的位图存储在一个变量中。看一看
bmpImage = cData.cAnimationData.cSprite.draw_Animation_Frame(ref cData);
接下来,Animate()
函数使用 PerPixelAlpha
类绘制窗体。你需要阅读那篇文章才能了解它的工作原理。
public void Animate()
{
if (TopMost != bolTopMost)
TopMost = bolTopMost;
cData.cAnimationData.intFrameIndex
= (cData.cAnimationData.intFrameIndex + 1)
% cData.cAnimationData.cAnimation.lstFrames.Count;
switch(eMove)
{
case enuMove.Crawl:
break;
case enuMove.turnLeft:
cData
.cAnimationData
.dblDrawAngle = cMath3.cleanAngle(cData.cAnimationData.dblDrawAngle - dblTurnAngle);
break;
case enuMove.turnRIght:
cData
.cAnimationData
.dblDrawAngle = cMath3.cleanAngle(cData.cAnimationData.dblDrawAngle + dblTurnAngle);
break;
}
bmpImage = cData.cAnimationData.cSprite.draw_Animation_Frame(ref cData);
SetBitmap(bmpImage, (byte)255);
classAnimation_Frame cFrame = cData.cDrawData_current.cFrame;
Point ptRelLimb = (Point)cData
.cDrawData_current
.cAT_LimbPositions
.Search(cFrame.Limb_DrawRelativeTo.Name);
Location = cMath3.SubTwoPoints(cData.pt, ptRelLimb);
if (Left + bmpImage.Width < 0)
cData.pt.X += (Screen.PrimaryScreen.WorkingArea.Width + bmpImage.Width);
if (Left > Screen.PrimaryScreen.WorkingArea.Width)
cData.pt.X -= (Screen.PrimaryScreen.WorkingArea.Width + bmpImage.Width);
if (Top + bmpImage.Height < 0)
cData.pt.Y += (Screen.PrimaryScreen.WorkingArea.Height + bmpImage.Height);
if (Top > Screen.PrimaryScreen.WorkingArea.Height)
cData.pt.Y -= (Screen.PrimaryScreen.WorkingArea.Height + bmpImage.Height);
cData.cAnimationData.intFrameIndex
= (cData.cAnimationData.intFrameIndex + 1)
% cData.cAnimationData.cAnimation.lstFrames.Count;
if (cData.cAnimationData.intFrameIndex == 0)
{
intSteps--;
if (intSteps <=0)
eMove = (enuMove)cRnd.getInt(0, (int)enuMove._numMoves);
}
}
接下来是棘手的部分。我们想将 sprite
定位到它所属的位置,这需要一些额外的功夫(请原谅双关语,我脑海中有一个声音觉得这很有趣,而且确实如此),因为你的 sprite
的 DrawRelativeTo
肢体会随着它们的移动而改变,你必须将图像绘制在相对于你的精灵设计围绕其绘制的肢体的正确位置。为此,你要求你的 sprite
友好地给你该肢体相对于 bitmap
的 TopLeft
的位置。
Point ptRelLimb = (Point)cData.cDrawData_current.cAT_LimbPositions
.Search(cFrame.Limb_DrawRelativeTo.Name);
它可能看起来很复杂,但也不是太糟糕。我们访问此特定 bug 的肢体当前位置,这些位置存储在此特定 bug 的 classSpriteData
中。这里的信息只反映此 bug,不反映其他 bug。任何由同一个 sprite
绘制且具有相同外观、动画、帧、肢体等的其他 bug 都将处于不同的状态,因此它们的肢体位置也不同。所以不要依赖精灵的肢体列表来了解这个生物正在做什么。相反,我们使用 classSpriteData
来获取我们需要的信息。在这里,我们需要的是当前帧的 DrawRelativeTo
肢体的肢体位置。我们可以通过使用帧的 DrawRelativeTo
肢体的名称搜索 LimbPositions
alpha 树来获取它。我们将搜索返回的对象强制转换为我们期望的类型,在这种情况下,LimbPosition
alpha 树包含 Point
变量,因此我们将数据对象强制转换为 (Point)
类型,然后将值分配给最近声明的 ptRelLimb
点变量。有点棘手?
我们搜索了通过此 bug 实例的 classSpriteData
(名为 cData
)访问的 cAT_LimbPosition
alpha 树。存储在所有 alphaTree
中的信息、数据必须首先转换为通用数据类型 object
。因此,每当我们从 alphaTree
中检索数据时,我们都必须将数据从 object 转换为在将其放入树中之前的任何类型。此树包含 Point
变量,因此我们将树返回的内容转换为 Point
类型并将其分配给变量 ptRelLimb
。
接下来,我们做一些数学计算。记住 cData.pt
是屏幕或图片框上的位置。由于我们希望动画平稳滚动,因此我们必须将图像放置在与位图的 TopLeft
和 DrawRelativeTo
肢体之间的距离相等的位置,也就是我们刚才从 alphatree
中获取的点。由于肢体的位置是相对于我们必须在屏幕上绘制的位图的 TopLeft
向下和向右测量的正 (X,Y
) 值,因此为了确保 DrawRelativeTo
肢体出现在它所属的屏幕位置,我们必须从 cData.pt
值中减去这些 X
和 Y
值。classMath
可以通过这个简单的调用完成此操作,它将结果直接分配给窗体在屏幕上的位置。
Location = cMath3.SubTwoPoints(cData.pt, ptRelLimb);
Animate()
函数的其余部分循环遍历此精灵的一个动画帧,并选择并倒数其步数,直到达到零,然后选择一个新的动作(右
、左
或爬行
)。在关注将精灵左右、上下、内外、斜向移动以使其继续爬行之后,函数将控制权交还给调用函数,该调用函数会调用其列表中下一个 bug 的相同 Animate()
函数。