思考并回答以下问题:
在讲技能之前,先介绍一下游戏特效,游戏特效制作方式主要分为三种:序列帧特效、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);
        }
    }
}
再将该脚本挂接到对象上面,然后释放技能,实现效果如下所示: