65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.80/5 (4投票s)

2019年1月23日

CPOL

28分钟阅读

viewsIcon

5451

downloadIcon

607

通过一个例子来使用你的精灵,这将给有“bug”的代码赋予新的含义

引言

那么,你有机会看到一些 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 个以上的分支节点,为字母表的每个字母使用不同的分支。它们非常快,可以容纳任何转换为通用 VisualStudio object 类的各种数据。只要记住你在哪棵树中存储了什么,将树中的所有对象保持相同的数据类型,我稍后会再次提到的 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:用于定位从肢相对于铰链及其主肢的弧度坐标的幅值乘数。最后两个变量(AngleSlaveContact_MagnitudeFactor)在每个动画的每一帧中设置,让开发人员可以比早期版本的 Sprite Editor 更灵活地定位和摆放精灵。

这里显示的字段大部分是上面列出的属性来源的重复,除了

  • Contact:包含弧度坐标 MasterSlave,它们将铰链定位相对于从肢和主肢,因此从肢相对于其主肢。这些弧度坐标与上面提到的 AngleSlaveContact_MagnitudeFactor 不同。这些是使用 SpriteEditor2017 的蓝色 HingeContact_Graphical 面板设置的值,而 AngleSlaveContact_MagnitudeFactor 是可以使用橙色 FrameEditor 面板更改的值。Contact 值是半永久性的,也就是说,它们可以在 SpriteEditor 中更改,但是一旦精灵保存到文件并准备好在应用程序中使用,就没有方便的方法在运行时更改这些值。然而,AngleSlaveContact_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,与我之前提到的类完全相同。它们易于使用,可靠且多功能。这个树保存你精灵的所有动画,因此如果你没有方便的枚举类型告诉你使用哪个索引来访问给定的动画,你可以通过名称来请求它。

    第一个示例来自 classSpriteMakerLoad() 函数,该函数从文件中加载动画,然后通过将 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。它没有主肢体,也没有铰链。因此,在整个 classSpriteSpriteEditor 中都必须进行测试,以区分普通肢体和 MasterLimb,因为对肢体进行的工作有一半涉及引用它们的铰链和主肢体,而 cLimb_Master 对此感到不满。
  • lstLimbSlavesGetRetVal - 是一个内部使用的列表,由一个递归函数使用,该函数查找给定肢体的所有从属肢体。
  • semDrawAnimation_Frame - 因为精灵使用 classSpriteData,它本质上是一个可移除的数据盒,你可以插入其中,告诉精灵要运行哪个动画和帧。需要“数据盒”是因为精灵在屏幕上的位置会随着精灵类根据给定肢体绘制而改变,并且从一个肢体到另一个肢体的过渡需要记住精灵上次绘制时的位置。当你只绘制给定精灵的一个实例时,这都很好,但是当你在屏幕上有十二个或二十个相同的精灵,穿着不同的服装并执行相同的动画时(在下一篇文章中,我将向你展示我的体操运动员克隆机,它使用肢体和动画列表相同的数十个精灵进行动画,但每个精灵都穿着不同的“制服”,并且必须拥有自己的 classSpriteData 来记住她在你的桌面舞台上侧身移动和翻筋斗时的位置。所以,言归正传,需要一个信号量来确保精灵类实例正在使用正确的数据,并且这些正确的数据不会被其他穿着粉色或蓝色紧身衣的跳舞体操运动员损坏。
  • AnimationAlphabetizeDeleteNew - 不言自明 - 没有惊喜
  • LimbNewDelete - 也相当简单明了
  • 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 类除了作为一个列表帧的容器,通过 AddSubCapture 函数进行管理之外,实际上并没有做太多事情。

  • cSprite - 只是一个指向其所属精灵的指针
  • lstFrames - 它的主要内容,就是它的全部作用。一个动画本质上真的只是一个帧的集合。
  • Name - 呃...我是睡着了吗...

这里的方法也没有多少...真正核心的是 Frames 类。

  • AddCaptureSub - 这里没有什么特别的。
  • DrawList_Add - 将新创建的肢体添加到其帧列表的每个帧中

差不多就是这样了。

所以,这里是精灵绘制你想要的图像所需知道的一切。它一点也不复杂。请记住,每个肢体都需要像设计者使用 SpriteEditor2017 IDE 定位一样在屏幕上定位。为此,它必须知道所有东西的位置,它通过记住每个肢体的角度和 SlaveMagnitudeFactor 来做到这一点。“但是等等,”我能听到房间角落里一些更敏锐的头脑的热情灵魂说,上面图像提供的列表中既没有 Angle 也没有 SlaveMagnitudeFactor 字样,你是对的,因为这两个非常重要的变量包含在 classAnimation_Frame_LimbData 中,我很高兴稍后和你一起讨论。我们快到了。

  • Limb_DrawRelativeTo - 是一个指向其他所有肢体都要围绕其绘制的肢体的指针。但是当精灵生成一个 LimbDrawRelativeTo 不是 MasterLimb 的帧时,实际上内部发生的事情与其他任何帧都相同。图像的绘制方式完全相同,唯一的区别是信息打包的方式。Limbs 本身会告诉你它们相对于 MasterLimb 的位置,但是 classSpriteDatacDrawData_Current 会告诉你每个肢体相对于帧位图的左上角的位置。既然你知道你在屏幕上放置位图的位置,这可能对你来说更容易处理,并且 classSprite 肢体的值会随着下一次调用 draw_Animation_Frame() 的下一帧而改变,这可能与你正在处理的值相同,也可能不同,具体取决于你的动画角色中有多少个使用相同的精灵。
  • HorizontalVertical Mirrors - 是布尔变量,用于确定你获得的图像是否翻转,以及 classSpriteData 的肢体位置是否反映其新外观
  • cCache 是一个类,它跟踪此帧上次绘制时的参数和位图,使你可以通过直接从缓存中获取内存中已有的副本,而不是重新绘制所有内容来加快速度
  • ptTLptBR - 是与 MasterLimb 的距离,两者相减即可得到输出位图的大小
  • lstData - 是一个 classAnimation_Frame_LimbData 类型的列表,它告诉帧以什么角度绘制每个肢体,相对于其 masterLimb 移动多远,以及绘制肢体的 LimbImage 的索引。分别对应:AngleSlaveMultiplicationFactorLimb,如下图所示。我唯一遗漏的是,有肢体 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 肢体,它们有三种状态:TurnLeftTurnRightCrawl,但所有这三种状态都使用相同的 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 功能正是如此,它切换它们是否绘制在所有其他窗体的顶部。AddSub 非常基本,而 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 变量 bolTopMostformBugs 用它来告诉它们所有它们的位置。但是看看枚举类型,很简单对吧。左、右或爬行。只是基本。这个 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);

