思考并回答以下问题:
在讲技能之前,先介绍一下游戏特效,游戏特效制作方式主要分为三种:序列帧特效、2D 骨骼动画特效、粒子特效。序列帧特效和 2D 骨骼动画特效大量运用在 2D 游戏中,而粒子特效主要是运用在 3D 游戏和 2D 游戏中。游戏特效可以提升整个游戏的画面品质,市场上的每款游戏都会有大量特效,可以让整个画面绚丽多彩。在游戏场景中,技能特效是伴随着角色动作播放的,角色动作可以使用我们前面介绍的角色系统创建,利用 FSM 播放动作,技能就是将动作和特效合在一起播放,在讲解技能之前,先给读者展示一下我们的技能模块设计图:
enter image description here
上图中,我们先介绍 IEffect 模块,所有的技能肯定有共同的属性,为了避免这些属性被重复的定义,我们将其放到一个父类中定义,在技能的父类就是 IEffect。大家跟着我的思路走,首先游戏中会有很多技能,这么多技能如何区分? 这就涉及到一个技能类型定义,技能类型定义可以使用字符串,也可以使用枚举。我们使用了枚举表示:1
2
3
4
5
6
7
8
9
10 public enum ESkillEffectType
{
eET_Passive,
eET_Buff,
eET_BeAttack,
eET_FlyEffect,
eET_Normal,
eET_Area,
eET_Link,
}
另外,技能还有一些共同的属性和方法,我们先定义属性,比如特效的运行时间、资源路径、生命周期、技能释放者和受击者、播放的音效等。这些我们可以自己根据需求去定义,代码如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//基本信息
public GameObject obj = null; //特效GameObject物体
public Transform mTransform = null;
protected float currentTime = 0.0f; //特效运行时间
public bool isDead = false; //特效是否死亡
public string resPath; //特效资源路径
public string templateName; //特效模板名称
public Int64 projectID = 0; //特效id 分为服务器创建id 和本地生成id
public uint skillID; //特效对应的技能id
public float cehuaTime = 0.0f; //特效运动持续时间或者是特效基于外部设置的时间 策划配置
public float artTime = 0.0f; //美术设置的特效时间 美术配置
public float lifeTime = 0; //特效生命周期, 0为无限生命周期
public UInt64 enOwnerKey; //技能释放者
public UInt64 enTargetKey; //技能受击者
public AudioSource mAudioSource = null; //声音
另外,特效属性有了,它播放后,朝着那个方向,以及发射位置,发射距离等等运动信息,这也需要我们去定义的,代码如下:1
2
3
4
5
6 //运动信息
public Vector3 fPosition;
public Vector3 fixPosition;
public Vector3 dir;
public Vector3 distance;
public ESkillEffectType mType;
特效的共同属性定义完了,下面定义它的共同方法,要使用特效,首先要创建特效,代码如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48//特效创建接口
public void Create()
{
//创建的时候检查特效projectId,服务器没有设置生成本地id
CheckProjectId();
//获取特效模板名称
templateName = ResourceCommon.getResourceName(resPath);
//使用特效缓存机制
obj = GameObjectPool.Instance.GetGO(resPath);
if (null == obj)
{
Debugger.LogError("load effect object failed in IEffect::Create" + resPath);
return;
}
//创建完成,修改特效名称,便于调试
obj.name = templateName + "_" + projectID.ToString();
OnLoadComplete();
//获取美术特效脚本信息
effectScript = obj.GetComponent<EffectScript>();
if (effectScript == null)
{
Debugger.LogError("cant not find the effect script in " + resPath);
return;
}
artTime = effectScript.lifeTime;
//美术配置时间为0,使用策划时间
if (effectScript.lifeTime == 0)
lifeTime = cehuaTime;
//否则使用美术时间
else
lifeTime = artTime;
//特效等级不同,重新设置
EffectLodLevel effectLevel = effectScript.lodLevel;
EffectLodLevel curLevel = EffectManager.Instance.mLodLevel;
if (effectLevel != curLevel)
{
//调整特效显示等级
AdjustEffectLodLevel(curLevel);
}
}
该函数的实现流程是先加载特效,也是从对象池生成,并且将其重新命名,然后调用函数 OnLoadComplete() 去设置特效的发射点,特效发射点要根据不同的技能去设置,在 IEffect 类中只是定义了一个虚函数:1
2
3 public virtual void OnLoadComplete()
{
}
具体的功能要在特效子类去实现,当然 IEffect 类并不是只有这一个函数实现,其他的功能函数可以根据需求自己定义了,如果没有可以不实现,比如函数 AdjustEffectLodLevel 用于设置特效等级,如果我们需求没有设置特效等级,那这个函数可以删除掉了。
特效父类 IEffect 已定义完成,接下来就要编写具体的子类了,上图图中列举了几个技能,根据需求可以扩展下去,我们先拿出 BeAttackEffect 被动技能举例说明,子类是如何编写的?在 IEffect 类的编写中,首先要继承 IEffect 类,在这里就略过去了,在父类 IEffect 中函数 OnLoadComplete 没有具体实现,只是提供了接口,子类重点就是实现这个函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public override void OnLoadComplete()
{
//判断enTarget
IEntity enTarget;
EntityManager.AllEntitys.TryGetValue(enTargetKey, out enTarget);
if (enTarget != null && obj != null)
{
//击中点
Transform hitpoit = enTarget.RealEntity.transform.FindChild("hitpoint");
if (hitpoit != null)
{
//设置父类和位置
GetTransform().parent = hitpoit;
GetTransform().localPosition = new Vector3(0.0f, 0.0f, 0.0f);
}
}
}
这个函数的功能是先在表里查找,如果找到了,则去查找对象的虚拟点,然后将该虚拟点设置给特效。为了搞清楚子类的编写,我们再举一个子类的实现 NormalEffect 正常特效,它也是继承自 IEffect 类,普通技能的 OnLoadComplete 函数实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public override void OnLoadComplete()
{
if (Target != null)
{
GetTransform().parent = Target.transform;
}
//无偏移位置,旋转
GetTransform().localPosition = new Vector3(0.0f, 0.0f, 0.0f);
GetTransform().localRotation = Quaternion.identity;
}
```
普通技能是设置技能释放的位置和旋转角度,是不是很简单?父类和子类设计完成,一个问题又摆在我们面前这么多技能,我们同样也需要一个管理器 EffectManager 类,用于对外提供创建特效接口,我们还是以 BeAttackEffect 类为例,我们要创建该特效,这个是在 EffectManager 类中实现的,代码实现函数如下:
```cs
public BeAttackEffect CreateTimeBasedEffect(string res, float time, IEntity entity)
{
if (res == "0")
return null;
BeAttackEffect effect = new BeAttackEffect();
//加载特效信息
effect.skillID = 0; //技能id=0
effect.cehuaTime = time;
effect.enTargetKey = entity.GameObjGUID;
effect.resPath = res;
//创建
effect.Create();
AddEffect(effect.projectID, effect);
return effect;
}
该函数功能首先将 BeAttackEffect 初始化,然后调用 Create 创建加载特效,已在上文实现过,为了便于管理我们调用了函数 AddEffect,目的是将特效加到表中,实现代码如下:1
2
3
4
5
6
7
8
9
10
11
12//添加特效到EffectMap表
public void AddEffect(Int64 id, IEffect effect)
{
if (!m_EffectMap.ContainsKey(id))
{
m_EffectMap.Add(id, effect);
}
else
{
Debug.LogError("the id: " + id.ToString() + "effect: " + effect.resPath + "has already exsited in EffectManager::AddEffect");
}
}
下面通过案例,给读者介绍如何使用?如果我们要创建 NormalEffect,只需要调用函数接口:1
NormalEffect absortSkillEffect = EffectManager.Instance.CreateNormalEffect(absortActPath, RealEntity.objAttackPoint.gameObject);
这样我们就创建了普通技能,当然它是配合角色动作一起创建的,角色在播放动作时释放技能,可以结合着我们的有限状态机一起使用,比如创建一个角色,释放技能效果,代码如下所示:
if (EntityManager.AllEntitys.TryGetValue(sGUID, out entity))
{
pos.y = entity.realObject.transform.position.y;
entity.EntityFSMChangeDataOnPrepareSkill(pos, dir, pMsg.skillid, target);
entity.OnFSMStateChange(EntityReleaseSkillFSM.Instance);
}
以上是创建角色,同时将角色的状态转换到释放技能动作,调用一个协成创建技能,代码如下:
IEffect effect = EffectManager.Instance.CreateNormalEffect(GameConstDefine.LoadGameSkillEffectPath + "release/" + skillconfig.effect, entitynpc.RealEntity.objPoint.gameObject);
将上述语句放到一个协成中,函数如下所示:1
2
3
4
5
6 IEnumerator ReleaseNormalSkill()
{
yield return 1;
IEffect effect = EffectManager.Instance.CreateNormalEffect(GameConstDefine.LoadGameSkillEffectPath + "release/" + skillconfig.effect, entitynpc.RealEntity.objPoint.gameObject);
}
然后在角色释放动作下面调用该函数即可:1
StartCoroutine(ReleaseNormalSkill())
在使用特效时,还有一个脚本需要注意,就是将 EffectScript 脚本挂到对象上面,EffectScript 主要是用于设置特效的生命周期,便于美术和策划调试,EffectScript 脚本代码实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 public class EffectScript : MonoBehaviour
{
/// <summary>
/// 特效生命周期
/// </summary>
public float lifeTime = 2.0f;
//特效显示等级
public EffectLodLevel lodLevel = EffectLodLevel.High;
// Use this for initialization
void Start()
{
//编辑器模式
if (Application.isEditor && !Application.isPlaying)
{
if (lifeTime == 0)
lifeTime = 10000000;
DestroyObject(gameObject, lifeTime);
}
}
}
再将该脚本挂接到对象上面,然后释放技能,实现效果如下所示: