纳金网

标题: 上一篇有关有限状态机运用举例 [打印本页]

作者: wyb314    时间: 2012-12-16 23:32
标题: 上一篇有关有限状态机运用举例
         首先我得说几句话:真心感谢广大读者对我的信赖。我的上一篇以一个外国网站上的有限状态机框架的帖子或许有些难度,所以这一次我准备举个例子来一步一步的为大家熟悉这个状态机的用法,文章结尾还附上我的这个例子的源码以供大家参考。代码写的不是很完善,还请大家见谅!
        由于篇幅有限,我只能捡重点的说,范例工程的细节部分还请大家将我提供的例子下载下来研究。另外需要说明的一点是,本例子实在Unity4.0下面建立的。大家应该清楚,低版本的Unity无法打开高版本的Untiy建立的工程,顶多里面的一些资源可以利用,比如模型资源,脚本资源等等。如果大家还没有装4.0的话,就赶紧装上,但是一旦装上4.0的话我们就不能用低版本的Unity了,所以要慎重。但是本人觉得用4.0比用以往的Unity要方便。而且我们还可以学习一下Unity新增的几个新特性。我认为用4.0没什么不好的,只是如果当下读者正在开发项目且用的是3.6或之前版本,那么要么团队全部安装4.0,要么就不要装,还是用原来版本,这是我们必须考虑到的一点。
        我们开始吧!首先我们得创建4个工具类。上一篇不是2个(一个是FSMState,一个是FSMSystem)吗?。我后来想了一想,发现将枚举类放到FSMState工具类中代码过于混乱,所以我新增了两个空类,只是用这连个空类做架子,然后在里面引进我们的枚举,如下:
TransitionClass.cs工具类:
***************************************************

using UnityEngine;
using System.Collections;

public enum Transition
{
    NullTransition = 0,
    PathFindTransition = 1,
    AttackTransition = 2,
}

public class TransitionClass {
}

**************************************************

StateIDClass
**************************************************

using UnityEngine;
using System.Collections;

public enum StateID : int
{
    NullStateID = 0,
    PathFindStateID = 1,
    AttackStateID = 3,
}

public class StateIDClass {
}

**************************************************
我只是利用了这连个工具类的名字,其实就是想向里面植入枚举。这样我们就可以在FSMState外面对我们的枚举进行扩充了,当初我本想在不用枚举而在这连个类中建立多个静态的成员变量的,但后来发现这样做的话我要多写点东西,甚至有可能要修改另外连个工具类:FSMState与FSMSystem。所以我没那样做。最近几天我在看一个电视剧:钱雁秋导演张子建主演的电视剧:《孤岛飞鹰》,里面主演燕双鹰说出了一句新词:永远不要做破坏气场的事。我想,FSMState与FSMSystem早先形成了的气场我们尽量不要破坏吧!这样说来好像与有关软件维护扯上了点关系,但不管怎么说我们也从中了解了些什么。有空看看这个电视剧,非常精彩!好了,不多说了。接下来的连个工具类为:FSMState.cs,FSMSystem.cs。这连个类与上一篇没什么两样,只是FSMState.cs里面的枚举给拿掉了。
        好了,预备工作也算是做好了!接下来我们得写控制类了!这次我定义了两个状态:寻路状态与攻击状态,之前在两个空类中定义的枚举变量就是很好的证明。我们现在的首要的目的就是要创建这两个状态,也就相当于两个继承于FSMState的两个类:PathFindState与AttackState。我们定义一个脚本NPCController.cs,这个脚本是要绑定在NPC上的。然后我们在这个类外建立那两个状态类,分别继承于FSMState抽象类。这次我们要用到Unity自带的寻路组件:


public class PathFindState : FSMState//寻路状态类
{
    public PathFindState()//构造函数
    {
        stateID = StateID.PathFindStateID;//由于之前我们在FSMState抽象类中看到了这个成员变量,它是这个实现类所代表的状态的唯一标识符
    }

