Unity运行时节点编辑器——互动电影案例

Unity运行时节点编辑器------互动电影案例

引子

最近需要做一个互动电影的小项目,需求很简单,就是有一堆的视频,然后在某视频播放完的时候,让观众做一个选择题,然后根据观众做出的选择,继续播放不同的视频,实现观看不同的结局的目的。于是,本着让策划傻瓜化使用的原则,就有了这个运行时的节点编辑器。

先来看看效果吧:

Unity制作运行时节点编辑器

其实之前就写过类似的功能:Unity Graph View打造图形化对话框编辑系统

虽然这个Graph View很方便,但是它只能运行在Unity的编辑模式,不能在"Unity运行时"进行编辑,至少我目前未找到如何运行时使用这个功能。所以全部都需要自己来整,幸好并不是很复杂。

技术点

  • 数据描述。其实严格来说,当配置完成,处于运行模式的时候,这些节点的外观属性是不需要的,核心的数据只需要知道节点如何运行,以及节点的下一个接口是什么,参数节点的值是什么,这类似于一个"语法树"。然而,在编辑模式下,还需要额外增加编辑的属性,比如节点的位置之类的,这需要一个有效的数据结构。
  • 节点的连线。没错,其实节点的连线远比我想想的复杂。可能有人第一感觉想到LineRenderer,但用LineRenderer其实并不太适合,原因是,这些节点在UI层,需要被Mask ,比如你的图在一个Scroll View下面,超出去视图的部分,应该被遮蔽,而如果用LineRenderer,这将是一件很麻烦的事。幸好,之前研究过在UGUI画线的方法(请参考Unity UGUI优雅的绘制线段)。还有另外一件事,就是线段的"拾取",本来,我是使用"直线"来连接节点的,用直线不仅方便,性能好,而且拾取算法很容易写,用点到直线的距离公式即可,但是直线看上去不是那么"高大上",然后就有了贝塞尔曲线版本,然后你判断鼠标是否点到了线上,就比较麻烦了,不过测试下来,性能还可以接受。

实现

  • 基本的数据描述,这里只给出了关键的接口,具体实现太冗长了,而且并不复杂。
csharp 复制代码
// 节点布局形式
public enum Layout
{
	LeftOnly,		// 只有左边的接口
	RightOnly,		// 只有右边的接口
	Both			// 左右都有接口
}

// 节点类型
public enum NodeType
{
	Empty,		// 空节点 
	Video,		// 视频节点
	Question,	// 问题和玩家选择节点
	String,		// 字符串节点
	Text,		// 文本节点
	// .... 将来扩充更多类型的节点,比如逻辑判断。。。
}

// 接口类型
public enum PortType
{
	Input,		// 程序走向,输入接口
	Output,		// 程序走向,输出接口
	Params,		// 参数接口(输入)
	Value		// 参数值接口(输出)
}

// 节点类(仅接口描述)
public class Nodebase
{
	// 节点的位置
	public Vector2 position { get; set; }
	// 节点布局形式
	public Layout layout { get; set; }
	// 运行节点
	public virtual void Run() {}
	// 尝试获取节点的接口
	public bool TryGetPort(string portName, out InterfacePort port);
	// 获取节点的值(比如当节点是一个文本节点,则返回string类型的文本值)
	public T GetParamsValue<T>();
	// 创建节点
	public InterfacePort CreatePort(string portName, PortType type,
	// 节点列表
	private Dictionary<string,InterfacePort> ports = new ();
}

public class InterfacePort
{
	// 接口连线处的坐标
	public Vector2 PortPosition { get; }
	// 接口类型
	public PortType Type { get; }
	// 所属的节点
	public Nodebase OwnerNode { get; }
	// 建立链接
	public void MakeLink(InterfacePort other);
	// 清除指定链接
	public void ClearLink(InterfacePort other);
	// 清楚所有链接
	public void ClearAlllink();
	// 判断到指定的端口可否建立链接(类型判断等,比如两个输入端口不能连在一起)
	public bool IsConnectable(InterfacePort target);
}
csharp 复制代码
[RequireComponent(typeof(CanvasRenderer))]
public class LineRendererOnGUI : MaskableGraphic, ICanvasRaycastFilter, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
{
	// 线的半径
    [SerializeField] private float _Radius = 2f;

	// 各类事件
    public UnityEvent<LineRendererOnGUI> OnClick;
    public UnityEvent<LineRendererOnGUI> OnPointerHover;
    public UnityEvent<LineRendererOnGUI> OnPointerLeave;
    
    // 半径
    public float Radius
    {
        get => _Radius;
        set
        {
            _Radius = value;
            float v = _Radius + 2;
            squareOfRadius = v * v;
        }
    }

    private Vector2 A;		// 点A
    private Vector2 B;		// 点B
    private bool aIsRight;	// 点A是否朝右
    private bool bIsRight;	// 点B是否朝右
    private float squareOfRadius;	// 半径的平方(判断点击用)
    private RectTransform _Parent;
    
    protected override void Awake()
    {
        base.Awake();
        squareOfRadius = ( _Radius + 2 ) * ( _Radius + 2 );
        _Parent = transform.parent.GetComponent<RectTransform>();
    }

    public void SetStartPos(Vector2 pos, bool bRight, bool bUpdate = true)
    {
        A = pos;
        aIsRight = bRight;
        if (bUpdate)
            RebuildLine();
    }

    public void SetEndPos(Vector2 pos, bool bRight, bool bUpdate = true)
    {
        B = pos;
        bIsRight = bRight;
        if (bUpdate)
            RebuildLine();
    }

