E Story 故事编辑器开发笔记 #18 实现变量系统(1)

若想进一步提升游戏的互动性,可引入变量系统。简单来说就是,当玩家在剧情分支做出不同选择时,能够修改预先设定好的变量数值,后续系统读取改动后的数值,判断条件并展示不同的后续剧情。

我们可以设计好感度、生命值、货币等这类变量,让玩家的每一次选择都产生长远影响,这样便能极大地丰富游戏的互动层次。

因为内容较多,故我分成了三篇文章进行详解,以下内容是第一部分:

在编辑器中实现变量面板

显示与隐藏变量面板

打开StoryEditorWindow.cs,新增若干字段与方法,并进行调用:

C#
/*** 新增字段 ***/

private Button btnVars;

/*** 新增方法 ***/

// 切换变量面板
private void ToggleVarPanel()
{
    graphView.ToggleVarPanel();
    // 切换按钮样式
    btnVars.ToggleInClassList("button__selected");
}

/*** 设置调用 ***/

// 添加工具栏
private void AddToolbar()
{
    /* ... 此处代码已省略 ... */

    btnVars = ElementUtility.CreateButton("变量面板", () => ToggleVarPanel());

    /* ... 此处代码已省略 ... */

    toolbar.Add(btnVars);

    /* ... 此处代码已省略 ... */
}

打开StoryGraphView.cs,新增若干字段与方法,并进行调用:

C#
/*** 新增字段 ***/

private VarPanel varPanel;
public VarPanel VarPanel { get => varPanel; }

/*** 新增方法 ***/

// 添加变量面板
private void AddVarPanel()
{
    varPanel = new();
    varPanel.Init(this);
    varPanel.Draw();

    Add(varPanel);
}

// 切换变量面板
public void ToggleVarPanel()
{
    varPanel.visible = !varPanel.visible;
}

/*** 设置调用 ***/

// 构造器
public StoryGraphView(StoryEditorWindow window)
{
    /* ... 此处代码已省略 ... */
    AddVarPanel();
    /* ... 此处代码已省略 ... */
}

打开UI文件夹,创建C#文件,命名为VarPanel,打开VarPanel.cs,将内容修改如下:

C#
using UnityEditor.Experimental.GraphView;

namespace E.Story
{
    public class VarPanel : Blackboard
    {
        // 初始化
        public void Init(StoryGraphView graphView)
        {
            this.graphView = graphView;
            title = "变量";
            subTitle = "";
            scrollable = true;
            visible = false;
        }

        // 绘制
        public void Draw()
        {}
    }
}

新增、删除与编辑变量

打开Data文件夹,创建C#文件,命名为VarData,打开VarData.cs,将内容修改如下:

C#
using System;
using UnityEngine;

namespace E.Story
{
    // 变量数据
    [Serializable]
    public class VarData
    {
        [SerializeField] private string key;
        [SerializeField] private int value;

        // 变量名
        public string Key { get => key; set => key = value; }

        // 变量值
        public int Value { get => value; set => this.value = value; }


        public VarData(string key, int value = 0)
        {
            this.key = key;
            this.value = value;
        }
    }
}

打开VarPanel.cs,将内容修改如下:

C#
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

namespace E.Story
{
    public class VarPanel : Blackboard
    {
        public List<VarData> VarDatas { get; set; }

        public List<string> VarDataKeys
        {
            get
            {
                List<string> keys = new();
                foreach (VarData varData in VarDatas)
                {
                    string key = varData.Key;
                    keys.Add(key);
                }
                return keys;
            }
        }

        // 初始化
        public void Init(StoryGraphView graphView)
        {
            this.graphView = graphView;
            title = "变量";
            subTitle = "";
            scrollable = true;
            visible = false;
            VarDatas = new();

            addItemRequested = (bb) =>
            {
                VarData varData = new("新变量");
                VarDatas.Add(varData);

                VisualElement lineContainer = CreateVarData(varData);
                Add(lineContainer);

                CheckDuplicateVarKey();
            };

            moveItemRequested = (bb, index, ve) =>
            {
                Debug.Log("moveItemRequested");
            };

            editTextRequested = (bb, ve, text) =>
            {
                Debug.Log("editTextRequested");
            };
        }

        // 绘制
        public void Draw()
        {
            Clear();

            // 遍历
            foreach (VarData varData in VarDatas)
            {
                VisualElement lineContainer = CreateVarData(varData);
                Add(lineContainer);
            }
        }

