Unity StoryLine基于Node节点的剧情编辑器

Unity StoryLine基于Node节点的剧情编辑器

个人博客

介绍

随着《完蛋我被美女包围了》等互动影视游戏的大火,短视频平台那些剧情节奏快一集一个反转的视频快速批量的产出,那么在游戏开发制作方面就必须跟上视频产出的速度,所以有一套剧情编辑器就可以加速和规范开发流程。

在这里插入图片描述
在这里插入图片描述

设计思路

需求分析

需求如下:

  1. 支持显示视频和图片,可在图片或视频上叠加固定区域选项自定义位置选项QTE自定义位置按钮
  2. 视频播放完或者播放到某一时间会弹出选项,根据选项继续播放下一个视频或者图片,如果没有选项则自动播放
  3. 在播放下一个视频或图片之前,可以穿插小游戏玩法,比如视频播放完后进入一段打飞机游戏,小游戏失败可成功可进入不同的视频
  4. 叠加在视频或图片上的选项可配置 条件。条件用于判断是否显示选项,点击选项是否可触发。
  5. 在播放某个视频或者点击某个选项可触发自定义游戏内容,比如获取道具、获取成就等游戏业务逻辑。

具体分析:

  • 作为剧情编辑器,一目了然的看到剧情走向和各种剧情分支是极其重要的,所以首先就摒弃了纯参数填写的编辑器方式,把各种参数交给产品策划填写,太复杂填写容易出错不运行游戏根本发现不了,而且可读性极差。所以我选择了基于Unity中GraphView的Node节点的形式制作视频节点编辑器,官方的ShaderGraph就是节点编辑。

  • 决定好了在GraphView下用Node节点连线方式来做编辑器,那么首先根据需求拆分为 章节编辑器单元剧编辑器

    编辑器 说明
    章节编辑器 用于编辑故事分支走向
    单元剧编辑器 用于编辑每个故事节点的具体内容:播放什么视频图片,穿插小游戏等随意组合
  • 章节编辑器中则把节点分为:开始节点,结束节点,单元剧节点

    节点名称 说明
    开始节点 一个章节的开头,可能有具体的业务逻辑
    结束节点 一个章节的结束,用于链接下一章节或者有具体的业务逻辑
    单元剧节点 用于关联单元剧的
  • 单元剧编辑器咋把节点分为:开始节点、结束节点、视频节点、图片节点、事件节点、小游戏节点、跳转Jump节点

    节点名称 说明
    开始节点 用于单元剧的开头
    结束节点 单元剧结束,可有多个End节点,用于在章节编辑器中显示出口
    视频节点 游戏播放视频时的数据编辑,用于配置显示选项、视频资源等
    图片节点 游戏显示图片时的数据编辑,用于配置显示选项、视频资源等
    事件节点 用于连接StoryLine编辑器和正常开发的业务逻辑。比如一些获取道具成就啥的,不应该内置在编辑器中,而是由主程序根据实际情况拓展
    小游戏节点 主程序中实现小游戏接口即可在编辑器中选择小游戏。小游戏也是根据情况自行拓展的
    跳转Jump节点 在单元剧编辑器中的每个节点都有一个唯一id,那么Jump跳转节点既可以直接跳转到某个id的节点播放。目的是可能有一些特殊用途。
  • 在视频节点和图片节点中的选项里面可以添加条件,用于判断是否显示这个选项或者是否可以点击触发这个选项。那么这个条件的具体代码逻辑就不能内置在StoryLine包中,而是给Attribute特效,在主程序中给一个方法添加此特性,那么就可以在编辑器中选择这个条件方法。这样就可以连接编辑器和实际开发的业务需求了。

代码设计

有了大概的需求,那么就可以着手设计代码了。

在这里插入图片描述

事件系统

为了让StoryLine编辑器相对独立且有一定的拓展性,需要让游戏开发中部分业务逻辑可以在编辑器中编辑,那么我们就可以通过事件分发来实现。在业务逻辑中写一个事件分发所需要的数据结构,在编辑器中编辑对应的数据结构,然后StoryLine的事件系统把这个数据分发出去让业务层监听执行即可。

