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);}
}

关于源码

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/47227.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

科研绘图系列:R语言分割小提琴图(Split-violin)

介绍 分割小提琴图(Split-violin plot)是一种数据可视化工具,它结合了小提琴图(violin plot)和箱线图(box plot)的特点。小提琴图是一种展示数据分布的图形,它通过在箱线图的两侧添加曲线来表示数据的密度分布,曲线的宽度表示数据点的密度。而分割小提琴图则是将小提…

【题解】2014年408计网真题

15.【2014统考真题】使用浏览器访问某大学的Web网站主页时&#xff0c;不可能使用到的协议是&#xff08;&#xff09;。A. PPP B. ARP C. UDP D. SMTP 我们逐一分析给定的选项&#xff1a; A. PPP&#xff08;Point-to-Point Protocol&#xff0c;点对点协议&#xff09;&…

Python 模块导入方式

在Python 中&#xff0c;导入外部模块有2种方式 以 Pyhton 自带的 time 模块 为例&#xff1a; 使用 import time 导入方式 import time print(time.ctime()) 注意事项&#xff1a; time 模块导入后&#xff0c;使用以下格式来调用模块中的函数: 模块名.函数名 如果导入的模…

绿色算力|暴雨服务器用芯片筑起“十四五”转型新篇章

面对全球气候变化、技术革新以及能源转型的新形势&#xff0c;发展低碳、高效的绿色算力不仅是顺应时代的要求&#xff0c;更是我国建设数字基础设施和展现节能减碳大国担当的重要命题&#xff0c;在此背景下也要求在提升算力规模和性能的同时&#xff0c;积极探索推动算力基础…

day2加餐 Go 接口型函数的使用场景

文章目录 问题价值使用场景其他语言类似特性 问题 在 动手写分布式缓存 - GeeCache day2 单机并发缓存 这篇文章中&#xff0c;有一个接口型函数的实现&#xff1a; // A Getter loads data for a key. type Getter interface {Get(key string) ([]byte, error) }// A Getter…

【iOS】APP仿写——网易云音乐

网易云音乐 启动页发现定时器控制轮播图UIButtonConfiguration 发现换头像 我的总结 启动页 这里我的启动页是使用Xcode自带的启动功能&#xff0c;将图片放置在LaunchScreen中即可。这里也可以通过定时器控制&#xff0c;来实现启动的效果 效果图&#xff1a; 这里放一篇大…

31_MobileViT网络讲解

VIT:https://blog.csdn.net/qq_51605551/article/details/140445491?spm1001.2014.3001.5501 1.1 简介 MobileVIT是“Mobile Vision Transformer”的简称&#xff0c;是一种专门为移动设备设计的高效视觉模型。它结合了Transformer架构的优点与移动优先的设计原则&#xff0…

在eclipse中导入本地的jar包配置Junit环境步骤(包含Junit中的方法一直标红的解决方法)

搭建JUnit环境 一、配置环境 跟上一篇的那种方法不一样&#xff0c;直接Add to Build Path 是先将jar包复制到项目的lib目录下&#xff0c;然后直接添加 选定项目>>>右键>>>Bulid Path>>>Add Libraries>>>Configure Build Path(配置构建路…

Kotlin单例、数据类、静态

