【游戏设计入门与实战】深入聊聊UI跳字,和一比一复刻原神炫酷跳字(内附教程及代码)

精华修改于2023/07/151186 浏览主题教程

引子

观众老爷们大家好啊,可以称呼这里溯雪
转眼间,申请到星火的测试资格已经过去几个月的时间了,我也在星火交流群里潜水了几个月,从一名从业者转变为无业游民(..)
写这篇帖子的契机,是昨晚在群里,偶然看到天一老师建议大家都研究研究吸血鬼的设计、UI反馈等,我顺便问出了一直的疑问:”写一些跟编辑器本体可能关系不大,但和游戏设计本身相关的讲解+实战案例,算不算有贡献?“得到了肯定的答复后,在今晚紧赶慢赶码出了此篇攻略帖。
TapTap
TapTap

需要说明的事

        首先,此攻略探讨的UI跳字内容,只适合游戏已经制作到了铺量及待上线阶段,期望精益求精的游戏制作者,或希望了解游戏设计知识,对内容抱有好奇心的求知者。如果您还处于游戏制作初期阶段,探讨的内容可能并不会提供太大价值,当然也欢迎您粗略浏览后拉到最下方,点赞评论复制代码然后直接用[TapFamily_爱了]
        然后,个人与星火最终的愿景,都是希望能在taptap上,有更多的人能做出自己喜欢的、好玩的、有趣的游戏。所以我在本文(以及如果日后还有其他文章的话)内,会采取简单的讲理论-讲拆解-讲实战这样的方式,欢迎感兴趣的小伙伴或有缘得见的业内大佬一起探讨,共同进步,磨练设计_(:з)∠)_

引子2

在实践前需要准备的

  • 一个可以打开并编辑json文件的代码编辑器(记事本、notepad++、vscode等,下文演示为vscode)
  • 你希望对标UI风格的一款游戏,及该游戏带有跳字的视频
  • 星火编辑器和任意测试关卡
  • 2到3小时学习和实践的时间

接下来会讲什么

  • 什么是跳字;为什么需要跳字
  • 不同游戏和类型的跳字设计风格
  • 在星火编辑器内,跳字是如何实现和改动的
  • 原神风格的跳字拆解实战和json代码
  • 笔者在操作中遇到的一些问题

正文

什么是跳字,为什么需要跳字?

        你是否有过这样的疑问:有的游戏明明加了一堆酷炫狂霸的怪物,挂了各种闪光技能粒子特效,甚至全屏震颤和动态模糊都来了,怎么感觉好像...还差点意思?让我们来看两个演示吧!
TapTap
某个魔改魔兽跳字的效果
TapTap
星火编辑器默认的战斗跳字效果
        没错,区别可能在我今天想聊的,游戏跳字[TapFamily_摸鱼]
        跳字,属于游戏UI(用户界面)的一部分。在不同的游戏中,你看到的”奥数充能++“、”伤害99800“、甚至”暴击!“,都被包含在游戏跳字内。在不同类型的游戏中,花里胡哨的跳字承担着各式各样的功用,但总体来说,可以把其功用归为两类:信息传达情感传递。
        信息传达,顾名思义,是为了补全游戏内其他地方传达缺失的各类信息,或直接用文字形式告诉你一些信息。比如,我一刀砍下去砍掉了多少血,一个技能它的伤害类型是魔法还是物理,我一箭射到大怪鸟身上,到底是打到了头还是翅膀。伤害值、频率、类型、位置、警示信息等,通常是战斗跳字管辖的信息传达领域。
        情感传递,也很好理解。我给你看一张图就明白了[TapFamily_鸽]
TapTap
传奇类游戏的战斗表现
        一刀999,生命几百万,爽不爽?当然爽!这时候,跳字承载的,便是给玩家各种各样的游玩感受,诸如:”我好强“、”我伤害真高“、”miss了倒霉“,”一群菜X“等等。
        特别需要说明的还有两点:
        1.由于国内游戏发展的历史趋势和曾经大热的传奇类游戏的原因,以及按星火擅长制作的游戏类型来说,精心设计的战斗跳字是更加适合中国宝宝体质的。
TapTap
        2.即使不讨论游戏历史,仅聊手机因为屏幕尺寸相较电脑被大幅压缩,场景、人物、动作都为了让人看清而剪影化,更富有视觉张力。这时,手机游戏普遍也更需要,用更大面积、更高明度的跳字,来保证清晰的信息传达,匹配手游的视觉冲击力和高视觉张力。