        // 创建变量数据UI
        private VisualElement CreateVarData(VarData varData)
        {
            // 创建行容器
            VisualElement lineContainer = new();
            lineContainer.userData = userData;
            lineContainer.AddClasses("row-container");

            // 创建变量名输入框
            TextField tfdVarKey = ElementUtility.CreateTextArea(varData.Key, null, callback =>
            {
                varData.Key = callback.newValue;

                CheckDuplicateVarKey();
            });
            tfdVarKey.AddClasses
            (
                "textfield",
                "textfield__quote"
            );
            lineContainer.Add(tfdVarKey);

            // 创建变量值输入框
            IntegerField tfdVarValue = ElementUtility.CreateIntField(varData.Value, null, callback =>
            {
                varData.Value = callback.newValue;
            });
            tfdVarValue.AddClasses
            (
                "textfield",
                "textfield__quote"
            );
            lineContainer.Add(tfdVarValue);

            // 创建删除按钮
            Button btnDelete = ElementUtility.CreateButton("X", () =>
            {
                VarDatas.Remove(varData);
                Remove(lineContainer);

                CheckDuplicateVarKey();
            });
            btnDelete.AddClasses();
            lineContainer.Add(btnDelete);

            return lineContainer;
        }
        
        // 检测重复变量名
        private void CheckDuplicateVarKey()
        {
            List<string> keys = new();
            List<string> duplicateKeys = new();

            // 记录重复的变量名
            foreach (VisualElement lineContainer in Children())
            {
                TextField tfdKey = lineContainer.Q<TextField>();

                if (keys.Contains(tfdKey.value))
                {
                    duplicateKeys.Add(tfdKey.value);
                }
                else
                {
                    keys.Add(tfdKey.value);
                }
            }

            // 如果有重复,高亮所有相关条目的背景色
            foreach (VisualElement lineContainer in Children())
            {
                TextField tfdKey = lineContainer.Q<TextField>();

                if (duplicateKeys.Contains(tfdKey.value))
                {
                    lineContainer.AddClasses("error-bg");
                }
                else
                {
                    lineContainer.RemoveFromClassList("error-bg");
                }
            }

            // 如果有重复,显示副标题
            if (duplicateKeys.Count > 0)
            {
                this.Q<Label>("subTitleLabel").style.color = new StyleColor(new Color(1, 0.133f, 0.133f, 1));
                subTitle = "请勿设置同名变量";
            }
            else
            {
                this.Q<Label>("subTitleLabel").style.color = new StyleColor(new Color(0.133f, 0.133f, 0.133f, 1));
                subTitle = "";
            }
        }
    }
}

打开ElementUtility.cs,新增方法:

C#
// 创建整数字段
public static IntegerField CreateIntField(int value = 0, string label = null, EventCallback<ChangeEvent<int>> onValueChanged = null)
{
    IntegerField field = new()
    {
        value = value,
        label = label,
    };

    if (onValueChanged != null)
    {
        field.RegisterValueChangedCallback(onValueChanged);
    }

    return field;
}

新增默认变量

打开StoryGraphView.cs,新增方法并进行调用:

C#
/*** 新增方法 ***/

// 添加默认变量
public void AddDefaultVarData()
{
    VarData varData1 = new("变量一");
    VarData varData2 = new("变量二");
    VarData varData3 = new("变量三");
    varPanel.VarDatas.Add(varData1);
    varPanel.VarDatas.Add(varData2);
    varPanel.VarDatas.Add(varData3);
    varPanel.Draw();
}

/*** 设置调用 ***/

// 构造器
public StoryGraphView(StoryEditorWindow window)
{
    /* ... 此处代码已省略 ... */
    AddDefaultVarData();
    /* ... 此处代码已省略 ... */
}

打开StoryEditorWindow.cs进行调用:

C#
// 新建故事
private void NewStory()
{
    /* ... 此处代码已省略 ... */
    if (EditorUtility.DisplayDialog("警告", str, "确认", "取消"))
    {
        /* ... 此处代码已省略 ... */
        graphView.AddDefaultVarData();
        /* ... 此处代码已省略 ... */
    }
}

// 清空视图并添加默认数据
private void ClearGraphAndCreateDefaultDatas()
{
    /* ... 此处代码已省略 ... */
    if (EditorUtility.DisplayDialog("警告", str, "确认", "取消"))
    {
        /* ... 此处代码已省略 ... */
        graphView.AddDefaultVarData();
        /* ... 此处代码已省略 ... */
    }
}

保存与读取变量

打开StoryDataSO.cs,新增字段并进行初始化:

C#
/*** 新增字段 ***/

[SerializeField] private List<VarData> varDatas;
public List<VarData> VarDatas { get => varDatas; set => varDatas = value; }

/*** 修改方法 ***/

// 初始化
public void Init(string fileName)
{
    /* ... 此处代码已省略 ... */
    varDatas = new();  //初始化字段
}

打开StoryEditorWindow.cs,新增若干方法并进行调用:

C#
/*** 新增方法 ***/

// 保存变量数据
private void SaveVarDatas(List<VarData> vars)
{
    storyData.VarDatas = DataUtility.CloneVarDatas(vars);
}