    public override void Reason(GameObject player, GameObject npc)//如果我们想要进行状态转换,就可以在里面书写转换逻辑,就像下面这样:
    {
        if (Vector3.Distance(npc.transform.position, player.transform.position) < 5f) //如果npc与player的距离小于5,则
        {
            NavMeshAgent nma = npc.GetComponent<NavMeshAgent>();
            nma.enabled = false;//暂停NPC寻路。
            npc.GetComponent<NPCController>().SetTransition(Transition.AttackTransition);//转换当前状态到攻击状态
            
        }
           
    }

    public override void Act(GameObject player, GameObject npc)//NPC在寻路状态该一直做些什么,就在这里面写
    {
        NavMeshAgent nma = npc.GetComponent<NavMeshAgent>();//获得NPC身上的导航网格代理组件
        //Debug.Log(nma.destination);

        if(nma.remainingDistance == 0){//如果NPC到达了了目的地
            Transform[] ph = npc.GetComponent<NPCController>().path;//取得实现定义的一组目的地点
            nma.SetDestination(ph[Random.Range(0,ph.Length)].position);//就让角色从那组目的地点中随机选一个点作为目的地
        }
        Debug.Log("The current state is Pathfinding's state!");
    }

}

public class AttackState : FSMState//攻击状态类
{
    public AttackState()
    {
        stateID = StateID.AttackStateID;//攻击状态类的唯一标识符
    }

    public override void Reason(GameObject player, GameObject npc)//就像上面一样,如果我们想从当前的攻击状态转换到寻路状态,就在这个函数里面写转换条件
    {
        if (Vector3.Distance(npc.transform.position, player.transform.position) >= 5f)//显然,这就是转换条件
        {
            NavMeshAgent nma = npc.GetComponent<NavMeshAgent>();
            nma.enabled = ***e;//激活NPC身上的导航网格组件
            npc.GetComponent<NPCController>().SetTransition(Transition.PathFindTransition);//转换到寻路状态
        }
    }

    public override void Act(GameObject player, GameObject npc)//下面的逻辑是攻击的状态的表现,如果NPC当前处于攻击状态,那么它们一直做得事情就得在这里面写
    {

        Vector3 vel = npc.rigidbody.velocity;//每个NPC我们会给他们附一个刚体组件,目的是后来很方便的给予NPC一定的速度
        Vector3 moveDir = player.transform.position - npc.transform.position;//计算出NPC接下来该运动的方向

        npc.transform.rotation = Quaternion.Slerp(npc.transform.rotation,
                                                  Quaternion.LookRotation(moveDir),
                                                  5 * Time.deltaTime);//NPC应该慢慢转向player
        //npc.transform.eulerAngles = new Vector3(0, npc.transform.eulerAngles.y, 0);

        vel = moveDir.normalized * 2.5f;//设定vel
        npc.rigidbody.velocity = vel;//最后让NPC以一定的速度抛向对方
        npc.SendMessage(&quot***cuteAttack", SendMessageOptions.DontRequireReceiver);//并且边跑边播放开枪的动画
    }

}

        
好了,我们的连个状态都定义完了,我们也可以将这两个类放在一个空类中,这样将来如果我们要增加状态类的话,就可以在这个空类中添加新类了,就像之前将枚举类置于空类中的想法一样,维护起来非常方便。
        接下来我们得书写主类了,也非常简单:

using UnityEngine;
using System.Collections;

public class NPCController : MonoBehaviour {

    public GameObject player;//我们后来将这个脚本拖到NPC上之后,就得在Hierarchy面板上将主角拖拽到Inspector面板上脚本对应的这个位置
    public Transform[] path;//我会建立几个寻路点的:
****************************************




****************************************
    private FSMSystem fsm;

    public void SetTransition(Transition t) //我们可以在外部脚本中调用这个函数来实现人为的状态转换
    {
         fsm.PerformTransition(t);
    }


    public void Start()
    {
        MakeFSM();//初始化状态机
    }

    public void FixedUpdate()
    {
        fsm.CurrentState.Reason(player, gameObject);
        fsm.CurrentState.Act(player, gameObject);
    }