TapTap
对比以下同等屏幕大小下的跳字表现:逆水寒手游
TapTap
逆水寒端游

不同游戏和类型的跳字设计风格

        这边不去展开分析更多游戏,只举几个比较典型的。
        一种简单的两问判断方法是:
        1.游戏想突出的重点是什么?是希望提供给玩家某种沉浸式的游戏体验(如各类3a游戏),还是想让玩家体验刷刷刷的爽感?
        2.游戏的美术风格是什么?偏写实(标准7头身、高精度场景建模),还是偏卡通(3头身甚至更低、低多边形、简单纹理、大色块)?
        根据这两个问题回答,游戏会被粗略分为4个象限,其中,越沉浸、写实风格的,其战斗跳字也会表现的更加克制,如非衬线的圆润字体,风格统一、自然且流畅的动效;如今年新上的暗黑四的跳字风格。
TapTap
暗黑破坏神4战斗跳字
        相反的,风格化明显,美术偏向卡通且希望情感传达强烈的,则会采用各种天马行空,跳脱和具有设计感的跳字,如著名米家光污染游戏(
TapTap
崩坏 星穹铁道战斗跳字
TapTap
女神异闻录5 满屏乱飞的符号
       在这边再补充一下,星火所做的游戏,目前看来基本属于“爽感”+“卡通”象限维度的。因此,更适合配上风格化、富有设计感的跳字效果。

不同类型的跳字

        前面我们提到过,跳字兼具信息传达和情感传递两个功能。因此,在实际跳字设计的过程中,会根据跳字的实际使用场景,联想类似的情感表达,从而设计不同的表现动效。
先将跳字以可能的使用场景简单分类,我们会获得以下几类:
  • 物理攻击
  • 法术攻击
  • 暴击
  • 治疗、状态
  • 受伤、防御
  • 获取道具
  • ……
        其中,在攻击他人造成伤害时,我们会希望跳字富有力量感,带有侵略性。因此在动效设计上,一般会呈现快速放大、停留、变小消失的过程。同时,根据不同的伤害类型,也会存在一些模拟攻击武器的动效。如:物理类伤害可能是斧子劈砍,呈现横向迸射;也可能是锤子砸落,呈现“由饼缩小”;而法术攻击,则会模拟星星、火光、或流水,呈现一定的飘动和波浪感,或流线性。而对于暴击、背击等特殊类型伤害,则一般是以区别与普通伤害的更大的体积、特殊伤害标记(图标、文字等)、更高明度的颜色等,来与普通伤害区分。
TapTap
LOL模拟箭矢射中的战斗跳字表现
        在操控目标受伤时,本着朴素的不能宣扬自己遭罪的心情,飘字一般仅尽到提醒你受伤了作用,普遍比攻击飘字要小,且呈现快速出现、小幅度的移动和消失的表现。不过值得一提的是,如果游戏类型是玩家存活难度较高且血量有限、难补充的游戏类型中,也存在受伤跳字做的又大停留时间又久、且配上一系列的屏幕特效和受击特效来提供心理压迫感的设计。
        而治疗、防御、状态可以合并到一类,buff和debuff。一般此类影响会较为持久,根据效果的增益和减益性,一般会模拟一种持久、安定的设计体验,呈现慢速出现(或渐强)、停留后朝上或朝下移动且消失的设计。
        当然,以上只是概括了一些比较常见和典型的设计表现,在实际的设计和使用中,只要符合跳字的使用场景和整体的美观和谐体验,任何形式的表现均可以尝试。

星火的跳字是如何实现与改动的

星火跳字的定义       
        在星火中,我们新建一个工程文件,找到工程文件下的game_hud/acriselettertemplate.json文件,该文件即是该工程的跳字效果管理数据文件。我们打开该文件,拉到文件的最上方,可以看到初始开发人员完整的注释。
其中,一个完整的跳字效果的相关数据可以分为两部分:跳字本身的定义部分和跳字的动画效果部分。
TapTap
跳字定义部分
这边再具体自上至下地展开讲一下每条数据:
  • templatename:这个字段定义了该类型的跳字在编辑器内部被封装的类型;如金币、物理攻击、治疗等等;这字段不支持修改,也不支持个人拓展,仅可以联系星火官方后续拓展。
  • anchor:即跳字的挂点位置。挂点指字在跳出来的时候,到底处在跳字主体的什么位置。简单来说,如攻击的伤害,是跳在受到攻击的怪物身上,而anchor,则定义了实际跳出位置与怪物的距离和相对坐标。可以通过修改此值,使跳字更加远离怪物或靠近怪物。在部分类型内还提供了RandomRange的定义,指跳字可以在挂点附近的一个矩形范围内随机,而非每次都从同一个相对位置跳出。
  • addtimelimit:如一只怪物在1秒内受到5次20的攻击,若此值为1,则表现会是怪物身上的跳字由20变为40、60、80、100;若此值为0,则表现是怪物身上会出现五次20的跳字。该处可以控制跳字的频率和表现,如果在游戏后期高频率的攻击中,将一定时间内的跳字合并显示,将有助于整体画面的洁净,节省性能,减少玩家的厌烦感。
  • name、type均不支持修改,不展开赘述;
  • rect:我推测是跳字可以进行宽度和长度的缩放,但未实际尝试,可以让天一老师答疑解惑下。
  • color:一个rgba值,网上查找到的颜色rgb是该数值的前三位,第四位是alpha即透明度,但理论上应该是个0-1的值,实际调试透明度时跳字会呈现有和无的两端,不知道是否为bug。其中,这里和rect及下面的pretext有个合并的大坑,暂且按下不表。
  • font:指该跳字类型实际调用的字体。此处字体存在于game_hud\riseletter文件夹内。
TapTap
动画效果部分/直线的数值
随后,是跳字的动画效果。每个跳字类型下面的一系列动画效果组合,构成了跳字从出现到消失的全过程,通过分析想实现的跳字表现,我们就可以实现复刻某个希望对标的游戏跳字。
sequence:动作序列,在sequence下即黄线画出的右侧,均构成了一整个动作序列,会呈现播完第一个播第二个动作的过程。
time:一个动作持续的时长,是以秒计时。解释一下后面的线性插值,可以认为当位置、color和alpha有变化时,动作内会匀速的过度到变化后的值。即如果想做一个快速下落然后慢速下落消失的过程,需要分成两个动作来实现。(因为两段动作速度不一样)
type:动画的类型,粗略的查找了一下,存在两类:直线和贝塞尔曲线。
xy:动作的坐标,X增大向右移动,Y增大向上移动。
T:按描述是控制点的权重,常见只需要用到0和1(起点终点),但或许能实现如直线运动到一半才开始变淡的表现。
alpha:即透明度,255为100%透明度(完全可见),0为0%透明度。
TapTap
贝塞尔曲线的参数
贝塞尔曲线与直线略微不同,定义了四个控制点,自上至下分别为起点、控制点1、2、终点。修改控制点的坐标,可以做到画出不同形态的曲线。
星火的字体
        在星火中,目前预置的战斗跳字,是通过字符对应字的PNG图片的形式来进行的,通过网上搜索数字PNG设计资源,并切图和星火预置的跳字图片资源大小一致(20X27像素),替换资源,便可以实现根据游戏定制的风格化跳字设计。
TapTap
如仅替换2,可以呈现以上效果
TapTap
跳字图片资源及路径地址
        但这里也有一个坑:星火为了简化在此方面的开发难度,预置的跳字资源是带有符合伤害类型的基本颜色的。而跳字效果管理json内,定义跳字的color/scale,是在跳字图片资源基础上进行颜色叠加、素材放大。因此,如果有进阶的修改需求时,会出现如字体颜色修改无效、字体放大后变模糊等问题。
        下面的案例中,我将以法伤和受伤做代表性讲解,我强烈建议需要修改字体颜色的同学,将其他伤害类型的字体资源引用全部改为“物理”(白色跳字)。

原神风格的跳字拆解实战

TapTap
攻击表现
        终于到了实战分析的环节了,此处,我们再祭出这个魔改原神风的魔兽跳字GIF,以攻击跳字和受伤跳字两个最典型使用的跳字,来实战拆解和复刻一下跳字设计思路。看到这里的同学,如果有自己想对标UI的游戏,也可以打开游戏视频,一起进行分析。
攻击跳字
可以看到,该风格的攻击跳字的动效,是类似于锤击的表现形式,侧重于表现攻击的力量感和伤害感受。
动效可以拆分为以下几段:
  1. 中速、中字号出现,快速向上小幅度飘动
  2. 字号快速放大
  3. 维持放大,向左上/右上跃动一小段距离
  4. 朝跃动的方向快速下落,同时渐隐
完成了整个跳字从出现到消失的全过程。其中,调整步骤1和3的跃动方向和速度,便可以变形为箭矢攻击(远程攻击),轻剑砍击等表现。
TapTap
受击表现
再来看看受到攻击,仅呈现了弱化且克制的视觉表现。
  1. 小字号出现
  2. 原位置停滞一段
  3. 向上飘动缓慢消失
        这些动效中,直线运动基本不必说,反复的调整尝试就可以获得接近的表现效果。曲线运动的贝塞尔曲线,这边推荐一个在线的绘图网站:https://www.bezier-curve.com/。可以大概拆解出动效的运动轨迹,在绘图网站进行模拟后,再将坐标移动到json内。
        另一个需要注意的是,由于跳字动效是个连续播放的动画,在多个动作中,需要注意前一个动作的结束位置、透明度、缩放与后一个动作一致,否则会出现断续的不合理表现。当然,也可以通过设计,表现出类似打水漂一类跃动的特殊跳字,但每段进入一般仍需要透明度从0到有的过度,来实现视觉的自然(而不是看起来掉帧)

来看看成果和代码吧

TapTap
在游戏初期,我们得到了这个
TapTap
在战斗后期,怪物和攻速提升后
 谢谢大家!
horizontal linehorizontal line
  // RiseLetter_NormalAttack Group
    {
      "TemplateName": "RiseLetter_NormalAttack", // 名字必须是这个
      "RandomRange": [ 100, 80 ],//随机范围: RangeX, RangeY;将实际的Anchor放在「以Anchor为中心,RandomRange范围的矩形内」
      "Anchor": [ 127.5, 12 ],
      "AddTimeLimit": 0.3, // 可累加的时间段限制
      "Layout": // Widget布局
      [
        // 数字
        {
          "Name": "Content",
          "Type": "TEXT",
          "Rect": [ -10, 82, 255, 24 ], // X, Y, Width, Height
          "Color": [ 223,22,23, 255 ], // rgba
          "Text": "12345",
          "Font": "物理",
          "Align": 1 // 0-左对齐, 1-居中, 2-右对齐
        }
      ],
      "AnimationSet": [// 动画列表(每次跳字事件发生 会随机选择一个播放)
        // 动画0
        {
          "Name": "01", // 名字可不设置
          "Sequence": // 每个动画由多个动作序列组成
          [
            // 动作01
            {
              "Time": 0.15,
              "Curve": {
                "Type": "Line", // 直线类型
                "Ctrls": [
                  {
                    "X": -10,
                    "Y": -60
                  },
                  {
                    "X": -10,
                    "Y": -60
                  }
                ] // 2个控制点(也就是直线的2个端点AB)
              },
              "Start": // 起点
              {
                "T": 0.0,
                "Alpha": 255,
                "Scale": [ 0.6, 0.6 ]
              },
              "End": // 终点
              {
                "T": 1.0,
                "Alpha": 255,
                "Scale": [ 0.8, 0.8 ]
              }
            },
            // 动作02
            {
              "Time": 0.3,
              "Curve": {
                "Type": "Line",
                "Ctrls": [
                  {
                    "X": -10,
                    "Y": -60
                  },
                  {
                    "X": -10,
                    "Y": -45
                  }
                ]
              },
              "Start": {
                "T": 0.0,
                "Alpha": 255,
                "Scale": [ 0.8, 0.8 ]
              },
              "End": {
                "T": 1.0,
                "Alpha": 255,
                "Scale": [ 0.8, 0.8 ]
              }
            },
            // 动作03
            {
              "Time": 0.2,
              "Curve": {
                "Type": "Line",
                "Ctrls": [
                  {
                    "X": -10,
                    "Y": -45
                  },
                  {
                    "X": -10,
                    "Y": -45
                  }
                ]
              },
              "Start": {
                "T": 0.0,
                "Alpha": 255,
                "Scale": [ 0.8, 0.8 ]
              },
              "End": {
                "T": 1.0,
                "Alpha": 255,
                "Scale": [ 0.8, 0.8 ]
              }
            },
            // 动作04
            {
              "Time": 0.3,
              "Curve": {
                "Type": "Line",
                "Ctrls": [
                  {
                    "X": -10,
                    "Y": -45
                  },
                  {
                    "X": -10,
                    "Y": -20
                  }
                ]
              },
              "Start": {
                "T": 0.0,
                "Alpha": 255,
                "Scale": [ 0.8, 0.8 ]
              },
              "End": {
                "T": 1.0,
                "Alpha": 0,
                "Scale": [ 0.8, 0.8 ]
              }
            }
          ]
        }
      ]
    },
 
    // RiseLetter_NormalAttackAP Group
    {
      "TemplateName": "RiseLetter_NormalAttackAP",
      "RandomRange": [ 100, 50 ],//随机范围: RangeX, RangeY;将实际的Anchor放在「以Anchor为中心,RandomRange范围的矩形内」
      "Anchor": [ 127.5, 12 ],
      "AddTimeLimit": 0.6, // 可累加的时间段限制
      "Layout": // Widget布局
      [
        // 数字
        {
          "Name": "Content",
          "Type": "TEXT",
          "Rect": [ 0, 0, 255, 24 ], // X, Y, Width, Height
          "Color": [ 123, 246, 255, 255 ], // rgba
          "Text": "12345",
          "Font": "物理",
          "Align": 1 // 0-左对齐, 1-居中, 2-右对齐
        }
      ],
      "AnimationSet": [// 动画列表(每次跳字事件发生 会随机选择一个播放)
        // 动画0
        {
          "Name": "01", // 名字可不设置
          "Sequence": // 每个动画由多个动作序列组成
          [
            // 动作01
            {
              "Time": 0.15,
              "Curve": {
                "Type": "Line", // 直线类型
                "Ctrls": [
                  {
                    "X": 6.2,
                    "Y": 14.3
                  },
                  {
                    "X": 6.2,
                    "Y": 14.3
                  }
                ] // 2个控制点(也就是直线的2个端点AB)
              },
              "Start": // 起点
              {
                "T": 0.0,
                "Alpha": 255,
                "Scale": [ 1.6, 1.6 ]
              },
              "End": // 终点
              {
                "T": 1.0,
                "Alpha": 255,
                "Scale": [ 1.6, 1.6 ]
              }
            },
            // 动作03
            {
            "Time": 0.3,
            "Curve": {
              "Type": "Line",
              "Ctrls": [
                {
                  "X": 6.2,
                  "Y": 14.3
                },
                {
                  "X": 6.2,
                  "Y": 14.3
                }
              ]
            },
            "Start": {
              "T": 0.0,
              "Alpha": 255,
              "Scale": [ 1.6, 1.6 ]
            },
            "End": {
              "T": 1.0,
              "Alpha": 255,
              "Scale": [ 1, 1 ]
            }
          },
            // 动作02
            {
              "Time": 0.8, // seconds.
              "Curve": {
                "Type": "Bezier",
                "Ctrls": [
                  {
                    "X": 6.2,
                    "Y": 14.3
                  },
                  {
                    "X": 28,
                    "Y": -15
                  },
                  {
                    "X": 32.5,
                    "Y": 30.0
                  },
                  {
                    "X": 29.5,
                    "Y": 58.5
                  }
                ]
              },
              "Start": {
                "T": 0.0,
                "Alpha": 255
              },
              "End": {
                "T": 1,
                "Alpha": 0
              }
            },
          ],
        },
        {
          "Name":"02", // 名字可不设置
          "Sequence": // 每个动画由多个动作序列组成
          [
            // 动作01
            {
              "Time": 0.15,
              "Curve": {
                "Type": "Line", // 直线类型
                "Ctrls": [
                  {
                    "X": 5,
                    "Y": 12
                  },
                  {
                    "X": 4,
                    "Y": 12
                  }
                ] // 2个控制点(也就是直线的2个端点AB)
              },
              "Start": // 起点
              {
                "T": 0.0,
                "Alpha": 255,
                "Scale": [ 1.5, 1.5 ]
              },
              "End": // 终点
              {
                "T": 1.0,
                "Alpha": 255,
                "Scale": [ 1.8, 1.8 ]
              }
            },
            // 动作03
            {
            "Time": 0.3,
            "Curve": {
              "Type": "Line",
              "Ctrls": [
                {
                  "X": 4,
                  "Y": 12
                },
                {
                  "X": 4,
                  "Y": 12
                }
              ]
            },
            "Start": {
              "T": 0.0,
              "Alpha": 255,
              "Scale": [ 1.8, 1.8 ]
            },
            "End": {
              "T": 1.0,
              "Alpha": 255,
              "Scale": [ 1.2, 1.2 ]
            }
            },
          // 动作02
          {
            "Time": 0.8, // seconds.
            "Curve": {
              "Type": "Bezier",
              "Ctrls": [
                {
                  "X": 4,
                  "Y": 12
                },
                {
                  "X": -36,
                  "Y": -18
                },
                {
                  "X": -32,
                  "Y": 39
                },
                {
                  "X": -9,
                  "Y": 56.5
                }
              ]
            },
            "Start": {
              "T": 0.0,
              "Alpha": 255
            },
            "End": {
              "T": 1,
              "Alpha": 0
            }
          }
        ]  
        }
      ]
    },
 
25
33
8