    private float delta;	// 精细度
    // 重建贝塞尔
    private void RebuildLine()
    {
        float h = Mathf.Abs(A.x - B.x) * 0.5f;
        Vector2 C, D;
        C.y = A.y;
        D.y = B.y;

        C.x = aIsRight ? A.x + h : A.x - h;
        D.x = bIsRight ? B.x + h : B.x - h;

        float dis = Vector2.Distance(A, B);
        delta = 1f / ( dis * 0.125f );	// 每8个像素增加一个细节,delta越小越精细
        positions.Clear();
        for (float t = 0; t <= 1f; t += delta)
        {
            float st = 1f - t;
            float st2 = st * st;
            float t2 = t * t;
            Vector2 p = st2 * st * A + 3 * st2 * t * C + 3 * st * t2 * D + t2 * t * B;
            positions.Add(p);
        }

        //SetVerticesDirty();
        //SetRaycastDirty();
        SetAllDirty();
    }

    private readonly List<Vector2> positions = new();
    protected override void OnPopulateMesh(VertexHelper vh) // 构造线段
    {
        if (positions.Count <= 1)
        {
            base.OnPopulateMesh(vh);
            return;
        }

        vh.Clear();

        int count = positions.Count;
        int csub = count - 1;
        for (int i = 0; i < count; ++i)
        {
            int ia1 = i + 1;
            int is1 = i - 1;

            if (i == 0)
            {
                FiristPoint(positions[i], positions[ia1], vh);
            }
            else if (i == csub)
            {
                LastPoint(positions[is1], positions[i], vh);
            }
            else
            {
                MidPoint(positions[is1], positions[i], positions[ia1], vh);
            }

            if (i > 0)
            {
                int id2 = i << 1;
                int is1d2 = is1 << 1;
                int is1d2a1 = is1d2 + 1;

                vh.AddTriangle(is1d2, id2, is1d2a1);
                vh.AddTriangle(is1d2a1, id2, id2 + 1);
            }
        }
    }

    private static readonly Quaternion orthogonality = Quaternion.AngleAxis(90, Vector3.forward);

    private void FiristPoint(Vector2 cur, Vector2 next, VertexHelper vh)
    {
        Vector2 left = (orthogonality * (next - cur)).normalized;
        vh.AddVert(cur + left * Radius, color, Vector2.zero);
        vh.AddVert(cur + left * -Radius, color, Vector2.zero);
    }

    private void LastPoint(Vector2 prev, Vector2 cur, VertexHelper vh)
    {
        Vector2 left = (orthogonality * (cur - prev)).normalized;
        vh.AddVert(cur + left * Radius, color, Vector2.zero);
        vh.AddVert(cur + left * -Radius, color, Vector2.zero);
    }

    /// <summary>
    /// 处理中间节点
    /// </summary>
    /// <param name="prev">上一个顶点</param>
    /// <param name="cur">当前顶点</param>
    /// <param name="next">下一个顶点</param>
    /// <param name="vh">顶点管理器</param>
    private void MidPoint(Vector2 prev, Vector2 cur, Vector2 next, VertexHelper vh)
    {
        Vector2 left1 = (orthogonality * (cur - prev)).normalized;
        Vector2 left2 = (orthogonality * (next - cur)).normalized;

        Vector2 left = ((left1 + left2) * 0.5f).normalized;
        float a = Vector2.Angle(left1, left2) * Mathf.Deg2Rad * 0.5f;

        float r = Radius / Mathf.Cos(a);

        vh.AddVert(cur + left * r, color, Vector2.zero);
        vh.AddVert(cur + left * -r, color, Vector2.zero);
    }

    // 射线击中过滤,
    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        if (raycastTarget && RectTransformUtility.ScreenPointToLocalPointInRectangle(_Parent, sp, eventCamera, out Vector2 lp))
        {
            float h = Mathf.Abs(A.x - B.x) * 0.5f;
            Vector2 C, D;
            C.y = A.y;
            D.y = B.y;

            C.x = aIsRight ? A.x + h : A.x - h;
            D.x = bIsRight ? B.x + h : B.x - h;

            for (float t = 0; t <= 1f; t += delta)
            {
                float st = 1f - t;
                float st2 = st * st;
                float t2 = t * t;
                Vector2 p = st2 * st * A + 3 * st2 * t * C + 3 * st * t2 * D + t2 * t * B;
                if ((lp - p).sqrMagnitude < squareOfRadius)
                    return true;
            }
        }

        return false;
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        OnClick?.Invoke(this);
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        OnPointerHover?.Invoke(this);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        OnPointerLeave?.Invoke(this);
    }
}

关于源码

这个项目目前还没做完,涉及到运行时问问题的界面还需要策划和美术去敲定,所以目前,问题节点的运行还没有实际做完,但是作为一个抛砖引玉的东西,用来研究一下思路还是可以滴。另外,代码写的有点乱,因为思路变了好几次,所以工程源码还有很多可以改进的地方。最后,不建议小白去下载。一定想下载研究的话,请一定看前面这段话,三思之后再下载。
点击下载源码

相关推荐
charon877814 小时前
UE ARPG | 虚幻引擎战斗系统
游戏引擎
小春熙子15 小时前
Unity图形学之Shader结构
unity·游戏引擎·技术美术
Sitarrrr17 小时前
【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
3d·unity
极梦网络无忧17 小时前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
逐·風1 天前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i1 天前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
代码盗圣1 天前
GODOT 4 不用scons编译cpp扩展的方法
游戏引擎·godot
Leoysq1 天前
【UGUI】实现点击注册按钮跳转游戏场景
游戏·unity·游戏引擎·ugui
PandaQue2 天前
《潜行者2切尔诺贝利之心》游戏引擎介绍
游戏引擎
_oP_i2 天前
unity中 骨骼、纹理和材质关系
unity·游戏引擎·材质