Unity游戏开发之手把手教你实战剧情对话

爱上游戏开发

共 33687字,需浏览 68分钟

 ·

2021-05-09 19:15

作者: 宏哥1995

来源: https://blog.csdn.net/lyh916/article/details/45126317

尝试

在网上看到了一篇不错的文章,讲的是游戏中的剧情动画,感觉去做一做也是挺好玩的事,于是就有了这篇文章。游戏中的剧情(非CG动画)主要有两种,一种是自动播放的,另一种是含有对话的。可以把剧情中的一个个动画(这里的动画不仅仅包含角色动画,还包含位移,旋转,时间等待等等)当成一种种状态(有点像状态机),放到一个链表或者队列中,每当一个状态完成时便跳到下一个状态,于是就会形成连环的剧情动画了。

要注意的是,在普通状态下,一个动作完成会跳到下一个动作,但是在对话状态下,一个动作完成时不会自动跳到下一个动作,而是每出现一个新的对话,就会触发动作。

在这里给出核心代码(一个动作基类以及一个管理动作的类):

using UnityEngine;
using System.Collections;
 
public class Command {
 
    public virtual void Execute() { }
    public void Next() 
    {
        CommandManager.instance.Next2();
    } 
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
public class CommandManager : MonoBehaviour {
 
    public static CommandManager instance;
 
    private List<Command> commands = new List<Command>();
    private int nowIndex = 0;
    public bool isDialogueMode = false;
 
    //缓存Transform
    [SerializeField]
    public List<string> startObj = new List<string>();//一开始就存在的物体
    public Dictionary<string,Transform> allObj = new Dictionary<string,Transform>();
 
    void Awake()
    {
        instance = this;
    }
 
    void Start()
    {
        for (int i = 0; i < startObj.Capacity;i++ )
            allObj.Add(startObj[i], GameObject.Find(startObj[i]).transform);
 
        commands.Add(new CommandMusic("Taylor Swift - Red"));
 
        //场景一 对话驱动画面
        commands.Add(new CommandDialogueModeEnter());
 
        commands.Add(new CommandDialogue("Story1", 0, 0, 1));
        commands.Add(new CommandMove("Main Camera", new Vector3(-3.5f, 3.4f, -5.5f), 6f));
 
        commands.Add(new CommandDialogue("Story1", 1, 1, 2));
        commands.Add(new CommandMove("Main Camera", new Vector3(-4.8f, 3.4f, -4.8f), 0f));
        commands.Add(new CommandMove("Main Camera", new Vector3(-2.5f, 3.4f, -4.8f), 5f));
 
        commands.Add(new CommandDialogue("Story1", 2, 2, 3));
        commands.Add(new CommandRotate("Main Camera", new Vector3(10f, 180f, 0f), 0f));
        commands.Add(new CommandMove("Main Camera", new Vector3(-2.5f, 3.4f, -1f), 0f));
        commands.Add(new CommandMove("Main Camera", new Vector3(-4.6f, 3.4f, -1f), 5f));
 
        commands.Add(new CommandDialogueModeExit());
 
        //场景二 自动
        commands.Add(new CommandMove("Main Camera", new Vector3(-20.5f, 5.8f, -7.9f), 0f));
        commands.Add(new CommandRotate("Main Camera", new Vector3(12f, 180f, 0f), 0f));
 
        commands.Add(new CommandWait(2f));
        commands.Add(new CommandPlayAnim("skeleton_warrior1", AnimatorManager.instance.run));
        commands.Add(new CommandPlayAnim("skeleton_warrior2", AnimatorManager.instance.run));
        commands.Add(new CommandPlayAnim("skeleton_warrior3", AnimatorManager.instance.run));
        commands.Add(new CommandPlayAnim("skeleton_warrior4", AnimatorManager.instance.run));
        commands.Add(new CommandPlayAnim("skeleton_warrior5", AnimatorManager.instance.run));
 
        commands.Add(new CommandWait(2f));
        commands.Add(new CommandPlayAnim("magic", AnimatorManager.instance.attack1));
 
        commands.Add(new CommandWait(1f));
        commands.Add(new CommandPlayAnim("skeleton_warrior1", AnimatorManager.instance.dead));
        commands.Add(new CommandPlayAnim("skeleton_warrior2", AnimatorManager.instance.dead));
        commands.Add(new CommandPlayAnim("skeleton_warrior3", AnimatorManager.instance.dead));
        commands.Add(new CommandPlayAnim("skeleton_warrior4", AnimatorManager.instance.dead));
        commands.Add(new CommandPlayAnim("skeleton_warrior5", AnimatorManager.instance.dead));
 
        commands.Add(new CommandPlayAnim("magic", AnimatorManager.instance.stand));
 
        commands.Add(new CommandDialogue("Story1", 3, 5, 0));
 
        commands.Add(new CommandWait(1f));
    }
 
 
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
            commands[nowIndex].Execute();
    }
 
    //对话模式下直接进入下一状态
    public void Next()
    {
        nowIndex++;
        if (nowIndex < commands.Count)
            commands[nowIndex].Execute();
    }
 