// 载入变量数据
private void LoadVarDatas(List<VarData> oldVarDatas)
{
    List<VarData> newVarDatas = DataUtility.CloneVarDatas(oldVarDatas);
    graphView.VarPanel.SetVarDatas(newVarDatas);
}

/*** 设置调用 ***/

// 保存数据
private void SaveDatas()
{
    /* ... 此处代码已省略 ... */
    SaveVarDatas(graphView.Vars);
    /* ... 此处代码已省略 ... */
}

// 载入数据
private void LoadDatas(StoryDataSO storyData)
{
    /* ... 此处代码已省略 ... */
    LoadVarDatas(storyData.VarDatas);
    /* ... 此处代码已省略 ... */
}

打开VarPanel.cs,新增若干方法:

C#
// 设置变量数据
public void SetVarDatas(List<VarData> varDatas)
{
    VarDatas = varDatas;

    Draw();
}

// 清除面板
public void ClearPanel()
{
    VarDatas.Clear();

    Draw();
}

打开StoryGraphView.cs,新增属性并修改方法:

C#
/*** 新增属性 ***/

public List<VarData> Vars 
{
    get => varPanel.VarDatas;
}

/*** 修改方法 ***/

// 清除视图
public void ClearGraph()
{
    graphElements.ForEach(e => RemoveElement(e));

    varPanel.ClearPanel();
}

打开DataUtility.cs,新增方法:

C#
// 克隆变量数据列表
public static List<VarData> CloneVarDatas(List<VarData> oldDatas)
{
    if (oldDatas == null)
    {
        return null;
    }

    List<VarData> newDatas = new();
    foreach (VarData data in oldDatas)
    {
        VarData newData = new(data.Key, data.Value);
        newDatas.Add(newData);
    }

    return newDatas;
}

在游戏界面显示变量信息

添加基础交互UI

  1. 打开Example场景,在Canvas下创建变量信息面板,命名为Pan Vars
  2. 在Pan Vars下创建一个空对象,命名为Item Var
  3. 在Item Var下创建两个文本对象,分别用于显示变量名和变量值

添加组件脚本

打开Component文件夹,创建C#文件,命名为UIVarList,打开UIVarList.cs,将内容修改如下:

C#
namespace E.Story
{
    // UI变量列表
    public class UIVarList : UIList<VarData>
    {
    }
}

继续创建C#文件,命名为UIVarItem,打开UIVarItem.cs,将内容修改如下:

C#
using UnityEngine;
using UnityEngine.UI;

namespace E.Story
{
    public class UIVarItem : UIItem<VarData>
    {
        [Header("UI组件")]
        [SerializeField] private Text txtKey;
        [SerializeField] private Text txtValue;

        public override void Refresh()
        {
            // 更新选项文本
            txtKey.text = Data.Key;
            txtValue.text = Data.Value.ToString();
        }
    }
}

设置显示逻辑

打开StoryUI.cs,新增若干字段与方法:

C#
/* 变量面板 */
[SerializeField] private GameObject panVars;
[SerializeField] private UIVarList uiVarList;

// 显示变量面板
public void ShowVars(List<VarData> varDatas)
{
    panVars.SetActive(true);
    uiVarList.SetDatas(varDatas);
}

// 隐藏变量面板
public void HideVars()
{
    panVars.SetActive(false);
}

打开StoryRunner.cs,新增字段并调用方法:

C#
/*** 新增字段 ***/

[SerializeField] private List<VarData> varDatas;

/*** 设置调用 ***/

private void Start()
{
    /* ... 此处代码已省略 ... */
    storyUI.HideVars();
}

// 开始故事
public void StartStory()
{
    /* ... 此处代码已省略 ... */

    isStoryPlaying = true;
    varDatas = DataUtility.CloneVarDatas(currentStory.VarDatas);  //此处调用

    /* ... 此处代码已省略 ... */
    storyUI.ShowVars(varDatas);  //此处调用

    DoNode(nodeData);
}

// 结束故事
public void EndStory()
{
    // 重置数据
    /* ... 此处代码已省略 ... */
    varDatas = null;

    // 重置UI
    /* ... 此处代码已省略 ... */
    storyUI.HideVars();
}

// 执行节点
public void DoNode(NodeData nodeData)
{
    /* ... 此处代码已省略 ... */

    // 根据当前节点类型执行对应的操作
    switch (nodeData.Type)
    {
        /* ... 此处代码已省略 ... */

        case NodeType.End:
            /* ... 此处代码已省略 ... */
            storyUI.HideVars();
            break;

        /* ... 此处代码已省略 ... */
    }
}

挂载组件并赋值

  1. 将UIVarItem.cs挂载在Item Var对象上,并对红框中的两个变量赋值:

  2. 将UIVarList.cs挂载在Pan Vars对象上,并对红框中的两个变量赋值:

  3. 将Story Manager对象的StoryUI.cs组件红框中的两个变量赋值:

测试效果

如图所示配置好变量:

最终运行效果如下:

相关链接

留下评论