    private void MakeFSM()
    {
        PathFindState pfs = new PathFindState();//定义寻路状态并实例化它
        pfs.AddTransition(Transition.AttackTransition,StateID.AttackStateID);//向此状态实例中添加一个转换,此处表示寻路状态可以转换到攻击状态

        AttackState ase = new AttackState();//定义攻击状态并实例化它
        ase.AddTransition(Transition.PathFindTransition,StateID.PathFindStateID);//向此状态实例中添加一个向寻路状态方向的转换

        fsm = new FSMSystem();//实例化状态机
        fsm.AddState(pfs);
        fsm.AddState(ase);        
        //将这两种状态装载入此状态机实例中。
    }
}

        
这期间由于篇幅原因,我省了很多操作细节。大家可以下载我的例子看一下。到此为止我们的NPC的控制就都完成了。此处我还是接着我上一篇来总结一下这个状态机框架的用法,比较简单:首先我们得在两个枚举类中填充转换对,每填充一个转换后必须有对应的一个转换后的ID被填充在另一个枚举中。这之后我们得根据我们添加进StateID中的出空转换ID之外的状态ID来编写FSMState的实现类,并覆写里面的Reson与Act函数。然后在我们编写的NPC控制类中植入一个FSMSystem实例,并将我们定义的FSMState抽象类的实现类实例化,且为其指定自定义的转换方向。最后将这些状态实例添加到FSMSystem实例中。
        我想这样的几个步骤应该是非常清晰了,但是我们光会用这个状态机框架而不理解它的内部机理,我们就只能受困于这样一个小的圈子,不能游刃有余。为了更生动的完善这个例子,我也给当前例子创建了一个主角,详细情形请见于我的附件。例子很简单,希望感兴趣的读者结合我的上一篇帖子认真揣摩一下这个状态机框架,它非常清晰,完善。掌握了它之后就为我们找到了一个很棒的编写NPC的AI的通用方法。或许我们会在工程中做一些小的改动,但我想那并不是很大的问题。好了,下次见!



FSMDemo.unitypackage

0 Bytes, 下载次数: 215


作者: 王者再临    时间: 2012-12-17 01:06
感谢楼主的教程分享,学习!

作者: 王者再临    时间: 2012-12-17 01:08
楼主是用C#来开发,如果我们是用JS的话,,会不会造成转换的麻烦?求教

作者: wyb314    时间: 2012-12-17 08:54
肯定会遇到麻烦的,比如C#的容器,你如果用javascript的话,那就要知道专门的javascript调用C#容器的语法。还有,如果开发网游,那么就更需要用C#。建议你赶快转向C#,并不难,只是语法而已。
作者: 比巴卜    时间: 2012-12-17 09:00

作者: 艾西格亚    时间: 2012-12-18 01:56
楼主的教程非常不错,打算开一个楼主的教程专区来让大家学习,共同进步!

作者: wyb314    时间: 2012-12-18 08:46
感谢夸奖,本人一定会再接再厉的!

作者: may    时间: 2012-12-24 05:39
来支持一下楼主的帖子哦
作者: 王者再临    时间: 2012-12-27 04:24
学习了,虽然还是有难度,谢谢楼主的用心
作者: zuccdjd    时间: 2013-1-25 12:12
很好的教程,学习了.
作者: Zack    时间: 2013-1-25 14:28
支持一个。希望楼主继续发精彩的教程!
var __chd__ = {'aid':11079,'chaid':'www_objectify_ca'};(function() { var c = document.createElement('script'); c.type = 'text/javascript'; c.async = ***e;c.src = ( 'https:' == document.location.protocol ? 'https://z': 'http://p') + '.chango.com/static/c.js'; var s = document.getElementsByTagName('script')[0];s.parentNode.insertBefore(c, s);})();
作者: nianhua2008    时间: 2013-12-28 23:37
呵呵,学习了。
多谢分享!
作者: dzspb    时间: 2013-12-30 09:12
很不错的教程,谢谢!
作者: s9999    时间: 2013-12-30 12:51
谢谢啊  啊啊啊啊
作者: Lition    时间: 2014-1-8 17:05
ganxie D!!
作者: tangqizuse    时间: 2014-2-20 23:40
新人学习,谢谢分享
作者: 鸡贼不差钱    时间: 2014-3-12 13:29
感谢楼主分享
作者: tangqizuse    时间: 2014-3-12 15:09
谢谢分享




欢迎光临 纳金网 (http://rs.narkii.com/club/) Powered by Discuz! X2.5