比如业务层需要在播放完一个视频后获取一个道具,作为独立package 的StoryLine肯定不知道有这个业务,所以业务层只需编写一个获取道具所需的数据结构(道具id,道具数量),然后StoryLine的事件系统分发这个数据结构,让业务层执行获取道具的任务即可。这样就可以任意拓展了。

对于事件系统代码设计,我参考了知名框架ET中的EventSystem。

新增了两个Attribute,用于收集所有的EventStruct和Event要执行的代码逻辑。

  • EventAttribute:用于标记业务代码逻辑
  • StoryLineEventAttribute:用于标记数据结构
[AttributeUsage(AttributeTargets.Class)]
public class BaseAttribute: Attribute
{
}

public class EventAttribute: BaseAttribute
{
  public EventAttribute()
  {
  }
}

[AttributeUsage(AttributeTargets.Struct,AllowMultiple = true)]
public class StoryLineEventAttribute : Attribute
    {
        private long _eventId;
        public long eventId => _eventId;

        private string _eventName;
        public string eventName => _eventName;
        public StoryLineEventAttribute(long eventId,string eventName)
        {
            _eventId = eventId;
            _eventName = eventName;
        }
}

在CodeTypes.cs脚本中,收集上述的两个Attribute

public void Init(Assembly[] assemblies)
        {
            Dictionary<string, Type> addTypes = AssemblyHelper.GetAssemblyTypes(assemblies);
            foreach ((string fullName, Type type) in addTypes)
            {
                this.allTypes[fullName] = type;
                
                if (type.IsAbstract)
                {
                    continue;
                }
                
                // 记录所有的有BaseAttribute标记的的类型
                object[] objects = type.GetCustomAttributes(typeof(BaseAttribute), true);

                foreach (object o in objects)
                {
                    this.types.Add(o.GetType(), type);
                }
                
                object[] objectsStoryLineEventAttribute = type.GetCustomAttributes(typeof(StoryLineEventAttribute), true);
                foreach (object o in objectsStoryLineEventAttribute)
                {
                    this.types.Add(o.GetType(), type);
                }
            }
        }

最后在EventSystem.cs中分发即可。遍历数据结构T对应的所有业务逻辑片段,然后挨个执行Handle即可

 public void Publish<T>(T a) where T : struct
        {
            List<EventInfo> iEvents;
            if (!this.allEvents.TryGetValue(typeof (T), out iEvents))
            {
                return;
            }
            
            foreach (EventInfo eventInfo in iEvents)
            {
                
                if (!(eventInfo.IEvent is AEvent<T> aEvent))
                {
                    Debug.LogError($"event error: {eventInfo.IEvent.GetType().FullName}");
                    continue;
                }
                aEvent.Handle(a);
            }
        }

具体实现可以查看框架ET中的EventSystem。

数据结构

数据结构这边不多赘述,在章节编辑器中需要章节开始节点,章节结束节点,单元剧节点的数据。在单元剧编辑器中需要开始节点、结束节点、视频节点、图片节点、事件节点、小游戏节点、跳转Jump节点。

单元剧编辑器创建编辑的DialogueContainer的ScriptableObject对应了章节编辑器中单元剧节点的数据,章节编辑器本质就是创建编辑ChapterContainer的ScriptableObject。

编辑器

编辑器的开发中,我导入了Odin插件加速开发。

剧情编辑器

关于Node剧情编辑器,我从总体的构想上进行,实现一个可用的节点编辑器,而不是顺序性的实现一个简单的图。

下面以章节编辑器为示例。单元剧编辑大同小异,原理都是一样的。

GraphView

GraphView类是一个UIElements,继承于VisualElement,继承GraphView类以 创建自己的 UI 控件 ,GraphView提供的功能仅仅是提供图的一些基本功能。可以注册 添加/移除 节点/边 的事件,能获取到图的各种信息。

ISearchWindowProvider 接口

这个接口用于提供一个右键菜单,指明右键有哪些节点能被创建。

在这里插入图片描述

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

namespace StoryL
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值