文章目录 1. 数据类2. 单例3. 静态 1. 数据类 data class Cellphone(val brand: String, val price: Double)自动生成了get set hashcode equals toString等方法。通过反编译(打开AS&#xff0c;在Tools-Kotlin-ShowBytecode-Decompile)可以得到如下结果 public final class …

python—爬虫爬取电影页面实例

下面是一个简单的爬虫实例&#xff0c;使用Python的requests库来发送HTTP请求&#xff0c;并使用lxml库来解析HTML页面内容。这个爬虫的目标是抓取一个电影网站&#xff0c;并提取每部电影的主义部分。 首先&#xff0c;确保你已经安装了requests和lxml库。如果没有安装&#x…

Fast Planner规划算法(一)—— Fast Planner前端

本系列文章用于回顾学习记录Fast-Planner规划算法的相关内容&#xff0c;【本系列博客写于2023年9月&#xff0c;共包含四篇文章&#xff0c;现在进行补发第一篇&#xff0c;其余几篇文章将在近期补发】 一、Fast Planner前端 Fast Planner的轨迹规划部分一共分为三个模块&…

4.基础知识-数据库技术基础

基础知识 一、数据库基本概念1、数据库系统基础知识2、三级模式-两级映像3、数据库设计4、数据模型&#xff1a;4.1 E-R模型★4.2 关系模型★ 5、关系代数 二、规范化和并发控制1、函数依赖2、键与约束3、范式★3.1 第一范式1NF实例3.2 第二范式2NF3.3 第三范式3NF3.4 BC范式BC…

rockchip的yolov5 rknn python推理分析

rockchip的yolov5 rknn推理分析 对于rockchip给出的这个yolov5后处理代码的分析&#xff0c;本人能力十分有限&#xff0c;可能有的地方描述的很不好&#xff0c;欢迎大家和我一起讨论&#xff0c;指出我的错误&#xff01;&#xff01;&#xff01; RKNN模型输出 将官方的Y…

直方图的最大长方形面积

前提知识&#xff1a;单调栈基础题-CSDN博客 子数组的最大值-CSDN博客 题目描述&#xff1a; 给定一个非负数&#xff08;0和正数&#xff09;&#xff0c;代表直方图&#xff0c;返回直方图的最大长方形面积&#xff0c;比如&#xff0c;arr {3, 2, 4, 2, 5}&#xff0c…

景区导航导览系统:基于AR技术+VR技术的功能效益全面解析

在数字化时代背景下&#xff0c;游客对旅游体验的期望不断提升。游客们更倾向于使用手机作为旅行的贴身助手&#xff0c;不仅因为它能提供实时、精准的导航服务&#xff0c;更在于其融合AR&#xff08;增强现实&#xff09;、VR&#xff08;虚拟现实&#xff09;等前沿技术&…

C++编程:实现一个跨平台安全的定时器Timer模块

文章目录 0. 概要1. 设计目标2. SafeTimer 类的实现2.1 头文件 safe_timer.h源文件 safe_timer.cpp 3. 工作流程图4. 单元测试 0. 概要 对于C应用编程&#xff0c;定时器模块是一个至关重要的组件。为了确保系统的可靠性和功能安全&#xff0c;我们需要设计一个高效、稳定的定…

十三、网络编程正则表达式设计模式(模块23)

网络编程&正则表达式&设计模式 模块23_网络编程&正则表达式&设计模式第一章.网络编程1.软件结构2.服务器概念3.通信三要素4.UDP协议编程4.1.客户端(发送端)4.2.服务端(接收端) 5.TCP协议编程4.1.编写客户端4.2.编写服务端 6.文件上传6.1.文件上传客户端以及服务…

AI学习指南机器学习篇-SOM算法原理

AI学习指南机器学习篇-SOM算法原理 自组织映射&#xff08;Self-Organizing Map, SOM&#xff09;算法是一种常用的无监督学习算法&#xff0c;被广泛应用于数据聚类、可视化和模式识别等领域。SOM算法可以帮助我们发现数据中的隐藏结构&#xff0c;并且能够在高维空间中有效地…

【开发踩坑】 MySQL不支持特殊字符(表情)插入问题

背景 线上功能报错&#xff1a; Cause:java.sql.SQLException:Incorrect string value:xFO\x9F\x9FxBO for column commentat row 1 uncategorized SQLException; SQL state [HY000]:error code [1366]排查 初步觉得是编码问题&#xff08;utf8 — utf8mb4&#xff09; 参考上…

Leetcode 2520. 统计能整除数字的位数

问题描述&#xff1a; 给你一个整数 num &#xff0c;返回 num 中能整除 num 的数位的数目。 如果满足 nums % val 0 &#xff0c;则认为整数 val 可以整除 nums 。 示例 1&#xff1a; 输入&#xff1a;num 7 输出&#xff1a;1 解释&#xff1a;7 被自己整除&#xff0…