不知道大家游戏的技能编辑器是怎么实现的呢,可能不同类型的游戏,技能编辑会有很大的不同吧。下面就介绍一个《龙之谷》手游作为一款经典的 ARPG 游戏,技能编辑器是如何实现的。

技能编辑器界面预览

在unity对应的编辑器

数据保存格式-xml

龙之谷的技能编辑生成的中间数据是 xml,用来供游戏逻辑来读取,进一步实现相关的逻辑。客户端使用System.Xml.Serialization.XmlSerializer序列化解析xml,服务器使用tinyxml来解析xml

技能编辑是基于帧率的,游戏锁定在30帧。 技能编辑器总体设置镜头FOV, 配置文件的物理位置, 技能的长度, 技能使用的animation动画等。

龙之谷的技能类型由三种类型:JA(普通技能), Arts(大招),Combine(组合技能) ,下面主要介绍Arts类型的技能为切入点,介绍龙之谷技能的实现方式。

龙之谷技能编辑器的实现是基于Unity Editor扩展实现的。策划在编辑器里编辑,编辑好之后保存成xml格式。 运行时读取xml。 运行时又区分在unity里预览编辑器里效果和游戏实际运行的效果。

下面的配置是基于帧率实现的:

Result 技能作用效果

配置打击点 子弹效果

伤害范围一共分为两种Sector Damage勾选上表示扇形(配置360度就是圆形了)和方形
扇形和方形的范围计算参照:

if (CurrentSkillData.Result[nHotID].Sector_Type)  //扇形
{
    float m_Theta = 0.01f;
    Vector3 beginPoint = Vector3.zero;
    Vector3 firstPoint = Vector3.zero;

    for (float theta = 0; theta < 2 * Mathf.PI; theta += m_Theta)
    {
        float x = CurrentSkillData.Result[nHotID].Range / ShownTransform.localScale.y * Mathf.Cos(theta);
        float z = CurrentSkillData.Result[nHotID].Range / ShownTransform.localScale.y * Mathf.Sin(theta);
        Vector3 endPoint = new Vector3(x, 0, z);
        if (theta == 0) firstPoint = endPoint;
        else Gizmos.DrawLine(beginPoint, endPoint);
        beginPoint = endPoint;
    }
    Gizmos.DrawLine(firstPoint, beginPoint);
    if (CurrentSkillData.Result[nHotID].Low_Range > 0)
    {
        m_Theta = 0.01f;
        beginPoint = Vector3.zero;
        firstPoint = Vector3.zero;
        for (float theta = 0; theta < 2 * Mathf.PI; theta += m_Theta)
        {
            float x = CurrentSkillData.Result[nHotID].Range / ShownTransform.localScale.y * Mathf.Cos(theta);
            float z = CurrentSkillData.Result[nHotID].Range / ShownTransform.localScale.y * Mathf.Sin(theta);
            Vector3 endPoint = new Vector3(x, 0, z);
            if (theta == 0)  firstPoint = endPoint;
            else  Gizmos.DrawLine(beginPoint, endPoint);
            beginPoint = endPoint;
        }
        Gizmos.DrawLine(firstPoint, beginPoint);
    }
}
else  //方形
{
    Vector3 fr = new Vector3(CurrentSkillData.Result[nHotID].Scope / 2.0f, 0, CurrentSkillData.Result[nHotID].Range / 2.0f);
    Vector3 fl = new Vector3(CurrentSkillData.Result[nHotID].Scope / 2.0f, 0, CurrentSkillData.Result[nHotID].Rect_HalfEffect ? 0 : (-CurrentSkillData.Result[nHotID].Range / 2.0f));
    Vector3 br = new Vector3(-CurrentSkillData.Result[nHotID].Scope / 2.0f, 0, CurrentSkillData.Result[nHotID].Range / 2.0f);
    Vector3 bl = new Vector3(-CurrentSkillData.Result[nHotID].Scope / 2.0f, 0, CurrentSkillData.Result[nHotID].Rect_HalfEffect ? 0 : (-CurrentSkillData.Result[nHotID].Range / 2.0f));

    Gizmos.DrawLine(fr, fl);
    Gizmos.DrawLine(fl, bl);
    Gizmos.DrawLine(bl, br);
    Gizmos.DrawLine(br, fr);
}

Hit 配置技能的打击效果

可以在hit Dummy 设置一个被打击的对象
type是被打击的效果
还可以配置一些打击使用的特效

FMOd 技能使用的音效

配置基本上播放其实帧数 还有就是fmod需要的一些参数如路径、还有频道

Fx 配置技能在某帧播放的特效

配置帧的起始位置,特效对应的prefab 地址等相关的属性
编辑器里还支持跟随效果、延时播放 transform 缩放和postion偏移

对应的代码实现