    //自动模式下直接进入下一状态
    public void Next2()
    {
        if (isDialogueMode)
            return;
        else
            Next();
    }
}

只需要根据自身的需要,写一些继承Command的类即可达到想要的效果了。以下是本人自己扩展的类:

效果图:

完整视频链接:http://v.youku.com/v_show/id_XOTMxMjIyMDU2.html

大约两分钟,建议全屏模式下播放(因为不太清晰),自认为剧情还是挺好的。。。

一个更好的方法是写一个编辑器类,将配置保存下来(保存在xml或者json文件),每次运行时读取。不过这里先放一放,最近好忙啊(因为课程的关系,大二下比之前都要忙啊),也不知道下一次写博客是什么时候!哎!

优化

上面讲到我们在CommandManager中直接写入一个个的指令,实际上这样是很不好的,因为一个游戏可能会出现很多的剧情动画,那么不就是要写n个类吗?而且修改起来也不是很好的。所以一个比较好的方法就是写一个编辑器类。

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
 
public enum Option
{
    CommandDestory,
    CommandDialogue,
    CommandDialogueModeEnter,
    CommandDialogueModeExit,
    CommandGenerate,
    CommandMove,
    CommandMusic,
    CommandPlayAnim,
    CommandRotate,
    CommandWait
}
 
public class Window : EditorWindow
{
    List<Option> options = new List<Option>();
    List<string[]> strings = new List<string[]>();
    List<int[]> ints = new List<int[]>();
    List<float[]> floats = new List<float[]>();
    List<Vector3[]> vector3s = new List<Vector3[]>();
 
    Vector2 v2 = new Vector2(0, 0);//滚动条参数
 
    [MenuItem("Window/My Window")]
    static void Init()
    {
        EditorWindow.GetWindow(typeof(Window));
    }
 
    void OnGUI()
    {
        if (GUILayout.Button("保存命令"))
        { 
        }
 
        if (GUILayout.Button("添加命令"))
        {
            Option x = Option.CommandDialogueModeEnter;
            options.Add(x);
            string[] a = new string[5];
            strings.Add(a);
            int[] b = new int[5];
            ints.Add(b);
            float[] c = new float[5];
            floats.Add(c);
            Vector3[] d = new Vector3[5];
            vector3s.Add(d);
        }
 
        if (GUILayout.Button("删除命令"))
        {
            options.RemoveAt(options.Count - 1);
            strings.RemoveAt(options.Count - 1);
            ints.RemoveAt(options.Count - 1);
            floats.RemoveAt(options.Count - 1);
            vector3s.RemoveAt(options.Count - 1);
        }
 
        v2 = EditorGUILayout.BeginScrollView(v2,false,true,null);
        for (int i = 0; i < options.Count; i++)
        {
            options[i] = (Option)EditorGUILayout.EnumPopup("选项" + i, options[i]);
            switch (options[i])
            {
                case Option.CommandDestory:
                    strings[i][0] = EditorGUILayout.TextField("物体名字", strings[i][0]);
                    break;
                case Option.CommandDialogue:
                    strings[i][0] = EditorGUILayout.TextField("xml文本名字", strings[i][0]);
                    ints[i][0] = EditorGUILayout.IntField("开始序号", ints[i][0]);
                    ints[i][1] = EditorGUILayout.IntField("结束序号", ints[i][1]);
                    ints[i][2] = EditorGUILayout.IntField("点击后执行的步骤数", ints[i][2]);
                    break;
                case Option.CommandDialogueModeEnter:
                    break;
                case Option.CommandDialogueModeExit:
                    break;
                case Option.CommandGenerate:
                    strings[i][0] = EditorGUILayout.TextField("物体名字", strings[i][0]);
                    vector3s[i][0] = EditorGUILayout.Vector3Field("生成位置", vector3s[i][0]);
                    vector3s[i][1] = EditorGUILayout.Vector3Field("生成角度", vector3s[i][1]);
                    break;
                case Option.CommandMove:
                    strings[i][0] = EditorGUILayout.TextField("物体名字", strings[i][0]);
                    vector3s[i][0] = EditorGUILayout.Vector3Field("终点位置", vector3s[i][0]);
                    floats[i][0] = EditorGUILayout.FloatField("所用时间", floats[i][0]);
                    break;
                case Option.CommandMusic:
                    strings[i][0] = EditorGUILayout.TextField("音乐名字", strings[i][0]);
                    break;
                case Option.CommandPlayAnim:
                    strings[i][0] = EditorGUILayout.TextField("物体名字", strings[i][0]);
                    ints[i][0] = EditorGUILayout.IntField("动画状态", ints[i][0]);
                    break;
                case Option.CommandRotate:
                    strings[i][0] = EditorGUILayout.TextField("物体名字", strings[i][0]);
                    vector3s[i][0] = EditorGUILayout.Vector3Field("终点角度", vector3s[i][0]);
                    floats[i][0] = EditorGUILayout.FloatField("所用时间", floats[i][0]);
                    break;
                case Option.CommandWait:
                    floats[i][0] = EditorGUILayout.FloatField("等待时间", floats[i][0]);
                    break;
                default:
                    break;
            }
        }
        EditorGUILayout.EndScrollView();
    }
}

