首页 / 资讯中心 / 文章详情

UE4 2D游戏核心计算:坐标、移动与伤害公式实战解析

UE4 2D游戏核心计算:坐标、移动与伤害公式实战解析 ★ FEATURED ARTICLE
做2D游戏做到中后期你会发现真正卡住进度的往往不是美术资源也不是动画状态机而是藏在各个逻辑里的计算。这篇文档是2D游戏制作系列的计算篇重点解决 UE4 里坐标换算、移动手感、战斗伤害这些天天都要碰的数学问题。前几篇已经把纸片人显示、动画、输入基础搭好了这一期开始我们把目光从“怎么让角色动起来”转移到“怎么让角色动得对、打得准、数值不崩”。写这篇之前我把手上这个2D横版Demo里所有涉及计算的节点翻了一遍发现大多数问题都不是节点不会用而是对“算出来的结果应该是什么”心里没数。坐标单位搞混、击退方向飞上天、伤害莫名其妙变成负数这些问题用Print String一看就露馅但排查过程非常浪费时间。所以这篇文档不讲虚的全部围绕实际开发里会遇到的“计算”展开适合已经搭好2D基础、正在调数值和逻辑的同学作为备用参考。1. 这一期为什么值得单独拿出来说1.1 计算不像动画和素材错了不会直接报错动画资源缺失会红屏材质节点断裂会显示紫色你一眼就能看到问题。但计算出错的时候画面往往一切正常只有数值行为不对角色速度略快、伤害偶尔为负、镜头边界露出黑边、敌人在某个角度被击飞得很离谱。这类问题最麻烦的地方在于它不会弹错误提示只会在特定输入组合下悄悄出现。我在做这个2D Demo的过程中就踩过一个典型的坑角色加速移动时我把目标速度直接放到了每帧执行的逻辑里却忘了乘DeltaTime。看起来代码完全没问题但60帧和30帧下角色移速差了将近一倍。这种问题如果不是单独把“计算”拎出来审查根本不显眼。把计算单独成篇的另一个原因是蓝图节点一旦拖多了可读性会快速下降。公式逻辑混在事件分发、状态切换中间变量名又起得随意最后没人敢改数值。我后来把所有常用计算统一收进Blueprint Function Library每个函数只做一件事参数命名带单位后缀比如Speed_cm_s、Damage_f。事实证明后续调手感、调战斗数值的效率翻了不止一倍。1.2 这篇文档覆盖哪些计算范围考虑到2D游戏最核心的体验本文主要覆盖三块空间坐标换算、移动与插值计算、战斗与数值计算。空间坐标这部分解决“对象到底出现在屏幕哪个位置”移动插值解决“角色手感是否跟手”战斗数值解决“伤害公式怎么设计才不会数值膨胀”。最后还会补充我实际调试中遇到的典型陷阱以及排查手段。不覆盖的内容也提前说明骨骼动画、物理模拟的底层算法这些不在讨论范围里。我们用的是纯蓝图实现项目是简单的2D横版动作Demo没有引入C扩展也没有做联网同步。计算层面的思路和参数设计仍然可以通用套用到不同项目上只是具体数值需要你根据自己游戏类型调整。2. 空间计算坐标、像素和视口换算2.1 先搞清楚UE4里的度量单位UE4默认的一单位是1厘米这是所有空间计算的基准。2D游戏里我们经常直接把图片素材当成“像素”来想但放进引擎后这个“像素”会被换算成引擎单位。Paper2D项目设置里有一个Pixels Per Unreal UnitPPU的概念默认情况下可以理解为1像素对应1引擎单位也就是1厘米。这个比例对2D游戏非常重要。假设你导入了一张512×512的立绘PPU设为1那它在世界里就是512单位宽。如果相机可视宽度是960单位这张图就占了屏宽一半还多。如果你的美术资源全是美术同学按1280×720的分辨率出的直接按1:1导入横版关卡会变得很大角色跑起来需要很长的路程。我见过不少新手项目最后所有坐标都大得离谱就是没意识到PPU对世界尺寸的影响。我的做法是先确定“一个设计分辨率下对应多少世界单位”。比如我想让1920像素宽的屏幕背景正好铺满就把相机OrthoWidth设为1920默认PPU取1那布局时每1像素就是1引擎单位。如果你觉得数值太大也可以把PPU设为2即2像素对应1单位这样同样一张图导入后逻辑尺寸会变成一半。确定之后所有角色规格、攻击距离、关卡长度都按同一套比例来算后面就不会乱。2.2 世界坐标、视口坐标和屏幕坐标的换算关系2D游戏开发中经常需要把某个Actor的位置换算成屏幕坐标。比如血条UI要跟随角色头顶、小地图标记要吸附到屏幕边缘、伤害数字要在受击点附近弹出这些都涉及坐标空间转换。UE4蓝图里主要用两个节点Project World Location to Screen负责把世界坐标转屏幕坐标Deproject Screen to World负责把屏幕坐标转回世界坐标方向。这里有一个容易误解的地方Deproject返回的是从相机位置出发的一条射线不是单一目标点。在透视相机下你要自己取射线上的某一段距离来定位在2D游戏常用的正交相机下这条射线和平面的交叠关系更简单基本可以直接当作世界坐标参考来用。我封装的随机出生点函数就是这样实现的先拿到Viewport Size把屏幕左下角和右上角分别Deproject到世界空间得到两个点然后在这两点范围内做随机插值生成一个屏幕范围内的世界坐标。2.3 正交相机视野边界计算2D游戏镜头通常用正交相机核心参数是OrthoWidth。很多人只设置了OrthoWidth就开拍没有意识到视野高度是由宽高比决定的。公式很简单可视宽度等于OrthoWidth可视高度等于OrthoWidth除以宽高比。宽高比是Viewport Size的X除以Y比如1920×1080的宽高比约1.78。所以在1920×1080项目里如果你把OrthoWidth设为1920相机能看到的高度就是1080正好等于屏幕分辨率。按这个思路你就可以精确推算角色从关卡左边走到右边时相机应该在什么范围移动才不会让背景露馅。我当时给横版关卡做了边界限制代码如下CameraHalfWidth OrthoWidth / 2 CameraMinX LeftBoundary CameraHalfWidth CameraMaxX RightBoundary - CameraHalfWidth 相机X Clamp(角色X, CameraMinX, CameraMaxX)关卡总宽度3000OrthoWidth设为960时CameraHalfWidth为480。那么相机X只允许在480到2520之间移动。这个计算不复杂但能避免最丑的露边问题。如果关卡左右还有对称的装饰背景我通常会在LeftBoundary和RightBoundary上额外留出一两百单位的安全余量防止镜头在极端位置看到背景边缘。3. 移动手感计算从输入到速度3.1 输入只是方向速度才是手感轴事件返回的值是-1到1之间的“方向”直接把这个值加到位置上角色会有一种“键盘骑士”的僵硬感没有加速和惯性。要做出2D动作游戏那种轻重有别的手感中间必须再加一层速度插值。我常用的结构是先定义目标速度目标速度等于轴输入乘以最大跑步速度然后用FInterpTo把当前速度平滑逼近目标速度。FInterpTo每帧都会计算出一个新值参数里的InterpSpeed控制逼近速度数值越大角色越快达到目标速度手感越灵敏。一个实际配置示例TargetSpeed InputAxis * 600.0 CurrentSpeedX FInterpTo(CurrentSpeedX, TargetSpeed, DeltaTime, 8.0) NewLocation GetActorLocation() (CurrentSpeedX * DeltaTime * FVector(1,0,0))这里AccelerationRate取8手感偏轻快。如果想做重装战士那种起步沉重的角色降到2到3起步会有明显延迟感。不同角色类型对参数要求差别很大我建议直接把加速度和最大速度做成角色类的变量调试时不用改蓝图逻辑。3.2 为什么所有移动都要乘DeltaTime这个点老生常谈但真的是2D项目里出现频率最高的问题。UE4的Tick事件每一帧都会执行但每帧间隔不一样。60帧下1秒分成60次更新144帧下分成144次。如果每帧移动固定距离高帧率角色就跑得更快。所有和速度相关的位移计算最终都必须乘DeltaTime。DeltaTime就是这个“每帧持续了多少秒”的数值乘上去之后速度就变成每秒多少单位了。一个很容易搞混的地方是如果用了MovementComponent并调用AddMovementInput引擎内部通常已经帮你处理了DeltaTime这时候你不要再手动叠加一步同一速度的位移否则等于二次加速。我做这个Demo时敌人AI的移动和角色移动用了两套写法结果敌人移动速度在不同刷新率下表现不一致。后来统一改成“速度值乘以DeltaTime再叠加到位置”的写法问题才彻底消失。所以每次新加移动逻辑先问自己一句这个数值是每秒的概念还是每帧的概念3.3 插值不只是用于移动插值计算在2D游戏里还有两个很重要的应用场景镜头跟随和血量条平滑变化。镜头如果直接绑定到角色位置角色跳跃时镜头会非常生硬。更常用的做法是让镜头位置每帧向目标位置靠近一点使用VInterpTo节点即可。DesiredCameraLocation TargetActor.GetActorLocation() FVector(0, 0, 0) NewCameraLocation VInterpTo(CurrentCameraLocation, DesiredCameraLocation, DeltaTime, 5.0)InterpSpeed数值建议根据游戏类型调快节奏游戏用8到10镜头能迅速跟上角色探索型游戏用3到4镜头移动更舒缓。插值算法的好处是无论角色怎么瞬移镜头始终有一个平滑过渡不会跳变。血量条、能量条也别直接设置百分比显示而是用一个当前显示值和目标百分比做FInterpTo这样受伤掉血时血条会有一段“延迟回落”视觉反馈远比瞬间变化舒服。数值计算上就是DisplayPercent FInterpTo(DisplayPercent, TargetPercent, DeltaTime, 4.0)这种插值手感属于低成本高收益强烈建议都用上。3.4 2D角色朝向与翻转计算横版2D角色转向通常不是旋转模型而是翻转Flipbook组件的X方向。蓝图里判断输入方向无非是比较输入值的正负。我建议用Sign函数而不是直接判断大于0因为浮点数的微小波动可能导致等于0的状态被漏掉。FacingRight InputAxis 0 ? true : false翻转的瞬间有一点需要注意不要每帧去设置Flipbook的Flip值只在方向改变时设置一次否则会在动画播放过程中反复翻转视觉上会出现抖动。我在关卡里放了一个“记录上一帧朝向”的变量每次只有新方向和旧方向不一致时才执行翻转。4. 打击判定和击退计算4.1 攻击判定盒的偏移计算2D动作游戏常用BoxOverlap做攻击判定。攻击判定盒的位置通常不在角色中心而是偏移到武器前方。这个偏移值需要根据角色当前的朝向动态计算朝右时偏移量为正朝左时偏移量为负。每个角色判断方式是ForwardOffset FVector(FacingDirection * AttackRange, VerticalOffset, 0) AttackBoxCenter GetActorLocation() ForwardOffset AttackExtent FVector(HalfWidth, HalfHeight, 0)这个计算里最关键的是FacingDirection要取整数。朝右为1朝左为-1而不是直接取得角色当前的速度向量因为角色在攻击瞬间可能处于静止状态或击退状态速度向量并不能代表他面朝的方向。我用角色朝向翻转的布尔值来生成这个1/-1安全可靠。4.2 击退方向的正确算法击退是2D动作游戏高频使用的反馈。很多人图省事直接取攻击者到被攻击者的方向向量再乘以击退速度。这个做法在水平地面上问题不大但如果攻击者跳到敌人头顶向下攻击被攻击者就可能被击飞向上方45度看起来很夸张。我的处理方式是击退方向拆成水平和垂直两部分分别设计。水平方向只看攻击者与被攻击者的X轴相对位置计算出向左或向右的固定方向垂直方向则根据打击类型定义普通攻击通常给一个轻微向上分量挑飞攻击才给大幅度向上分量。KnockbackDir_X AttackLocation.X TargetLocation.X ? 1 : -1 KnockbackVector FVector(KnockbackDir_X * HorizontalStrength, VerticalStrength, 0)受击瞬间直接给被攻击者叠加一个速度值之后速度会被移动系统的摩擦和插值消化掉形成“被打退后逐渐停下”的效果。千万不要把击退向量直接设置成目标位置否则角色会被硬拉过去表现非常僵硬。4.3 重复打击检测的时间窗计算用BoxOverlap做攻击判定时有一个很常见的bug攻击帧持续过程中同一个敌人每帧都会被检测到伤害会被触发多次。解决办法是给每个敌人记录一次“最近受击时间”。if (WorldTime - LastHitTime ComboInterval) 跳过本次伤害 else 记录LastHitTime并执行伤害ComboInterval可以设成0.2到0.3秒表示这个攻击内容在一个短时间窗内不会被同一攻击反复触发。如果把时间窗设为0.1秒连续攻击依然可以靠后几帧触发伤害这正好实现了连击的伤害频率控制。我后来把这个逻辑封到了通用函数里所有近战武器都复用同一套判断省了很多重复节点。5. 战斗数值和核心循环计算5.1 一套可持续调整的伤害公式战斗数值设计没有绝对标准但有两点核心要求数值要可控调试要直观。我在这套2D Demo里试过减法公式和除法公式最终选了减法公式加最小伤害保底再配合暴击和浮动系数。基础公式如下最终伤害 Max(1, 攻击力 * 技能系数 - 防御力) * 伤害浮动 * 暴击系数 * 等级补正减法公式的好处是每个属性含义直观攻击力100防御30打出来的基础就是70。缺点是当防御接近攻击力时伤害会趋近于0所以要用Max函数强制保底1点伤害否则游戏会进入“你打我不破防我打你也不掉血”的死局。防御减伤更平滑的替代方案是除法公式减伤率等于防御除以防御加常数伤害永远不会归零但参数设计不够直观。如果做的是暗黑类刷宝游戏用除法公式更合适如果做的是街机动作类减法公式的反馈更加清楚。你先明确自己的游戏类型再去选公式不要两款游戏都在用同一套。5.2 再看等级补正的曲线等级差会影响伤害我用的是线性补正LevelDiff AttackerLevel - TargetLevel LevelFactor 1 LevelDiff * 0.05每差一级伤害浮动5%。这个参数对同等级PVE游戏比较友好不会出现低级玩家完全打不动的情况。等级差距超过10级后补正会变得很明显需要根据你的设计目标去限制最大补正百分比否则后期刷低级地图时伤害会溢出过头。5.3 暴击、伤害浮动与随机数暴击判断用的是标准概率模型。随机函数生成0到1之间的小数小于暴击率就判定暴击暴击伤害再乘一个暴击倍率。这里提醒一个容易被忽略的点不要直接拿固定概率作为唯一判定玩家连续多刀不暴击的体验很差。我模仿经典做法做了一个保底暴击机制每次不暴击暴击率临时增加一定值一旦暴击保底计数清零。这样虽然没有改变长期概率分布但玩家体感上会更均匀。伤害浮动是数字不单调的关键。满暴击的伤害数字永远是整数也不舒服可以加一个0.9到1.1的随机浮动最后四舍五入。浮动要注意对最终值进行RoundToInt否则会出现小数伤害显示和逻辑都不自然。5.4 冷却时间更适合用时间戳计算技能冷却每个人都有自己的写法最自然但最容易出错的是每秒自减倒计时。这个方案在暂停游戏、时间膨胀、调试跳帧时会积累误差。更稳的做法是每次技能释放时记录一个时间戳CanUseSkill 当前时间 下一次可使用时间 下一次可使用时间 当前时间 冷却秒数UI显示剩余时间时再计算差值剩余冷却 Max(0, 下一次可使用时间 - 当前时间)这种方法的好处是无论中间发生暂停、减速还是调时间Scale冷却计算都不会错。而且断点调试时你只需要看时间戳变量不用猜“当前倒计时是不是被多减了”。5.5 状态效果的乘算与加算角色可能同时吃到多个减益效果。比如被减速30%同时被“时间缓速”影响50%。如果直接做加法合计减速80%效果会被过度放大如果用乘法最终速度倍率是0.7乘0.5等于0.35倍也就是减速65%。我统一采用乘算规则避免玩家叠加多个减速技能直接把角色定在原地。同类增益也建议乘法叠加比如两个10%攻击加成合起来不是20%而是21%。差异不大但能抑制无限堆属性的数值膨胀。5.6 经验值与升级循环计算升级曲线我用了一个二阶多项式经验需求随等级平方增长低等级升得快高等级变慢。公式为经验需求 BaseExp (Level - 1) * GrowthFactor (Level - 1)^2 * CurveFactor升级逻辑写在事件里用循环处理一次获得大量经验时连续升级的情况。设计时给最大等级加一个上限避免玩家经验溢出后循环执行太多转身把游戏卡死。这点在测试阶段输入异常数值时必须考虑。6. 常见计算事故与排查排障手册6.1 零向量归一化导致的NaN归一化一个长度为零的向量会得到含有NaN的结果。2D游戏中最典型的场景是角色站在攻击者正上方攻击者和被攻击者位置完全重合然后你取方向向量做一个GetSafeNormal。方向向量 目标位置 - 自身位置 方向向量 方向向量.GetSafeNormal() // 可能变成 NaN解决办法很简单归一化之前检查向量平方长度是否大于一个极小值比如0.0001。如果长度过小直接使用预设的默认方向。排查时一旦看到伤害变成-1.#IND或者角色瞬移优先怀疑位置方向计算里有NaN产生。6.2 除以零和防御力变负战斗计算里尽量避免直接除变量非要除一定要先检查被除数是否为0。防御力出现负数会让伤害公式崩坏属性配置时最好加一个Clamp防呆。我一般所有测试数值假人都有一个“数值异常检查”的统一入口进入战斗前打印攻击、防御、暴击率三个值确认范围再开打。6.3 帧率依赖和TimeScale混用前面已经讲过DeltaTime的重要性。另外要提醒如果项目里有自定义TimeDilation注意你取到的DeltaTime已经被全局时间膨胀影响了。如果某些计算需要真实世界时间要用App Time相关的节点否则暂停游戏后还在偷偷计算冷却对单机游戏来说体验很差。6.4 浮点数大小比较的坑蓝图里比较浮点数时用“等于”节点经常不触发因为浮点精度误差可能让0.10.2不等于0.3。血量百分比、进度条这类判断建议做区间判断百分比小于0.01视为空大于0.99视为满。不要写严格的相等判断除非你确实知道数值来源是整数类型。6.5 常用排查手段速查现象可能原因排查方式角色越跑越快位置叠加漏乘DeltaTime断开速度累加用Print看单帧数值伤害为NaN或负值零向量归一化、防御为负在伤害入口处Print后退出函数镜头出现黑边边界限制公式错误在相机位置处Draw Debug Box冷却条反复跳动每帧重置了时间戳检查时间戳是否在Tick里被误更新攻击命中不触发判定盒偏移方向与朝向不一致用Draw Debug Box看检测盒6.6 调试时优先使用Draw Debug和Print String调试计算逻辑最忌讳猜。Draw Debug Box和Draw Debug Line可以在场景里直接看到判定范围、位移路径和镜头边界。Print String配合Format Text能把你关心的变量值全部拼合输出比如角色当前速度、当前朝向、冷却剩余时间。我调攻击判定时会在事件Graph里临时挂各种Print String确认每步数值再逐个移除。7. 做完计算篇后的几点手感和扩展方向这一系列写到这里我最大的感受是2D游戏的计算并不是越复杂越好。坐标换算、移动插值、伤害公式每一块都是独立的小模块把它们拆开封装成一个一个函数以后调试和维护都轻松了很多。如果你现在也用蓝图写2D游戏我强烈建议把常数值全部做成可在蓝图细节面板调整的参数不要写死在节点上。调手感的时候你每改一次数字就切到游戏里跑一圈效率高很多。这套计算逻辑还可以扩展的方向不少。比如AI预判移动轨迹、弹道抛物线的命中点计算、组队战斗里的仇恨值权重分配都是在现有坐标系和数值公式上叠加的。下一期如果继续写我大概率会选弹道与追踪计算作为主题因为这类逻辑接收的输入更多计算链条也更长验证起来特别有意思。在那之前先把这篇里的坐标换算和伤害公式吃透后面会省力不少。
阅读完成 · 觉得有帮助?
咨询建站