protected override void OnInnerGUI()
{
    for (int i = 0; i < Hoster.SkillData.Fx.Count; i++)
    {
        Hoster.SkillData.Fx[i].Combined = (Hoster.SkillData.TypeToken == 2);
        Hoster.SkillData.Fx[i].Index = i;
        EditorGUILayout.BeginHorizontal();
        Hoster.SkillData.Fx[i].Type = (SkillFxType)EditorGUILayout.EnumPopup("Type Based on", Hoster.SkillData.Fx[i].Type);
        if (GUILayout.Button(_content_remove, GUILayout.MaxWidth(30)))
        {
            Hoster.SkillData.Fx.RemoveAt(i);
            Hoster.SkillDataExtra.Fx.RemoveAt(i);
            EditorGUILayout.EndHorizontal();
            continue;
        }
        EditorGUILayout.EndHorizontal();

        Hoster.SkillDataExtra.Fx[i].Fx = EditorGUILayout.ObjectField("Fx Object", Hoster.SkillDataExtra.Fx[i].Fx, typeof(GameObject), true) as GameObject;
        if (null == Hoster.SkillDataExtra.Fx[i].Fx || !AssetDatabase.GetAssetPath(Hoster.SkillDataExtra.Fx[i].Fx).Contains("Resources/Effects/"))
        {
            Hoster.SkillDataExtra.Fx[i].Fx = null;
        }
        if (null != Hoster.SkillDataExtra.Fx[i].Fx)
        {
            string path = AssetDatabase.GetAssetPath(Hoster.SkillDataExtra.Fx[i].Fx).Remove(0, 17);
            Hoster.SkillData.Fx[i].Fx = path.Remove(path.LastIndexOf('.'));
            EditorGUILayout.LabelField("Fx Name", Hoster.SkillData.Fx[i].Fx);

            EditorGUILayout.Space();
            Vector3 vec = new Vector3(Hoster.SkillData.Fx[i].ScaleX, Hoster.SkillData.Fx[i].ScaleY, Hoster.SkillData.Fx[i].ScaleZ);
            vec = EditorGUILayout.Vector3Field("Scale", vec);
            Hoster.SkillData.Fx[i].ScaleX = vec.x;
            Hoster.SkillData.Fx[i].ScaleY = vec.y;
            Hoster.SkillData.Fx[i].ScaleZ = vec.z;
            vec.Set(Hoster.SkillData.Fx[i].OffsetX, Hoster.SkillData.Fx[i].OffsetY, Hoster.SkillData.Fx[i].OffsetZ);
            vec = EditorGUILayout.Vector3Field("Offset", vec);
            Hoster.SkillData.Fx[i].OffsetX = vec.x;
            Hoster.SkillData.Fx[i].OffsetY = vec.y;
            Hoster.SkillData.Fx[i].OffsetZ = vec.z;

            EditorGUILayout.Space();
            float fx_at = (Hoster.SkillData.Fx[i].At / XSkillInspector.frame);
            EditorGUILayout.BeginHorizontal();
            fx_at = EditorGUILayout.FloatField("Play At", fx_at);
            GUILayout.Label("(frame)");
            GUILayout.Label("", GUILayout.MaxWidth(30));
            EditorGUILayout.EndHorizontal();

            Hoster.SkillDataExtra.Fx[i].Ratio = fx_at / Hoster.SkillDataExtra.SkillClip_Frame;
            if (Hoster.SkillDataExtra.Fx[i].Ratio > 1) Hoster.SkillDataExtra.Fx[i].Ratio = 1;

            if (Hoster.SkillData.Fx[i].End < 0) Hoster.SkillData.Fx[i].End = Hoster.SkillDataExtra.SkillClip_Frame * XSkillInspector.frame;
            float fx_end_at = (Hoster.SkillData.Fx[i].End / XSkillInspector.frame);
            EditorGUILayout.BeginHorizontal();
            fx_end_at = EditorGUILayout.FloatField("End At", fx_end_at);
            GUILayout.Label("(frame)");
            GUILayout.Label("", GUILayout.MaxWidth(30));
            EditorGUILayout.EndHorizontal();

            if (Hoster.SkillData.Fx[i].Type == SkillFxType.FirerBased)
                Hoster.SkillData.Fx[i].Follow = EditorGUILayout.Toggle("Follow", Hoster.SkillData.Fx[i].Follow);
            else
                Hoster.SkillData.Fx[i].Follow = false;
            EditorGUILayout.Space();
            EditorGUILayout.BeginHorizontal();
            Hoster.SkillData.Fx[i].Destroy_Delay = EditorGUILayout.FloatField("Delay Destroy", Hoster.SkillData.Fx[i].Destroy_Delay);
            GUILayout.Label("(s)");
            EditorGUILayout.EndHorizontal();
            Hoster.SkillData.Fx[i].Shield = EditorGUILayout.Toggle("Shield", Hoster.SkillData.Fx[i].Shield);
        }
        else
        {
            Hoster.SkillData.Fx[i].Fx = null;
        }
    }
}

运行时有一个timer,在指定帧触发回调,不同的逻辑到通过虚函数分发到不同的脚本。

void LateUpdate()
{
    if (_attribute != null) _attribute.UpdateRotation();
    //trigger 在技能触发的时候会赋值
    if (!string.IsNullOrEmpty(trigger) && ator != null && !ator.IsInTransition(0))
    {
        if (trigger != AnimTriger.ToStand &&
            trigger != AnimTriger.ToMove &&
            trigger != AnimTriger.EndSkill)
            //casting
            for (int i = 0, max = skills.Count; i < max; i++)
            {
                skills[i].Execute();
            }

        ator.speed = 1;
        ator.SetTrigger(trigger);
        trigger = null;
    }
}