最后

在这里,我们使用xml来实现保存与读取功能。首先,是头文件的引用。微信搜索公众号 [爱上游戏开发],回复 “资料”,免费领取 200G 学习资料

using System.Xml;

保存到xml文件:

void Save()
    {
        string filePath = Application.dataPath + @"/my.xml";
 
        XmlDocument xmlDoc = new XmlDocument();
        XmlElement root = xmlDoc.CreateElement("root");
 
        for (int i = 0; i < options.Count; i++)
        {
            XmlElement child = xmlDoc.CreateElement("child");
            XmlElement x = xmlDoc.CreateElement("type");
            x.InnerText = options[i].ToString();
            child.AppendChild(x);
 
            int count = 1;
            for (int q = 0; q < strings[q].Length; q++)
            {
                XmlElement q1 = xmlDoc.CreateElement("string_" + count);
                q1.InnerText = strings[i][q];
                child.AppendChild(q1);
                count++;
            }
 
            for (int q = 0; q < ints[q].Length; q++)
            {
                XmlElement q1 = xmlDoc.CreateElement("int_" + count);
                q1.InnerText = ints[i][q].ToString();
                child.AppendChild(q1);
                count++;
            }
 
            for (int q = 0; q < floats[q].Length; q++)
            {
                XmlElement q1 = xmlDoc.CreateElement("float_" + count);
                q1.InnerText = floats[i][q].ToString();
                child.AppendChild(q1);
                count++;
            }
 
            for (int q = 0; q < vector3s[q].Length; q++)
            {
                XmlElement q1 = xmlDoc.CreateElement("vector3_" + count);
                q1.InnerText = vector3s[i][q].ToString();
                child.AppendChild(q1);
                count++;
            }
            root.AppendChild(child);
        }
 
        xmlDoc.AppendChild(root);
        xmlDoc.Save(filePath);
        AssetDatabase.Refresh();
        Debug.Log("Ok!!");
    }

读取xml文件:

void Load(string xmlPath)
    {
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(xmlPath);
 
        XmlNodeList childs = xmlDoc.SelectSingleNode("root").ChildNodes;//所有child
        for (int i = 0; i < childs.Count; i++)
        {
            XmlNodeList nodes = childs[i].ChildNodes;//单个child的所有节点
            
            string a = nodes[0].InnerText;
            switch (a)
            {
                case "CommandDestory":
                    commands.Add(new CommandDestory(nodes[1].InnerText));
                    break;
                case "CommandDialogue":
                    int int1 = int.Parse(nodes[6].InnerText);
                    int int2 = int.Parse(nodes[7].InnerText);
                    int int3 = int.Parse(nodes[8].InnerText);
                    commands.Add(new CommandDialogue(nodes[1].InnerText, int1, int2, int3));
                    break;
                case "CommandDialogueModeEnter":
                    commands.Add(new CommandDialogueModeEnter());
                    break;
                case "CommandDialogueModeExit":
                    commands.Add(new CommandDialogueModeExit());
                    break;
                case "CommandGenerate":
                    Vector3 v1 = StringToVector3(nodes[16].InnerText);
                    Vector3 v2 = StringToVector3(nodes[17].InnerText);
                    commands.Add(new CommandGenerate(nodes[1].InnerText,v1,v2));
                    break;
                case "CommandMove":
                    Vector3 v = StringToVector3(nodes[16].InnerText);
                    commands.Add(new CommandMove(nodes[1].InnerText, v, float.Parse(nodes[11].InnerText)));
                    break;
                case "CommandMusic":
                    commands.Add(new CommandMusic(nodes[1].InnerText));
                    break;
                case "CommandPlayAnim":
                    commands.Add(new CommandPlayAnim(nodes[1].InnerText, int.Parse(nodes[6].InnerText)));
                    break;
                case "CommandRotate":
                    Vector3 v3 = StringToVector3(nodes[16].InnerText);
                    commands.Add(new CommandRotate(nodes[1].InnerText, v3, float.Parse(nodes[11].InnerText)));
                    break;
                case "CommandWait":
                    commands.Add(new CommandWait(float.Parse(nodes[11].InnerText)));
                    break;
                default:
                    break;
            }
        }
    }
 
    Vector3 StringToVector3(string s)
    {
        s = s.Substring(1,s.Length - 2);
        string[] a = s.Split(',');
        Vector3 b = new Vector3(float.Parse(a[0]), float.Parse(a[1]), float.Parse(a[2]));
        return b;
    }

可以看到,节点名称都被标号了,这样读取的时候就很容易拿到我们想要的数据了。

其实做编辑器工具是一种挺好的加快效率的方式,因为生活中,程序主要负责的是程序,策划主要就是弄表格等等的,这样就可以把程序的一部分工作交给策划来弄,策划只需在编辑器上使用这些工具,就可以实现同等的功能,可谓是双赢啊哈哈。。


-- END --

    


公众号后台回复「资料」获取超多学习福利

      
>>> 点击进入技术讨论群 <<<
▽想深入了解么?

长按/扫码关注我吧↑↑↑


觉得不错就点个在看

浏览 15
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报