技能编辑器实现
不知道大家游戏的技能编辑器是怎么实现的呢,可能不同类型的游戏,技能编辑会有很大的不同吧。下面就介绍一个《龙之谷》手游作为一款经典的 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;
}
}