ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Godot 2D俯视角生存射击:用Y-Sort与光照实现伪3D视觉层次

Godot 2D俯视角生存射击:用Y-Sort与光照实现伪3D视觉层次 1. 为什么第15篇要碰伪3D视觉俯视角生存射击的关卡感从哪来做2D俯视角生存射击做到第15篇其实已经到了一个很微妙的节点。基础操作、角色移动、子弹碰撞、敌人追踪这些东西前十几篇里该写的都写了大多数照着做的读者都能跑起来一个“能玩”的Demo。但这个阶段最容易出现的尴尬是游戏确实能玩但画面看起来特别“平”像一张纸在桌面上滑来滑去。敌人、角色、树、石头全部贴在一个平面上完全没有层次玩久了眼睛累也很难产生“沉浸感”。这一篇我决定把伪3D视觉和地形层叠这件事讲透。你没看错2D游戏一样可以做“深度感”而且不用引入任何3D模型。Godot的2D引擎里有一套非常适合俯视角生存射击的体系核心思路是用高度分层 阴影偏移 遮挡关系模拟来制造“2.5D”的错觉让玩家觉得角色真的在场景里跑而不是在贴图上跑。这个思路也是最近社区里讨论热度很高的“2D图像堆叠为3D图像”的实用落地版不涉及复杂的数学重建只需要美术素材配合节点结构就能出效果。这套东西适合什么人如果你正在做2D俯视角生存射击、ARPG、或者任何带有场景纵深感的2D游戏这章内容能直接解决“场景平、层次乱、遮挡错”这三个老大难。跟着做完你的游戏会立刻从“学生作业感”变成“有商业Demo内味儿”。2. Y-Sort节点排序2D场景立体感的基石2.1 Y-Sort到底是什么为什么俯视角必须用它先从一个很多新手理解偏了的概念说起Godot 2D场景里的绘制顺序。默认情况下Godot的CanvasItem按树结构顺序绘制先画的在底层后画的在顶层。很多新手做俯视角游戏时为了让人物显示在树后面就把树的Z Index调成负数或者把人物的Z Index调成正数。结果就是人站在树底下时人被树盖住没问题但人站在树上方时树依然盖着人穿帮了。俯视角游戏里物体遮挡关系应该由“谁离摄像机近”决定。用Godot的行话说就是Y值越大屏幕越靠下的物体越应该绘制在前面。Y-Sort就是干这个的它让同一个排序层里的所有子节点按照全局Y坐标动态调整绘制顺序不需要你手动改Z Index。在项目设置里有个rendering - 2d - y_sort_enabled的全局开关但这个开关默认关着我建议一直开着然后每个具体场景用YSort节点精确控制。实际操作新建一个Node2D名字叫YSortRoot在检查器里勾选Y Sort Enabled。然后把这个节点放在场景根节点下面角色、敌人、树干、石头、掉落物全部挂到这个节点下。我前几篇写的敌人和子弹都是直接挂在根节点下的这一步需要统一调整不然排序会乱。2.2 一个最容易忽略的坑Sprite的偏移原点Y-Sort的排序基准是节点的全局位置不是Sprite贴图的中心点。这就牵扯到一个很多人踩过的坑贴图的“脚底”必须对准节点原点。举个具体例子。一棵树的贴图是128x128像素树根在贴图的最底边中间也就是(64, 128)这个像素位置。如果你把Sprite的Offset设为(64, 128)那树的节点原点就在树根处角色走到树根Y坐标下方时才会被树遮挡走到树根上方时角色遮挡树符合真实视觉。如果你忘了设Offset节点原点在贴图中心那树根的实际Y坐标比节点原点大了64像素。角色明明站在树根上方视觉上应该被树盖住但因为角色Y坐标比节点原点小排序结果却是角色盖住树穿帮非常明显。我的习惯是在美术资源导入时就定好规范所有角色和场景物体贴图的“脚底中心点”统一在资源命名时标注导入后给Sprite设置统一的Anchor和Offset。像树木、石头这种垂直物体Offset设为(贴图宽度/2, 贴图高度)角色这种有行走动画的Offset设为(贴图宽度/2, 贴图高度 - 2像素)留一点贴地余量防止脚底和地面之间有缝隙。还有就是TextureFilter建议统一设为Nearest或Linear。像素风素材用Nearest高清手绘素材用Linear但一定要全场景统一混用会出现边缘模糊或像素颗粒粗细不一的问题观感很掉价。2.3 分层级联排序大型场景性能与效果的折中方案随着游戏规模变大一个Y-Sort节点管几十个物体没问题但管几百个物体时Godot的CanvasItem排序计算量会上升尤其在低端手机上可能掉帧。这时候要做分区Y-Sort。思路是把大地图切成区域块每个区域块一个YSort节点块与块之间通过ZIndex分层。比如整个地图横向切成3块块1的ZIndex为0块2为1块3为2。块内依然用Y-Sort精确排序块间用固定层级叠放。因为玩家同时能看到的区域有限相邻两个块的边缘会偶尔出现排序瑕疵但在俯视角生存射击这种快节奏游戏里玩家注意力在怪物身上瑕疵很难被注意到性能收益却很明显。我在自己的项目里用了一个更简单的方案把地图按房间/区域划分玩家进入哪个区域哪个区域的YSort节点才启用ProcessMode为WhenInView或者手动开关。配合Godot 4的VisibleOnScreenNotifier2D离开屏幕的敌人和掉落物直接暂停逻辑CPU占用降了一个量级。生存射击游戏后期敌人数量一多这种优化早晚要做不如从第15篇就开始养成习惯。3. 光照与阴影2D画面“活”起来的核心武器3.1 2D光照模式选择依然是Compatibility渲染器最省心很多用Godot 4的读者问过我一个问题2D游戏该用哪个渲染器我的答案很直接没有特殊需求就用Compatibility兼容渲染器也就是项目设置里rendering - renderer/rendering_method设为gl_compatibility。Forward Plus前向渲染在2D场景下唯一明显的优势是2D光照支持更精细的Normal Map效果但代价是光照计算开销更大而且对低端设备不友好。Compatibility渲染器在Godot 4.2之后对2D光照的支持已经非常完善PointLight2D、DirectionalLight2D、阴影、发光遮罩都能用绝大多数俯视角生存射击游戏完全够用。光照和Y-Sort结合时要特别注意一个性质光照会计算所有受光物体的轮廓物体的绘制顺序影响阴影形状。在Y-Sort排序正确的前提下光影关系基本是真实的——树在角色上方树阴影盖住角色角色身上会有一层树的阴影这个效果非常赞能瞬间拉开与“无光照2D游戏”的差距。不过要注意光照的Shadow属性不要全局都开。我把树、石头、建筑物这些静态物体手动标记到LightOccluder2D上开阴影角色和敌人这类动态物体不开只接收阴影。原因很简单动态物体的阴影每帧都要重新计算OccluderPolygon在敌人数量二三十个时每帧多几百次多边形计算手机端立刻卡顿。3.2 阴影偏移参数的实操调校光照阴影做出来之后一定会遇到一个经典问题阴影和物体“粘在一起”或者阴影歪得离谱。这跟Shadow Filter和Shadow Smoothing有关但更关键的其实是PointLight2D的Shadow Offset或者光照角度。很多新手把光源放在物体正上方阴影会缩在物体正下方几乎看不见。俯视角游戏里光应该从“斜上方”打下来比如把DirectionalLight2D的旋转设为45度阴影就能拉出一个合理的长度。如果你用的是PointLight2D比如玩家手电筒效果更稳妥的做法是给光源加一个轻微的Offset或者在光源属性里调Shadow Smoothing到 2~3 像素。这个数值太大阴影会糊成一片太小边缘锯齿明显。我的调校顺序是先保证Y-Sort正确再开光照再看阴影方向最后调阴影过滤。不要一开始就沉迷阴影参数否则你会被一堆叠加的视觉问题搞到怀疑人生。4. 地形堆叠与TileMap层级从“纸片地面”到“立体场景”的关键4.1 多层TileMap的层级设计俯视角生存射击的场景我建议拆成三到四层TileMap来搭地面层Base草地、泥地、沙地用于大面积铺底。这一层不参与Y-SortZIndex为0。地面装饰层GroundDetail小石子、裂缝、脚印、落叶。这一层可以参与Y-Sort让角色踩在碎石上时能产生“踩在石头上”的遮挡关系但注意装饰物的Y偏移不能太大否则角色会像“漂浮”在装饰物上方。这一层ZIndex为0挂在Y-SortRoot下。物体层Object树、房子、栅栏、宝箱。这一层必须参与Y-Sort所有物体脚底对准原点。ZIndex为1。屋顶/遮挡层Ceiling树冠、屋顶、洞穴顶。这一层比较特殊我通常用一种“区域遮挡”的方式实现不直接在这个层放整棵树的素材而是给每棵树建一个Area2D当玩家进入树冠范围时把树冠的ShowBehindParent属性设为开启让树冠绘制在玩家后面——不对说反了正确做法是树冠绘制在玩家前面也就是玩家被树冠盖住。这里有一个社区里讨论比较多的方案树冠会随着玩家进入范围“变透明”或者切换成半透明版本。我做下来的体会是全透明会丢失层次感半透明Modulate的Alpha设为0.5到0.7加上轻微颜色偏移比如让树冠颜色暗一点效果比单纯透明要好很多。4.2 TileSet的物理碰撞与Y-Sort兼容性TileMap的物理碰撞默认用的是TileShape也就是一方块。这在Y-Sort场景里有个很尴尬的问题树根部的碰撞方块是架空的角色走过去明明能碰到树干但因为Y-Sort排序视觉上角色被树干盖住物理上却没碰到任何东西。我的解决办法是给树木这种物体单独用StaticBody2DCollisionPolygon2D不用TileMap的碰撞。TileMap只负责画地面和地面装饰所有需要Y-Sort遮挡交互的物体都用独立节点。这样物理和视觉可以分开控制视觉上树冠遮人物理上碰撞精确到树干两不误。如果你确实需要TileMap参与遮挡比如一整面墙延伸出去可以在TileSet里给墙Tile设置YSortEnabled和自定义Origin墙根对齐到Tile原点。但实测下来这种方案对美术资源规范化要求很高一个Tile的偏移不对整面墙的排序都错。我建议新手还是独立节点为主。4.3 地形生成思路从手摆到半自动生存射击游戏的地图通常比较大全手摆Tile效率太低。我目前用的一套半自动流程是先用FastNoiseLite生成噪声图根据噪声阈值决定地面Tile的类型草地、泥水、沙地然后用一个自定义栅格脚本在特定Tile位置上自动放置石头和树木资源节点。噪声值作为“概率权重”比如某格噪声值高放树的概率就大。为了让树不生成在玩家出生点和路径上还要加一个距离检测生成点与玩家出生点的距离要大于某个值我设的是120像素否则跳过。这套流程写在_ready()里直接执行就行不需要用TileMap的自动铺设因为树叶要生成的是带碰撞和光照遮罩的独立场景实例。实测下来200x200格的地图生成三千棵树的场景实例启动时间大概1到2秒完全可接受。5. 敌人掉落物与Z轴假象2D游戏中的“高度”欺骗术5.1 掉落物弹跳的伪Z轴实现俯视角生存射击免不了掉落物金币、血包、弹药如果只是平铺在地图上没有任何立体感捡起来很干瘪。这里我用了一个“伪高度”技巧本质上就是热词里的“2D图像堆叠为3D图像”的简化应用——不改变实际坐标只改变视觉表现。用Tween实现掉落物“弹跳着落”的过程func spawn_drop(pos: Vector2): var coin coin_scene.instantiate() coin.position pos add_child(coin) var original_y pos.y var tween coin.create_tween() # 先快速弹起到高处 tween.tween_property(coin, position:y, original_y - 24, 0.15) \ .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) # 再落回地面 tween.tween_property(coin, position:y, original_y, 0.3) \ .set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT) # 同时缩放模拟远近效果 tween.parallel().tween_property(coin, scale, Vector2(1.0, 1.0), 0.3) \ .from(Vector2(0.6, 0.6))实质就是Y坐标临时向上偏移24像素模拟“跳起来”然后弹回原位。因为Y-Sort是实时根据Y坐标排序的掉落物弹到空中的那一瞬间它在排序里会比树木的根部更靠上视觉上就像真的从空中落下来砸在树根旁边。这个效果成本极低观感提升非常明显。5.2 角色受击闪白与阴影动态受击闪白是生存射击游戏必须有的反馈Godot里直接调Modulate或SelfModulate就行。但我要提一个很多教程不会讲到的细节闪白时要同步调整影子透明度不然角色会像“飘起来”。阴影如果是个独立的Sprite2D黑色半透明的圆形贴图角色闪白瞬间影子不该跟着闪白反而应该保持暗色。如果整个角色节点都闪白影子也闪白了角色瞬间失去地面联系感被击中反馈会很怪异。我的做法是把影子单独放在角色节点下的第一个子节点用LightOccluder2D不需要管阴影本身的颜色但要确保影子的Modulate不被受击闪白影响。所以受击闪白时我只修改角色的主体Sprite和AnimationPlayer里的材质闪白不动影子节点。5.3 尸体与销毁的排序处理敌人死亡后尸体留在原地也会参与Y-Sort。这里有个性能陷阱尸体的Y-Sort计算不会因为死亡而停止几百具尸体摆在场上每帧都在参与排序CPU白烧。我的处理是敌人死亡后把尸体节点从YSortRoot移到场景根节点然后给它一个固定的ZIndex比如 -1让所有尸体统一绘制在地面层之下。这样视觉上像尸体陷进地面但玩家根本不会注意到——注意力在战斗上。或者更干脆一点设置一个尸体淡出计时器5秒后自动queue_free()。生存射击游戏里敌人是无限刷的尸体不清理内存迟早爆。6. 俯视角射击的核心手感瞄准、后坐力与命中反馈6.1 鼠标瞄准与射击方向的一致性这是生存射击游戏体验的分水岭。前几篇里的演示项目子弹是从角色中心发射的鼠标在屏幕哪个方向子弹就往哪飞。这样确实能玩但感觉像“发射器长在胸口”完全不真实。正确的做法是让武器从角色位置生成子弹射击方向是“鼠标世界坐标”与“角色世界坐标”的差向量归一化。同时枪口位置要偏移到角色朝向的一侧让子弹从枪口喷出而不是从角色中心喷出。func shoot(): var mouse_world get_global_mouse_position() var dir (mouse_world - global_position).normalized() var gun_tip global_position dir * 18 var bullet bullet_scene.instantiate() bullet.position gun_tip bullet.velocity dir * bullet_speed bullet.rotation dir.angle() get_parent().add_child(bullet)这个18像素的枪口偏移数值虽小手感提升却极大。子弹从角色身体的侧面飞出去而不是从“胸口”飞出去玩家会觉得武器是真的握在手里。6.2 后坐力与准星扩散的实现生存射击的“生存感”很大程度来自枪械的不稳定感。完全没有后坐力枪就是激光笔爽快感不足后坐力太大又会让瞄准变得痛苦把休闲玩家劝退。我的方案是给枪械加一个“后坐力偏移角”射击时准星在屏幕上的位置不直接等于鼠标位置而是鼠标位置加上一个随机噪声var recoil recoil_amount # 根据武器类型设置手枪0.02步枪0.06 var noise Vector2( randf_range(-recoil, recoil) * 120, randf_range(-recoil, recoil) * 120 ) var target get_global_mouse_position() noise var dir (target - global_position).normalized()注意这里的噪声用的是屏幕偏移量不是角度偏移量。角度偏移在近距离游戏里会产生“瞄准方向飘掉”的感觉而屏幕偏移更接近真实FPS里的准星扩散。每次射击后后坐力值按固定速率回落玩家松开鼠标一段时间后准星自动恢复这个恢复速度就是手感的关键。我调过很多次最终参数是恢复速度为每秒衰减50%的当前后坐力值手感很顺滑。6.3 命中反馈的三件套生存射击游戏的爽感一半来自命中反馈。我认为三件套缺一不可受击闪白敌人被子弹击中瞬间把SelfModulate从白色闪到红色再闪回来持续时间0.1秒。这个反馈比单纯扣血数字直观得多。伤害飘字用LabelTween在敌人头顶往上飘字从白色渐变为黄色再变为红色随伤害数值增大而变色。飘字不参与Y-Sort直接挂在CanvasLayer上避免被场景遮挡。屏幕微震命中敌人时摄像机做一次4像素幅度的快速震动50毫秒内恢复。这个震动幅度不能大大了会让玩家头晕小了没有打击感。我习惯用Camera2D的offset配合Tween来回弹两次而不是持续抖动。三个反馈全部触发时玩家的每一次命中都能获得清晰、即时、多感官的正向反馈这是“手感好”这个词背后真正可量化的东西。7. 常见问题与排查技巧实录7.1 Y-Sort排序错乱角色被不该盖住的东西盖住先检查YSort节点是否勾选了Y Sort Enabled再检查所有参与排序的节点是否都挂在这个YSort节点之下。如果都确认了看每个节点的ZIndexY-Sort只会影响同一个ZIndex层级内的节点顺序ZIndex不同就直接按ZIndex分层了。我遇到过有人给树木的ZIndex设成1角色的ZIndex设成0结果无论怎么调Y-Sort角色永远在树后面——这就是Y-Sort和ZIndex混用的典型错误。7.2 光照开了但阴影不显示可能性很多我按排查顺序列一下光源的Shadow Enabled没开。被遮挡物没有加LightOccluder2D或者Occluder的OccluderPolygon2D是空的。光源的Range太小阴影超出了光照范围。渲染器选了Forward Plus但没开2D阴影的额外设置。最重要的检查光源节点和遮挡物是否在同一个CanvasLayer内。跨层光照和阴影在Godot 4里经常失效我建议所有光照相关节点都挂在同一个CanvasLayer下。7.3 子弹射击有延迟感先用print()检查子弹生成的时间点。注意queue_free()是延迟到帧末才执行的如果你在子弹碰撞后马上又准备生成下一发子弹可能会因为前一帧的清理还没完成而出现短暂卡顿。另外get_global_mouse_position()是每帧读取如果在_physics_process里读取鼠标位置可能会读到上一帧的缓冲值。射击类操作我放在_process处理物理碰撞放_physics_process两套循环各司其职。还有一个容易被忽略的点子弹的CollisionShape2D如果用默认的圆形在高速移动下可能“穿越”薄碰撞体。Godot 4的物理引擎对连续碰撞检测支持有限最简单的解决方式是增大子弹碰撞体积并把子弹移动方式改为每帧做一次射线检测intersect_ray而不是移动碰撞体这个改动在子弹速度超过800像素/秒时几乎是必须的。7.4 敌人多了之后帧率断崖下跌优先检查敌人每帧是否有不必要的节点操作。最常见的是每个敌人都在_process里做get_node()查找一次两次还好几十个敌人叠加起来就是灾难。正确做法是在敌人初始化时把目标节点onready缓存到变量里不要每帧重复查找。其次检查影子。每个敌人的LightOccluder2D如果都开阴影几百个光遮挡物同时参与阴影计算再强的GPU也扛不住。我中途把敌人阴影全部关掉帧率直接从45帧回到60帧。7.5 2D场景中“视觉上明明在物体后面却被渲染到前面”这个问题90%是因为节点不在同一个层级。Godot的CanvasItem绘制顺序遵循“先父后子、同层按树序”的规则。哪怕Y-Sort启用了它也只对同一个YSort节点下的直接子节点生效。如果你的角色是A节点的子节点树挂在B节点下而A和B都是C节点的子节点那Y-Sort的排序范围是C而不是A和B角色和树不会互相排序。这个嵌套结构问题排查起来很隐蔽务必把所有需要互相比遮挡关系的物体放到同一个父级YSort节点下。8. 进阶扩展这套伪3D体系还能怎么玩写到这里第15篇的核心内容已经完整了。但既然到了中后期我想再给一个扩展思路方便你把刚才学的空间感继续放大。目前做的Y-Sort 光照 阴影本质上是“同一平面上的深度模拟”。如果你还想更进一步可以做多层视差背景前景遮挡也就是在地图的最上层再加一个半透明的装饰层挂一些悬垂的树枝、藤蔓、灰尘粒子。这层用固定的ZIndex比如10不参与Y-Sort始终绘制在所有物体之上。玩家经过时会被树藤“拂过”生存射击游戏里非常出氛围。具体实现上前景装饰层用Parallax2D节点挂载 Motion Scale 设为0.8到0.9——意思就是它比主场景稍微“远”一点移动速度略慢于地面产生视差效果。前景层里的Sprite要设置为“不可被射线检测”否则子弹打到隐形的前景上会让玩家以为游戏出Bug了。另外可以尝试把敌人设计成“多部分实体”头和身体分开做两个Sprite都挂在同一个YSort节点下。头用Sprite2D的Offset轻微向下偏移身体跟随敌人的position。当玩家走到敌人下方Y坐标更大时敌人身体遮挡玩家玩家走到敌人上方时玩家遮挡敌人身体但敌人的头依然在玩家上方——这在视觉上就模拟了一个“立体的人”站在场景里而不是一个纸片人。很多商业2D游戏的NPC就是这么做的。根据我的经验把这套伪3D和遮挡体系真正吃透之后你再回头看那些“看起来特别贵”的2D独立游戏会发现它们很多效果都是这些基础玩法的组合拳——Y-Sort排序卡得好、光照阴影填充层次、前景背景拉开空间、细节动画填充手感仅此而已。最后分享一个操作层面的小细节不要打开Godot项目设置里的Rendering - 2D - Default Clear Color默认纯色背景改成带轻微渐变或深色底色。俯视角生存射击场景里纯黑或纯白的底色会让光照效果大打折扣深灰偏蓝的底色更能突出2D光照的暖冷对比画面质感会立刻上一个台阶。下一篇我会继续围绕这套体系讲敌人AI的行为树改造把第15篇做出来的场景纵深感和光照体系用在一个真正聪明的Boss战里。到时候你就知道前面所有关于排序和层次的功夫都会在复杂的战斗场景里成倍回报给你。
返回列表