变量 intXintY 被随机设置,以将 bug 定位在屏幕上的某个位置,我并没有过多考虑,但重要的是要注意 cData.cAnimationData.intFrameIndexcData.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 定位到它所属的位置,这需要一些额外的功夫(请原谅双关语,我脑海中有一个声音觉得这很有趣,而且确实如此),因为你的 spriteDrawRelativeTo 肢体会随着它们的移动而改变,你必须将图像绘制在相对于你的精灵设计围绕其绘制的肢体的正确位置。为此,你要求你的 sprite 友好地给你该肢体相对于 bitmapTopLeft 的位置。

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 是屏幕或图片框上的位置。由于我们希望动画平稳滚动,因此我们必须将图像放置在与位图的 TopLeftDrawRelativeTo 肢体之间的距离相等的位置,也就是我们刚才从 alphatree 中获取的点。由于肢体的位置是相对于我们必须在屏幕上绘制的位图的 TopLeft 向下和向右测量的正 (X,Y) 值,因此为了确保 DrawRelativeTo 肢体出现在它所属的屏幕位置,我们必须从 cData.pt 值中减去这些 XY 值。classMath 可以通过这个简单的调用完成此操作,它将结果直接分配给窗体在屏幕上的位置。

 Location = cMath3.SubTwoPoints(cData.pt, ptRelLimb);

Animate() 函数的其余部分循环遍历此精灵的一个动画帧,并选择并倒数其步数,直到达到零,然后选择一个新的动作(爬行)。在关注将精灵左右、上下、内外、斜向移动以使其继续爬行之后,函数将控制权交还给调用函数,该调用函数会调用其列表中下一个 bug 的相同 Animate() 函数。

© . All rights reserved.