<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>游戏开发 on lanzainc.xyz</title><link>https://lanzainc.xyz/tags/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/</link><description>Recent content in 游戏开发 on lanzainc.xyz</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Tue, 02 Feb 2021 00:00:00 +0000</lastBuildDate><atom:link href="https://lanzainc.xyz/tags/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/index.xml" rel="self" type="application/rss+xml"/><item><title>制作 oblique/cabinet 投影的 2D 游戏时，确认精灵先后顺序的排序方法</title><link>https://lanzainc.xyz/tech/20210202/oblique-cabinet-2d-sprite-order/</link><pubDate>Tue, 02 Feb 2021 00:00:00 +0000</pubDate><guid>https://lanzainc.xyz/tech/20210202/oblique-cabinet-2d-sprite-order/</guid><description>&lt;p&gt;（&lt;a href="https://indienova.com/indie-game-development/oblique-cabinet-2d-sprite-order-/"&gt;元发布页&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;我经常说，我们以前玩过的很多游戏虽然看上去是2D的，但其实是3D游戏——比如热血物语、双截龙系列，它们具有一套三维的立体空间逻辑，从而不再使得游戏的舞台限制在只有左右移动和上下跳跃这两种轴向——拥有三个维度极为自由开放的移动范围相信也给很多玩家留下了深刻的印象，在&lt;a href="https://lanzainc.xyz/tech/20200305/make-a-fc-nes-collision/"&gt;上次的文章&lt;/a&gt;里，我们已经介绍了这种游戏的物理部分简单实现，本次将会聊到这类游戏在渲染上的重要技巧。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362255342An8.png"&gt;&lt;/p&gt;
&lt;p&gt;显然，在用2D精灵模拟的3D空间里，任何一个物体都不简单：如图所示的一个立方体，具有一个oblique视角(或者，我们叫cabinet视角更准确)的造型。这样的视角下，如果有另外一个精灵和它相互遮挡，应该怎么处理它们的渲染顺序关系呢？&lt;/p&gt;
&lt;p&gt;注：为了简化描述，本文章的3D空间假设所有物件的&lt;strong&gt;z坐标都相同&lt;/strong&gt;，即&amp;quot;没有和地面的高度差&amp;quot;。&lt;/p&gt;
&lt;h2 id="任意两个物体之间的排序"&gt;任意两个物体之间的排序&lt;/h2&gt;
&lt;p&gt;按照我们一般的简单思路(比如在经典的topdown视角下，如RPGMAKER系列)，谁在世界上具有较大的y值，谁就靠近屏幕，谁就应该更靠后地渲染，对吧？不过在这样的视角下，就没有那么简单了，来看一个例子：&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362255779DeL.png"&gt;&lt;/p&gt;
&lt;p&gt;如图，根据十字形标记，我们可以轻松地发现shari的y坐标小于立方体的坐标，所以她理应被挡在立方体的后面(先于立方体渲染)，对吧？可是当她来到另外一侧的时候，情况就变化了：&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362255983Qz6.png"&gt;&lt;/p&gt;
&lt;p&gt;如图，shari的y坐标仍然小于立方体的y坐标，但这个位置上，我们是希望shari遮挡立方体而不是被立方体遮挡的，所以我们需要考虑到更多的情况。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362256265Zbw.png"&gt;&lt;/p&gt;
&lt;p&gt;显然，这个世界里的任何一个物体都应该携带它的&amp;quot;&lt;strong&gt;底座&lt;/strong&gt;&amp;ldquo;信息。如图，红色的平行四边形就是上面立方体的&amp;quot;底座&amp;quot;信息，而绿色是它的高度信息(如果你需要一些更细致的排序，比如算上z轴，本文暂不讨论)有了这个信息，我们就可以确定任意两个对象之间的绘制顺序。&lt;/p&gt;
&lt;p&gt;我们把两个要排序的物体分别命名为A和B，A底座的上边缘称为aTop，下边缘称为aBottom；B底座的上边缘称为bTop，下边缘称为bBottom。&lt;/p&gt;
&lt;h3 id="abottom的y坐标小于btop的y坐标"&gt;aBottom的y坐标小于bTop的y坐标：&lt;/h3&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362256677qci.png"&gt;&lt;/p&gt;
&lt;p&gt;这时候A显然是在B的后方，也就是A先于B渲染。&lt;/p&gt;
&lt;h3 id="atop的y坐标大于bbottom的y坐标"&gt;aTop的y坐标大于bBottom的y坐标：&lt;/h3&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362256956zEh.png"&gt;&lt;/p&gt;
&lt;p&gt;这时候A显然是在B的前方，也就是B先于A渲染。&lt;/p&gt;
&lt;h3 id="其它的情况"&gt;其它的情况：&lt;/h3&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-533622575694jH.png"&gt;&lt;/p&gt;
&lt;p&gt;由于我们这里的视角下，侧面是一个45度的斜线，所以我们可以很轻松地把两个物体下边缘的y轴距离加到aBottom的右端点去进行判断，像是这样：&lt;/p&gt;
&lt;p&gt;distanceY = bBottom.y - aBottom.y&lt;/p&gt;
&lt;p&gt;(我们用left和right表示一个线段的&lt;strong&gt;左端点&lt;/strong&gt;和&lt;strong&gt;右端点&lt;/strong&gt;)&lt;/p&gt;
&lt;p&gt;aBottom.left.x &amp;gt; bBottom.right.x + distanceY&lt;/p&gt;
&lt;p&gt;如果这个判断是成立的，那就是上图所述的情况：A在B的前方，也就是B先于A渲染。&lt;/p&gt;
&lt;p&gt;否则就是下图所述的情况：A在B的后方，也就是A先于B渲染。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362257077JJW.png"&gt;&lt;/p&gt;
&lt;h2 id="对场景内所有物体的排序"&gt;对场景内所有物体的排序&lt;/h2&gt;
&lt;p&gt;好，至此我们已经可以为场景内任意的两个对象进行排序了，那么，我们也可以为场景中所有的对象组织排序。很多人的第一反应是，直接用冒泡/选择/归并/快排这样的算法来对场景中需要排序的物体进行排序不就好了吗？其实这是不合适的，因为场景中的物体遮挡存在拓扑关系，如果直接使用线性表的排序方法，可能在swap一对物体以确保他们遮挡关系的同时又会直接破坏了另一对物体的遮挡关系，因此，我们的第一步是对场景中需要排序的物体进行连线。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362258891Orn.png"&gt;&lt;/p&gt;
&lt;p&gt;带有弧线的那一端是射线的末端，末端的物体比首端的物体更先渲染(位置更靠后)。&lt;/p&gt;
&lt;p&gt;这里说明一下，我们只对&lt;strong&gt;需要排序的一对物体&lt;/strong&gt;进行连线(出于性能考虑)，所以你的显示对象可能需要有相应的bounding_box或是什么的。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362258896iL4.png"&gt;&lt;/p&gt;
&lt;p&gt;当场景中的物体足够多，遮挡关系足够复杂时，不难发现，我们的物体遮挡关系实际上构建出了一个&lt;strong&gt;有向图&lt;/strong&gt;。而有向图可以通过拓扑排序来获取一个不唯一的线性序列，照着这个线性序列进行渲染就可以获得正确的场景排序了。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="../u-53362258871vSu.gif"&gt;&lt;/p&gt;
&lt;p&gt;(图中蓝绿色的矩形是判断精灵相交/重叠用的bounding_box，DRAWIDX则是拓扑排序后得到的渲染顺序值)&lt;/p&gt;
&lt;p&gt;这样，就得到了整个场景的正确呈现顺序。&lt;/p&gt;
&lt;h2 id="最后一点问题"&gt;最后一点问题&lt;/h2&gt;
&lt;p&gt;相信我们都看到过一些&amp;quot;视觉错觉&amp;quot;的趣味图片，对吧？譬如三个无限循环的台阶，相互遮挡的三个棱柱……之类的，很有意思，不过在游戏开发当中可一点也不有趣。很显然，当游戏场景中出现了几个互相存在遮挡和被遮挡关系的对象时，我们构造出的有向图就&lt;strong&gt;成环&lt;/strong&gt;了。而成环的有向图是不能进行拓扑排序的，这就导致我们没有办法去按正确顺序呈现画面。解决这个问题的最好方法就是把容易引起该问题的那个物体分割为两个物体，使得其中的一个部分&amp;quot;专门遮挡&amp;quot;而另一个部分&amp;quot;专门被遮挡&amp;rdquo;，这样就可以消除我们构造的有向图中的环。&lt;/p&gt;
&lt;p&gt;本次的讨论就到这里，祝大家开发顺利，新年快乐！&lt;/p&gt;</description></item><item><title>实现一个类似于 FC 时代简易的碰撞系统</title><link>https://lanzainc.xyz/tech/20200305/make-a-fc-nes-collision/</link><pubDate>Thu, 05 Mar 2020 00:00:00 +0000</pubDate><guid>https://lanzainc.xyz/tech/20200305/make-a-fc-nes-collision/</guid><description>&lt;p&gt;（&lt;a href="https://indienova.com/indie-game-development/make-a-fc-nes-collision/"&gt;元发布页&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;注：本文章采用Y轴向下的屏幕直角坐标系&lt;/p&gt;
&lt;h2 id="3d游戏"&gt;&amp;ldquo;3D&amp;quot;游戏&lt;/h2&gt;
&lt;p&gt;我们玩到一些游戏时，往往按照画面表现给出一种分类，讲它是2D或是3D的电子游戏。&lt;/p&gt;
&lt;p&gt;实际上我们不仅可以从画面来分，更可以从游戏当中呈现给我们的活动范围来划分，比如经典的：FC《双截龙》系列。&lt;/p&gt;
&lt;p&gt;&lt;img alt="&amp;ldquo;3D&amp;quot;游戏《双截龙》" loading="lazy" src="../u-53363337783uXz.gif"&gt;&lt;/p&gt;
&lt;p&gt;我们可以发现，游戏对象有纵向移动、横向移动、还可以在高度轴上发生移动，&lt;/p&gt;
&lt;p&gt;实际上，这往往已经可以说明它是一个3D游戏了。&lt;/p&gt;
&lt;h2 id="物理的世界"&gt;物理的世界&lt;/h2&gt;
&lt;p&gt;我们知道现在有很多物理引擎——2D的、3D的都有，这些物理引擎能够制作出像《愤怒的小鸟》这样的具有典型的物理感、机关感的游戏。其实我们自己玩的时候会有一些&amp;quot;感觉&amp;quot;的，这些用专业物理引擎制作出的游戏物体运动和我们以前接触到的一些古典的游戏是有很大不同。这就是我们要去研究的点——在不使用专业物理引擎的情况下，怎么去避开这些麻烦的物理拟真，去做一个简单的、老式的具有运动物体和活动范围的空间呢？&lt;/p&gt;
&lt;h2 id="物体"&gt;物体&lt;/h2&gt;
&lt;p&gt;首先我们可以去设计一个物件，用它去表示物理世界内的一个可运动的物体。一般而言，在一开始设计物体时，你最先给出它的两个关键元素：坐标和速度。&lt;/p&gt;
&lt;p&gt;如果你的编程环境中有基本的一些数学工具(比如Vector3这样的东西)，做起来就会比较快，如果没有的话，可以自己简单地写一个——搬中学数学课本。&lt;/p&gt;
&lt;p&gt;物体的坐标用于直接表示它在这个物理世界当中所处的位置，而速度则指示该物体将要如何去变化它的位置——你在游戏循环中需要不断根据速度来修正其位置，这样就实现了物体的运动。&lt;/p&gt;
&lt;p&gt;当然，你还需要给物体施加一个重力(gravity)，让它无条件地产生某种速度变化。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 可以先这样写一个简单的实现，之后我们再修改&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;update&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gravity &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Vector3&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;new(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;speed &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; gravity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;position &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;speed
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样的示例代码就表示了一个简单的物体每帧受到在z轴为-0.1个单位的重力的运动模拟的过程。&lt;/p&gt;
&lt;p&gt;物体每帧受到z轴负向的重力就会产生自然下落，如果你要给物体一个z轴正向的速度，那么物体就会出现起跳~下坠，这样的一个很自然的过程。&lt;/p&gt;
&lt;p&gt;&lt;img alt="纯Z轴移动" loading="lazy" src="../u-53363339218spV.gif"&gt;&lt;/p&gt;
&lt;p&gt;通常你绘制一个2D物体时，直接将物体的xy坐标拿来加上摄像机坐标就可以拿去直接绘制精灵了——在绘制3D物体时，你需要体现第三轴向的变化。&lt;/p&gt;
&lt;p&gt;屏幕是一个二维空间，体现第三轴向的变化就需要借助xy这两个轴向才行，像是我举例的《双截龙》截图当中就是借助y轴体现z轴的变化的(跳跃只产生了z轴的变化，但是绘制时反映到了y轴上，你可以简单地认为绘制坐标 = 角色Y - 角色Z)。&lt;/p&gt;
&lt;p&gt;那么问题来了，y轴和z轴在绘制上合并了，那么如果物体同时在y轴和z轴发生移动的话，就会造成玩家的困惑，比如下图：&lt;/p&gt;
&lt;p&gt;&lt;img alt="Shari在跳跃的同时还在上下移动" loading="lazy" src="../u-53363340015kdd.gif"&gt;&lt;/p&gt;
&lt;p&gt;Shari在跳跃的同时还进行了上下移动，这在战斗场景等需要玩家判断位置的场合是容易带来困扰的，那么怎么解决呢？也很简单，加个影子就可以了：&lt;/p&gt;
&lt;p&gt;&lt;img alt="有阴影指示Y轴坐标" loading="lazy" src="../u-53363340329oOL.gif"&gt;&lt;/p&gt;
&lt;p&gt;有一个可以指示物体Y轴位置、且大小和浓度受高度影响的阴影，就可以更快使人透过屏幕而辨识出物体在空间当中的位置。&lt;/p&gt;
&lt;h2 id="舞台是一个大台阶"&gt;舞台是一个大台阶&lt;/h2&gt;
&lt;p&gt;刚才我们的示例当中已经做出了一个可以运动的物体，那么，现在我们需要给物体一个活动范围的限制，一般来说，也就是碰撞相关的内容了。&lt;/p&gt;
&lt;p&gt;像我们上面的代码，给出一个gravity，然后让速度不断添加，坐标也不断变化……那么我们看到的就是一个物体无限地朝着z轴负方向掉了下去。&lt;/p&gt;
&lt;p&gt;实际上这个过程我们是可以去施加一个限度的，你可以想象为设置一个&amp;quot;地板&amp;rdquo;，物体的z坐标到了这个地板就到底了，不能再降低了。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;height&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;update&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gravity &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Vector3&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;new(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;speed &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; gravity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;position &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;speed
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;position&lt;span style="color:#f92672"&gt;.&lt;/span&gt;z &lt;span style="color:#f92672"&gt;=&lt;/span&gt; height &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;position&lt;span style="color:#f92672"&gt;.&lt;/span&gt;z &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; height
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以此代码为例，我们写了一个height方法来获取高度，这个高度我们先写死为0——这样一来，物体的z坐标到0这个值就不会再降低了。&lt;/p&gt;
&lt;p&gt;当然，这样的话世界就变成了一个超大的平地，因此我们可以做点变化……&lt;/p&gt;
&lt;p&gt;def height(x, y)
return 32 if x &amp;gt; 100
return 0
end&lt;/p&gt;</description></item></channel></rss>