欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > Unity运行时节点编辑器——互动电影案例

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

2024/10/23 23:21:21 来源:https://blog.csdn.net/sdhexu/article/details/140533903  浏览:    关键词:Unity运行时节点编辑器——互动电影案例

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

引子

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

Unity制作运行时节点编辑器

其实之前就写过类似的功能:Unity Graph View打造图形化对话框编辑系统
虽然这个Graph View很方便,但是它只能运行在Unity的编辑模式,不能在“Unity运行时”进行编辑,至少我目前未找到如何运行时使用这个功能。所以全部都需要自己来整,幸好并不是很复杂。

技术点

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

实现

  • 基本的数据描述,这里只给出了关键的接口,具体实现太冗长了,而且并不复杂。
// 节点布局形式
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);
}
  • 下面给出UGUI上连线的方案,原理请参考《Unity UGUI优雅的绘制线段》
[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;		// 点Aprivate Vector2 B;		// 点Bprivate 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);}
}

关于源码

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

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com