开发者中心 开发者中心
  • 简体中文
  • English
视频教程
敢为云网站
  • 6.0版本
  • 6.1 版本
视频教程
敢为云网站
  • 平台概述
  • 平台功能
  • 平台安装
  • 开发者指南
    • 协议插件开发
    • 扩展插件开发
    • 报警插件开发
    • 应用插件开发
    • Web可视化开发
    • 3D可视化开发
      • 平台使用
      • 低代码/二次开发
        • 开发实战
          • 为什么要使用IoT3D
          • 环境要求
          • 技术要求
          • 项目示例及开发包
            • 开发流程
            • 项目打包
            • 调试信息
          • 入门
            • 三维场景
            • 设备开发
            • UI控件开发
            • UI面板开发
          • 进阶
            • 场景漫游
            • 场景事件
            • 第一人称视角
            • 设备定制图标
            • UI控件与三维场景交互
          • 常见问题
            • 打包后代码丢失
            • 使用第三方插件
            • 设备列表不显示静态设备
            • 场景加载错误
            • 可以访问IoTCenter的哪些数据?通过什么接口访问?
            • 是否支持Web版本
        • SDK 开发方式
        • API
    • 桌面可视化开发
    • 小程序开发
    • 应用模块接口
  • 项目实战
  • 附录

概述

# 概述

IoT3D与Unity的关系

IoT3D 是基于Unity开发的数字孪生平台,服务端使用的IoTCenter物联网平台。

# 为什么要使用IoT3D

详情

为什么要这样开发? 直接在Unity编辑器中开发不是更快速吗? 如下列举几个常见的场景:

  1. 客户需要删除、改装或更新现有设备。
  2. 客户有新的设备需要部署。
  3. 客户想更新设备控制。
  4. UI界面需要更新数据。
  5. 客户想要删除某个模块的UI界面。
  6. 客户想换一种风格和布局。
  7. 客户想修改UI分辨率。
  8. 客户想改个项目名和场景去演示。

诸如此类常见的需求是要在unity中开发、联调、测试、发布,这样做的后果就是周期长问题多耗时耗力。 IoT3D编辑器可以轻松并快速的解决这些问题,甚至客户自己学习下就可以完成修改。 而IoT3D开发者只需要开发和维护好自己的控件,工程人员可以通过IoT3D快速构建最低限度可行的产品,即时部署应用程序,完成项目快速交付。

# 环境要求

Unity 版本:可通过IoTCenter 3D.exe 右键属性->详细信息->产品版本。 渲染管线:URP 16.0版本以上。

# 技术要求

开发之前,需要掌握 C# (opens new window)和Unity (opens new window)的相关知识,可以结合Unity官方文档 (opens new window)学习Unity的相关知识。

# 项目示例及开发包

开发者可以通过3D开发开源仓库 (opens new window)获取开发包(仓库中也提供了3D可视化平台安装包,解压即可使用)

# 开发流程

# 项目打包

# 打包工具

详情

AssetBundle Browser 打包工具采用是Unity 标准的 AssetBundle打包方式;

  1. Configure:三维场景、三维设备、UI面板、UI控件等资源管理。
  2. Build:打包三维场景、三维设备、UI面板、UI控件及脚本到IoTCenter 3D平台。
  3. Build Target :打包的平台。
  4. Output Path :IoTCenter 3D所在的路径。
  5. Compression :一般使用LZ4的压缩方式。
  6. Force Rebuild :强制重新打包。
  7. Build : 全量
  8. OnlyCopyScripts:仅打包脚本。
  9. RunIoTCenter3D :运行IoTCenter3D平台。

# 资源设置

详情

资源设置方式有两种:

  1. 在选中Assets的文件,Inspector 面板的最下端,在AssetLabels 中设置Assetbundle资源。
  2. 拖动Assets下的文件,到AssetBundle Browser 对应的Asset下既可。

# 资源说明

详情

打包资源主要分为:三维设备,UI控件,UI面板,三维场景。 deviceres : 三维设备的资源包。 uicontrols :UI控件的资源包。 uiforms :UI面板资源包。 以上三个包是固定的名称不可更改。 scene_1 : 场景包,可跟随项目更改名称。

# 脚本设置

详情

AssetBundle 打包是不包含脚本的。所有脚本需要我们自己关联到 IoT.Module.Scene 这个库中,有两种关联方式。

  1. 如下图, 保证你的脚本和 IoT.Module.Scene 在相同目录中。

  1. 如下图,重新创建文件夹,右键Create->Assembly Definition Reference 创建引用文件 Assembly Definition 选中IoT.Module.Scene 即可完成代码的引用。

# 调试信息

控制台:可通过IoTCenter 3D 提供控制台(可通过按键**~** 快捷调出)

# 入门

# 三维场景

# 创建项目

详情

操作步骤 项目->新项目->编辑器版本->所有模板->Universal 3D 核心模板->项目名称->位置->创建项目

# 导入开发包

详情

操作步骤 Assets->Import Package->Custom Package -> 选中我们提供的二开包然后Import 。也可以直接将二开包拖到Assets目录下。

# 模型导入

详情 将FBX模型和贴图选中拖到Assets 下对应的文件夹中,并选中模型设置材质Materials->Location->Use External Materials(Legacy) 这样便可对模型的材质参数进行更改

# 场景搭建

详情

新建一个Unity场景,将模型拖入场景中然后对场景模型位置、地形、场景结构等进行调整。

# 结构说明
详情
  1. Effect:场景后期效果。
  2. Lights:灯光和反射探头。
  3. Objects:场景静态模型文件包括地形、周边建筑等。
  4. SceneRoot:场景设备组包括建筑、楼层、园区设备等。

如下图:

# 场景渲染

主要对场景进行模型材质、天空盒、光照、反射探头进行调整

# 楼层动画

# 楼层结构
详情 建筑模型需要按照如下图的建筑结构建模。

# 添加楼层脚本
详情
  1. 在ScenRoot上添加脚本 ScenRoot、MetaDataComponent,MetaDataComponent 脚本属性AssetType 设置为Instance(注:在Unity 场景编辑器中添加的设备都需要设置为 Instance 代表它是静态不可删除的)。
  2. 在 A塔 上添加Dev_Building 脚本,将 外墙 赋值到WallMesh属性上 ,设置属性Icon建筑图标文件。
  3. 在 楼层 上添加 MetaDataComponent 并设置Instance。
  4. 在 1F-46F 全选添加 Dev_Floor脚本

# 点击事件及视角
详情

1.在外墙玻璃 添加MeshCollider 用来响应点击事件。 2.选中A塔 在 Dev_Building 点击 设置为当前视角。运行Unity编辑器即可看到点击效果。

# 打包

# 场景AB包设置
详情

1.选中场景文件demo,在Inspector 最下面 AssetLabels ->AssetBundle -> new 设置为demo 拓展名设置 assetbundle.

注意

Unity场景名称和AssetBundle名称保持一致小写,不然会导致加载错误。

# 打包场景

# 加载场景

在IoTCenter 3D 中加载场景。打开编辑器->添加场景->选中demo场景文件即可完成加载。

# 设备开发

# 设备模型导入

同场景模型导入,这里不在赘述。

# 设备创建

详情

如下图:将设备模型拖入场景中,在设备下面创建一个空物体(注:这样做的原因是调整设备的中心点位置),调整它的位置。然后拖出来,再把模型放到这个物体中,拖到Devices目录下形成预制体设备。

# 脚本创建

详情

创建Dev_EntranceDoor 脚本文件,继承 DeviceBase,添加到设备上并保存预制体。

# 设备属性说明
详情

设备脚本属性如下图:

  1. DataType:IoT设备类型 Equip:标准设备;YC:遥测设备;YX:遥信设备。
  2. EquipNo:关联IoT设备号。
  3. Name:设备名称。
  4. Type:设备类型。
  5. ValueCMD:设备值命令配置。
  6. 设置为当前视角:可在Unity编辑器里设备设备默认视角。
  7. 查看设备视角:可查看设置的视角。
  8. CanLocat:是否可定位设备。
  9. ExceedDistance:设备取消聚焦的距离
  10. Animation:设备动画。
  11. MovedSpeed : 摄像机聚焦设备的速度。
  12. IsCreatIcon:是否创建设备图标。
  13. Icon:设备图标资源。
  14. DeviceIconTemplate : 设备图标模板,定制化的图标可以在此项赋值。
  15. DeviceFormId:设备聚焦后的弹窗面板。
  16. ClassifyShow:是否在大楼/楼层的统计面板中显示分类。
  17. ShowShadow:聚焦后是否显示光照阴影。

# 设备脚本说明
详情
  • 重载DeviceBase提供的模板方法。
using UnityEngine;
using System;
using IoT3D.Framework;
using IoT3D.Framework.UI;
using UnityEngine.Events;
using UnityEngine.UI;

public abstract class DeviceBase : DeviceLogic
{
    [SerializeField] private DeviceFormId deviceFormId = DeviceFormId.DeviceInfoForm;
    /// <summary>
    /// 设备聚焦界面ID
    /// </summary>
    [SerializeProperty]
    public DeviceFormId DeviceFormId
    {
        get => deviceFormId;
        set
        {
            deviceFormId = value;
        }
    }
    /// <summary>
    /// 设备界面
    /// </summary>
    protected IUIForm m_DeviceForm;
    /// <summary>
    /// 设备告警弹窗界面
    /// </summary>
    [HideInInspector]
    public DeviceAlarmUIForm m_DeviceAlarmForm;
    /// <summary>
    /// 设备是否在建筑/楼层统计界面中显示分类
    /// </summary>
    public bool ClassifyShow = true;
    /// <summary>
    /// 是否显示设备光影
    /// </summary>
    public bool ShowShadow = true;

    /// <summary>
    /// 相机取消聚焦设备距离
    /// </summary>
    [SerializeProperty]
    public float DistanceClose
    {
        get { return ExceedDistance; }
        set { ExceedDistance = value; }
    }
    protected override void Awake()
    {
    }
    protected override void Start()
    {
        base.Start();
    }
    /// <summary>
    /// 初始化设备
    /// </summary>
    protected override void OnInit()
    {
        base.OnInit();
    }
    /// <summary>
    /// 当设备点击时调用
    /// </summary>
    protected override void OnClick()
    {
        base.OnClick();
    }
    /// <summary>
    /// 显示设备图标
    /// </summary>
    public override void Show()
    {
        ShowIcon();
        IsShow = true;
        base.Show();
    }
    /// <summary>
    /// 隐藏设备图标
    /// </summary>
    public override void Hide()
    {
        if (m_DeviceForm != null)
        {
            UIModule.CloseUIForm(m_DeviceForm);
        }
        IsShow = false;
        HideIcon();
        base.Hide();
    }

    // private float distanceClose;

    /// <summary>
    /// 相机聚焦设备时调用
    /// </summary>
    public override void CameraMoved()
    {
        if (DeviceFormId != DeviceFormId.None)
        {
            m_DeviceForm = UIModule.OpenUIForm(DeviceFormId.ToString(), "UI", this);
        }
        base.CameraMoved();
        deviceIcon?.OnSelect();

    }
    /// <summary>
    /// 相机取消聚焦设备时调用
    /// </summary>
    public override void CameraLeaved()
    {
        if (m_DeviceForm != null)
        {
            UIModule.CloseUIForm(m_DeviceForm);
            m_DeviceForm = null;
        }

        deviceIcon?.UnSelect();
        base.CameraLeaved();
    }
    /// <summary>
    /// 定位设备时
    /// </summary>
    public override void OnLocat()
    {
        base.OnLocat();
    }
    protected virtual void Update()
    {
    }
    /// <summary>
    /// 显示设备图标
    /// </summary>
    protected void ShowIcon()
    {
        SetIconState(true);
    }
    /// <summary>
    /// 隐藏设备图标
    /// </summary>
    protected void HideIcon()
    {
        SetIconState(false);
    }

    /// <summary>
    /// 设置设备告警
    /// </summary>
    /// <param name="isWar"></param>
    public override void SetAlarm(bool isWar)
    {
        base.SetAlarm(isWar);
        deviceIcon?.SetState();
    }
    private void OnDisable()
    {
        SetIconState(false);
    }
    private void OnEnable()
    {
        SetIconState(IsShow);
    }
    /// <summary>
    /// 显示或隐藏设备图标
    /// </summary>
    /// <param name="isOpen"></param>
    private void SetIconState(bool isOpen)
    {
        if (deviceIcon != null)
        {
            if (isOpen)
            {
                deviceIcon.Show();
            }
            else
            {
                deviceIcon.Hide();
            }
        }
    }
    /// <summary>
    /// 设置设备图标是否可点击
    /// </summary>
    /// <param name="interactable"></param>
    public void SetIconInteractable(bool interactable)
    {
        Button iconBtn = deviceIcon.GetComponentInChildren<Button>();
        if (iconBtn != null)
        {
            iconBtn.interactable = interactable;
        }
    }
    /// <summary>
    /// 重置资源设备MetaID
    /// </summary>
    [ContextMenu("BuildID")]
    private void OnBuildId()
    {
        MetaDataComponent metaData = GetComponent<MetaDataComponent>();
        if (metaData == null)
        {
            Debug.LogError("没有找到:MetaDataComponent");
            metaData = gameObject.GetOrAddComponent<MetaDataComponent>();
        }
        if (string.IsNullOrEmpty(metaData.Id))
        {
            metaData.BuildResourceID();
        }
    }
}

# IoT设备关联动画

详情
  1. 这里使用IoTCenter平台已创建好的IoT设备。它有一个遥信状态:开关状态,两个设置项:开门/关门。

  1. 三维设备关联IoT设备号时可获得IoT的实时数据。

  1. 通过IoT设备开关门的设置,状态发生变化时三维设备的动画也会同时播放对应的动画。

下面是具体的脚本实现

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using IoT3D.Framework;

public class Dev_EntranceDoor : DeviceBase
{
    /// <summary>
    /// 状态数据
    /// </summary>
    YxItemData status;

    protected override void Awake()
    {
        base.Awake();
        m_Animation = GetComponentInChildren<Animation>();
        m_Animation.playAutomatically = false;
    }
    
    public override void Show()
    {
        base.Show();
        // 获取状态数据
        status = Device.IoTYxDatas[0];

        if (status != null)
        {
            //订阅状态事件
            status.PropertyChanged += YxValue_PropertyChanged;
        }
    }
    private void YxValue_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "State")
        {
            YxItemData itemData = sender as YxItemData;
            if (itemData != null)
            {
                //
                if (itemData.State == "开")
                {
                    AnimationPlay("open");
                }
                else
                {
                    AnimationPlay("close");
                }
            }
        }

    }
    public override void AnimationPlay(string aniName)
    {
        base.AnimationPlay(aniName);
    }
    public override void Hide()
    {
        base.Hide();
        if(status!=null)
        {
            status.PropertyChanged -= YxValue_PropertyChanged;
        }
    }
}


# 属性拓展

详情

编辑 Dev_EntranceDoor 脚本新增Color 并新增特性 [SerializeProperty],标识该属性会被IoT3D编辑器序列化和反序列化。

    private Color m_Color;

    /// <summary>
    /// 大门颜色属性
    /// </summary>
    [SerializeProperty]
    public Color Color
    {
        get 
        { 
            return m_Color; 
        }
        set
        { 
            m_Color = value;
            material.color = m_Color;
        }
    }

# 打包设置

详情

同场景AB打包流程一致。需要注意的是 AssetBundle 名称设置为deviceres

# UI控件开发

UI控件支持基于UGUI开发方式。

# 创建UI控件

详情
  1. 创建一个列表控件用于展示Equip数据表信息。
  2. 在场景创建一个Canvas,创建UI->Image 然后改名 UI_EquipDataList, 将列表控件ListView拖到 'UI_EquipDataList'下面并设置RectTransform属性如下:
  3. 拖到UIControls目录下构建成预制体。

# 脚本创建

详情
  1. 创建'UI_EquipDataList'脚本文件,继承UIControl。
  2. 挂载到UI控件上并设置名称 '设备数据表',并添加MetaDataComponent 脚本,AssetType 设置 Resource

# 脚本说明
详情
  • 重载UIControl提供的模板方法。
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Networking;

namespace IoT3D.Framework.UI;

public abstract class UIControl : MonoBehaviour, IUIControl
{
    /// <summary>
    /// 资源分类一般使用Custom, Basics:基础,None:未识别,Other:其它,Custom:定制化,LineChart:折线线图,
    /// CurveChart:曲线图,PieChart:饼图,RingChart:环形图,BarChart:柱状图,Button:按钮,
    /// BackgroundBlock:背景块,Text:文本,List:列表,Navigation:列表,Video:视频
    /// </summary>
    public abstract UIResEnum uIResEnum { get; }
    /// <summary>
    /// 获取RectTransform
    /// </summary>
    public RectTransform rectTransform => GetComponent<RectTransform>();
    [NonSerialized]
    protected Canvas canvas;
    /// <summary>
    /// 控件所在的Canvas
    /// </summary>
    public Canvas RectCanvas
    {
        get
        {
            if (canvas == null)
            {
                canvas = base.gameObject.GetComponentInParent<Canvas>();
            }

            return canvas;
        }
        set
        {
            canvas = value;
        }
    }
    /// <summary>
    /// 当前状态
    /// </summary>
    public bool IsOpen
    {
        get
        {
            return m_IsOpen;
        }
        set
        {
            m_IsOpen = value;
        }
    }
    /// <summary>
    /// 控件名称
    /// </summary>
    public string Name
    {
        get
        {
            return m_name;
        }
        set
        {
            m_name = value;
        }
    }
    /// <summary>
    /// 控件资源id
    /// </summary>
    public string ResId
    {
        get
        {
            return null;
        }
    }
    /// <summary>
    /// IoT3D加载序列化后
    /// </summary>
    public virtual void OnSerialized()
    {
    }
    /// <summary>
    /// 设置所有更改
    /// </summary>
    public virtual void SetAllDirty()
    {
    }
    /// <summary>
    /// 控件初始化后调用
    /// </summary>
    public virtual void OnInit()
    {
    }
    /// <summary>
    /// 切换业务场景后调用
    /// </summary>
    public virtual void OnOpen()
    {
    }
    /// <summary>
    /// 刷新数据每10秒一次
    /// </summary>
    public virtual void RefreshData()
    {
    }
    /// <summary>
    /// 更新控件UI
    /// </summary>
    public virtual void UpdateUI()
    {
    }
    /// <summary>
    /// 控件关闭时调用
    /// </summary>
    public virtual void OnClose()
    {
    }
    /// <summary>
    /// 重新创建资源Id
    /// </summary>
    [ContextMenu("CreateResId")]
    private void CreateResId()
    {
        if (GetComponent<MetaDataComponent>() == null)
        {
            base.gameObject.AddComponent<MetaDataComponent>().BuildResourceID();
        }
    }
   
}

# IoT数据对接

详情
  1. 创建一个EquipDataItem脚本用于显示数据库数据。并挂载到列表Item上如下操作。

  1. 创建一个类 EquipDataModel 用于显示以下表数据


using IoT.Module.Common;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class EquipDataItem : UniversalItem
{
    public TextMeshProUGUI sta_n, equip_no, equip_nm, equip_detail, proc_advice;

    /// <summary>
    /// 设置Item 数据
    /// </summary>
    /// <param name="item"></param>
    public override void SetData(IUniversalData item)
    {
        base.SetData(item);
        EquipDataModel equipData = (EquipDataModel)item;
        if(equipData !=null)
        {
            sta_n.text = equipData.sta_n;
            equip_no.text = equipData.equip_no;
            equip_nm.text = equipData.equip_nm;
            equip_detail.text = equipData.equip_detail;
            proc_advice.text = equipData.proc_advice;
        }
    }
}

public class EquipDataModel : IUniversalData
{
    /// <summary>
    /// 站点
    /// </summary>
    public string sta_n { get; set; }
    /// <summary>
    /// 设备号
    /// </summary>
    public string equip_no { get; set; }

    /// <summary>
    /// 设备名称
    /// </summary>
    public string equip_nm { get; set; }

    /// <summary>
    /// 通行协议
    /// </summary>
    public string equip_detail { get; set; }

    /// <summary>
    /// 建议
    /// </summary>
    public string proc_advice { get; set; }

}

  1. 修改表头和Item赋值

4.编辑UI_EquipDataList脚本,通过RPCModule 获取表数据。内容如下


using IoT.Module.Common;
using IoT3D.Framework;
using IoT3D.Framework.UI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UI_EquipDataList : UIControl
{
    /// <summary>
    /// 列表控件
    /// </summary>
    private UniversalListView listView;
    public override UIResEnum uIResEnum =>  UIResEnum.Custom;

    /// <summary>
    /// 控件显示的时候
    /// </summary>
    public override void OnOpen()
    {
        listView = GetComponentInChildren<UniversalListView>();
        base.OnOpen();
        GetData();
    }
    /// <summary>
    /// 获取表数据
    /// </summary>
    async void GetData()
    {
        string sql = "select *from Equip";
        List<EquipDataModel> equipDatas = await RPCModule.GetAsyncSQLData<List<EquipDataModel>>(sql);
        if(equipDatas!=null)
        {
            listView.DataSource = new UIWidgets.ObservableList<IUniversalData>(equipDatas);
        }
    }
    /// <summary>
    /// 控件关闭的时候
    /// </summary>
    public override void OnClose()
    {
        base.OnClose();
    }

}

# 打包设置

# 部署控件
详情

显示Equip 表数据。

# UI面板开发

# 创建UI面板

详情

我们要创建一个大门设备的信息面板替换掉默认面板,当设备聚焦时显示它当前状态。

1.选中UI面板模板 FormTemplate (注:也可以不用模板根据自己的需求),复制一个出来然后改成DoorInfoForm。

# 脚本创建

创建 DoorInfoForm 继承 DeviceUIFormBase,挂载到UI面板上并保存预制体。

# 脚本说明
详情
  • 重载DeviceUIFormBase提供的模板方法。
using DG.Tweening;
using IoT3D.Framework;
using IoT3D.Framework.UI;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DeviceUIFormBase : UIFormLogic
{
    public const int DepthFactor = 100;
    private const float FadeTime = 0.3f;

    private Canvas m_CachedCanvas = null;
    private CanvasGroup m_CanvasGroup = null;
    private List<Canvas> m_CachedCanvasContainer = new List<Canvas>();

    /// <summary>
    /// 界面原始层级
    /// </summary>
    public int OriginalDepth
    {
        get;
        private set;
    }
    /// <summary>
    /// 界面当前层级
    /// </summary>
    public int Depth
    {
        get
        {
            return m_CachedCanvas.sortingOrder;
        }
    }
    /// <summary>
    /// 关闭界面
    /// </summary>
    public virtual void Close()
    {
        Close(false);
    }

    /// <summary>
    /// 关闭界面
    /// </summary>
    /// <param name="ignoreFade">是否使用动画</param>
    public void Close(bool ignoreFade)
    {
        if (ignoreFade)
        {
            UIModule.CloseUIForm(UIForm);
        }
        else
        {
            if (gameObject == null)
                return;
            if (gameObject.activeSelf)
            {
                StartCoroutine(CloseCo(FadeTime));
            }

        }
    }
    /// <summary>
    /// 当初始化界面
    /// </summary>
    /// <param name="userData">用户参数</param>
    public override void OnInit(object userData)
    {
        base.OnInit(userData);

        m_CachedCanvas = gameObject.GetOrAddComponent<Canvas>();
        m_CachedCanvas.overrideSorting = true;
        OriginalDepth = m_CachedCanvas.sortingOrder;

        m_CanvasGroup = gameObject.GetOrAddComponent<CanvasGroup>();

        RectTransform transform = GetComponent<RectTransform>();
        transform.anchorMin = Vector2.zero;
        transform.anchorMax = Vector2.one;
        transform.anchoredPosition = Vector2.zero;
        transform.sizeDelta = Vector2.zero;

        gameObject.GetOrAddComponent<GraphicRaycaster>();
    }
    /// <summary>
    /// 当界面回收时
    /// </summary>
    public override void OnRecycle()
    {
        base.OnRecycle();
    }

    /// <summary>
    /// 当界面打开时
    /// </summary>
    /// <param name="userData"></param>
    public override void OnOpen(object userData)
    {
        m_CanvasGroup.alpha = 0f;
        DOTween.Kill(m_CanvasGroup);
        m_CanvasGroup.DOFade(1, FadeTime);
        m_CanvasGroup.blocksRaycasts = true;
        base.OnOpen(userData);
    }
    /// <summary>
    /// 当界面关闭时
    /// </summary>
    /// <param name="isShutdown"></param>
    /// <param name="userData"></param>
    public override void OnClose(bool isShutdown, object userData)

    {
        base.OnClose(isShutdown, userData);
    }
    /// <summary>
    /// 当界面暂停时
    /// </summary>
    public override void OnPause()

    {
        base.OnPause();
    }
    /// <summary>
    /// 当界面恢复时
    /// </summary>
    public override void OnResume()

    {
        m_CanvasGroup.alpha = 0f;
        DOTween.Kill(m_CanvasGroup);
        m_CanvasGroup.DOFade(1, FadeTime);
        base.OnResume();
    }
    /// <summary>
    /// 当界面遮盖时
    /// </summary>
    public override void OnCover()

    {
        base.OnCover();
    }
    /// <summary>
    /// 当界面显示时
    /// </summary>
    public override void OnReveal()
    {
        base.OnReveal();
    }
    /// <summary>
    /// 当界面重新聚焦时
    /// </summary>
    /// <param name="userData"></param>
    public override void OnRefocus(object userData)

    {
        base.OnRefocus(userData);
    }
    /// <summary>
    /// 界面更新
    /// </summary>
    /// <param name="elapseSeconds"></param>
    /// <param name="realElapseSeconds"></param>
    public override void OnUpdate(float elapseSeconds, float realElapseSeconds)

    {
        base.OnUpdate(elapseSeconds, realElapseSeconds);
    }
    /// <summary>
    /// 界面层级改变时
    /// </summary>
    /// <param name="uiGroupDepth"></param>
    /// <param name="depthInUIGroup"></param>
    public override void OnDepthChanged(int uiGroupDepth, int depthInUIGroup)
    {
        int oldDepth = Depth;
        base.OnDepthChanged(uiGroupDepth, depthInUIGroup);
        int deltaDepth = 1000 * uiGroupDepth + DepthFactor * depthInUIGroup - oldDepth + OriginalDepth;
        GetComponentsInChildren(true, m_CachedCanvasContainer);
        for (int i = 0; i < m_CachedCanvasContainer.Count; i++)
        {
           // Debug.LogError(deltaDepth);
            m_CachedCanvasContainer[i].sortingOrder += deltaDepth;
        }

        m_CachedCanvasContainer.Clear();
    }
    /// <summary>
    /// 界面关闭操作
    /// </summary>
    /// <param name="duration"></param>
    /// <returns></returns>
    private IEnumerator CloseCo(float duration)
    {
        if (m_CanvasGroup == null)
        {

            UIModule.CloseUIForm(UIForm);
            yield break;
        }

        m_CanvasGroup.blocksRaycasts = false;
        yield return new WaitForEndOfFrame();
        DOTween.Kill(m_CanvasGroup);
        m_CanvasGroup.DOFade(0, FadeTime);
        UIModule.CloseUIForm(UIForm);
    }

    /// <summary>
    /// 界面设置告警时
    /// </summary>
    /// <param name="isWar"></param>
    public virtual void SetAlarm(bool isWar)
    {

    }
}

# 绑定设备数据

详情
  1. 在枚举DeviceFormId脚本中添加 DoorInfoForm

  1. 编辑DoorInfoForm面板,新增一个Slider和Text 用于控制和显示门的状态。

  1. 编写脚本前我们需要去IoTCenter 平台上获取设备的控制信息,开门的设置点是1,关门是6。

  1. 编写脚本如下:
using IoT3D.Framework;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class DoorInfoForm : DeviceUIFormBase
{
    private Slider doorSwitch;
    private TextMeshProUGUI statusText;
    private YxItemData status;

    /// <summary>
    /// 界面打开时
    /// </summary>
    /// <param name="userData"></param>
    public override void OnOpen(object userData)
    {
        base.OnOpen(userData);
        //获取开关控件
        doorSwitch = GetComponentInChildren<Slider>();
        doorSwitch.onValueChanged.AddListener(OnSwitchChanged);
        //获取状态控件
        statusText = doorSwitch.GetComponentInChildren<TextMeshProUGUI>();

        Dev_EntranceDoor dev_Entrance = (Dev_EntranceDoor)userData;

        if(dev_Entrance!=null)
        {
            this.status = dev_Entrance.status;
            if(status!=null)
            {
                doorSwitch.SetValueWithoutNotify(status.State == "开" ? 1 : 0);
                statusText.text = status.State;
            }
        }
    }
    /// <summary>
    /// 开关响应事件
    /// </summary>
    /// <param name="value"></param>
    void OnSwitchChanged(float value)
    {
        bool isOpen = value > 0;
        if(isOpen)
        {
            //下发开门指令
            RPCModule.Rpc.SetParam(1,1);
        }
        else
        {
            //下发关门指令
            RPCModule.Rpc.SetParam(1, 6);
        }
        statusText.text = isOpen? "开":"关";
    }
    public override void OnClose(bool isShutdown, object userData)
    {
        base.OnClose(isShutdown, userData);
    }
}

  1. 打包运行IoT3D 选中在场景已部署的大门设备 DeviceFormId设置为DoorInfoForm

# 打包设置

详情

同场景AB打包流程一致。需要注意的是 AssetBundle 名称设置为uiforms

# 进阶

# 场景漫游

详情

以下视频介绍创建漫游路径->调整路径->调整角度和速度->在平台中的控制方法。

教程链接 (opens new window)

# 场景事件

详情

通过订阅以下场景事件做一些定制化的业务。例如:当切换到某个场景时立即开始漫游。

    //当场景切换后响应
    SceneModule.AddSceneChangedListener(OnSceneChanged);
    //当场景切换前响应
    SceneModule.AddSceneChangedBeforeListener(OnSceneChangedBefore);
       
    private void OnSceneChenged(SceneNode sceneNode)
    {
        if (sceneNode.SceneName == "安防态势")
        {
             Debug.Log("Do something");
        }
    }

# 第一人称视角

详情
  1. 项目中搜索 FirstPlayer 然后拖入场景中,调整角度和位置->隐藏后->在SceneRoot的Player上赋值这个对象。
  2. 在IoT3D运行时,按F2即可使用第一人称视角了。再次按F2既可还原视角。
  3. SceneRoot 脚本解析: 开发者可根据自己的需求修改脚本。需要注意的是要保证第一人称视角在碰撞体之上。
    private void Update()
    {
        //监听键盘用户是否按下F2
        if (Input.GetKeyDown(KeyCode.F2))
        {
            if (player != null)
            {
                isFirstPerson = !isFirstPerson;
                //隐藏平台摄像头
                SceneModule.SetCameraActive(!isFirstPerson);
                //显示第一人称视角
                player.gameObject.SetActive(isFirstPerson);
            }
        }
    }

# 设备定制图标

设备默认使用的是平台提供的图标。如果想更改,需要进行以下操作。

# 创建定制图标

在项目中搜索DeviceIconTemplate 复制一个出来, 双击进入预制体编辑器对图标样式进行修改。

# 脚本解析

详情

一般使用默认的DeviceIcon脚本。如果添加脚本只需继承DeviceIcon即可。 以下是DeviceIocn脚本模板

using DG.Tweening;
using IoT3D.Framework;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class DeviceIcon : DeviceIconBase
{
    /// <summary>
    /// 图标按钮
    /// </summary>
    public Button IconClickBtn;
    /// <summary>
    /// 当前设备
    /// </summary>
    private DeviceBase device;
    /// <summary>
    /// 设备文本
    /// </summary>
    public TextMeshProUGUI TitleName;

    private void Awake()
    {
    }
    /// <summary>
    /// 初始化图标
    /// </summary>
    /// <param name="userData"></param>
    public override void OnInit(object userData)
    {
        base.OnInit(userData);
        gameObject.SetActive(false);
        IconClickBtn.onClick.AddListener(OnIconClick);
        Icon.color = Color.white;
        if (userData is DeviceBase device)
        {
            this.device = device;
            transform.UIFollow(device.transform);
            if (device != null)
            {
                if (device.m_Icon != null)
                {
                    Icon.sprite = device.m_Icon;
                    SetState();
                    TitleName.text = device.name;
                }
                if (device.IsShow)
                {
                    Show();
                }
            }
        }
    }

    /// <summary>
    /// 实时刷新图标位置和状态
    /// </summary>
    private void Update()
    {
        if (device != null)
        {
            if (device.IsShow)
            {
                transform.UIFollow(device.transform);
                SetState();
            }
        }
        else
        {
            Destroy(gameObject);
        }
    }
    /// <summary>
    /// 当图标点击时
    /// </summary>
    public override void OnIconClick()
    {
        DeviceModule.manager.SetSelection(device);
    }
    /// <summary>
    /// 设置图标状态
    /// </summary>
    public override void SetState()
    {
      
    }
    public override void OnDisable()
    {
    }
    /// <summary>
    /// 显示图标
    /// </summary>
    public override void Show()
    {
        gameObject.SetActive(true);
        transform.localScale = Vector3.zero;
        StartCoroutine(ShowIcon());
    }

    IEnumerator ShowIcon()
    {
        yield return new WaitForEndOfFrame();
        if(transform!=null)
        {
            DOTween.Kill(transform);
        }
        transform.DOScale(1, 0.5f);
    }
    /// <summary>
    /// 隐藏图标
    /// </summary>
    public override void Hide()
    {
        if (transform != null)
        {
            DOTween.Kill(transform);
        }
        gameObject.SetActive(false);
    }

    /// <summary>
    /// 鼠标在图标上
    /// </summary>
    public override void MouseEnter()
    {
    }
    /// <summary>
    /// 鼠标退出图标
    /// </summary>
    public override void MouseExit()
    {
    }

    /// <summary>
    /// 图标选中
    /// </summary>
    public override void OnSelect()
    {
    }
    /// <summary>
    /// 取消图标选中
    /// </summary>
    public override void UnSelect()
    {
        if (IconClickBtn != null)
        {
            IconClickBtn.interactable = true;
        }
    }
}

# 绑定到设备

详情

将做好的图标关联到三维设备上、然后保存设备。

# 查看效果

# UI控件与三维场景交互

详情
  1. 我们先创建一个大门的设备列表。过程是标准的UI控件开发方式,需要创建如下几个文件和UI界面。

  1. 获取场景内已部署的三维设备。

编写UI_DoorList脚本。如下

public class UI_DoorList : UIControl
{
    public override UIResEnum uIResEnum =>  UIResEnum.Custom;
    private UniversalListView listView;
    /// <summary>
    /// 控件显示时
    /// </summary>
    public override void OnOpen()
    {
        base.OnOpen();
        listView = GetComponentInChildren<UniversalListView>();
        //获取场景内所有大门设备
        List<Dev_EntranceDoor> dev_EntranceDoors = FindObjectsByType<Dev_EntranceDoor>(FindObjectsSortMode.None).ToList();

        Debug.LogError(dev_EntranceDoors.Count);
        List<DoorListData> entranceDoors = new();
        foreach (var item in dev_EntranceDoors)
        {
            DoorListData data = new DoorListData { door = item };
            entranceDoors.Add(data);
        }

        //显示到列表中
        listView.DataSource = new UIWidgets.ObservableList<IUniversalData>(entranceDoors);
    }
    public override void OnClose()
    {
        base.OnClose();
    }
}
/// <summary>
/// 列表数据模型
/// </summary>
public class DoorListData : IUniversalData
{
    public Dev_EntranceDoor door;
}
  1. 然后控制大门开关动画。
public class DoorListItem : UniversalItem
{
    public TextMeshProUGUI Id, Name, Status;
    public Slider m_switchSlider;
    DoorListData data;

    protected override void Start()
    {
        base.Start();
        m_switchSlider.onValueChanged.AddListener(OnSwitchChanged);
    }
    public override void SetData(IUniversalData item)
    {
        base.SetData(item);
        data = item as DoorListData;
        //列表内属性赋值
        if(data != null)
        {
            Id.text = data.door.DeviceData.EquipNo.ToString();
            Name.text = data.door.name;
            YxItemData status = data.door.status;
            Status.text = status.State;
        }
    }
    void OnSwitchChanged(float value)
    {
        bool isOpen = value > 0;
        if (isOpen)
        {
            //下发开门指令
            RPCModule.Rpc.SetParam(1, 1);
        }
        else
        {
            //下发关门指令
            RPCModule.Rpc.SetParam(1, 6);
        }
        Status.text = isOpen ? "开" : "关";
    }
}
  1. 部署到场景看看效果。

# 常见问题

# 打包后代码丢失

检查脚本设置,在Unity 编辑器查看脚本 Assembly Information->Filename 是否属于IoT.Module.Scene.dll。

# 使用第三方插件

在插件源码脚本目录下创建 Assembly Definition Reference 并在 Assembly Definition 选中IoT.Module.Scene 即可完成代码的引用。注意要剥离UnityEditor脚本。

# 设备列表不显示静态设备

检查场景是否创建SceneRoot对象并添加SceneRoot脚本,保证静态设备都在SceneRoot下面,每层都要挂载MetaDataComponent脚本。

# 场景加载错误

1.检查场景名称和Assetbundle 包名是否保持一致并且小写。

2.在Unity编辑器中BuildSettings->AddOpenScenes 添加你的场景。

# 可以访问IoTCenter的哪些数据?通过什么接口访问?

答:1.设备数据(测点/控制)、数据库 可通过API 'RPCModule' 下面的接口访问。 2.WebAPI 通过API URL鉴权的方式访问。如下

   ```csharp
    UnityWebRequest webRequest = UnityWebRequest.Get($"http://127.0.0.1:44381/IoT/api/v3/Auth/userinfo");
    //加上Web的鉴权信息。
    webRequest.SetIoTRequestHeader();
   ``` 

# 是否支持Web版本

答:二开的方式支持Web版本,可以使用云渲染或SDK的开发方式支持。

SDK: 开发方式指的是使用Unity插件开发,开发者可以自行打包对应的平台。

云渲染 (opens new window):客户端部署云端渲染,渲染结果以视频流的方式呈现到浏览器,这样就实现了多终端交互。

上次更新: 2024/10/11 14:01:13

← 常见问题 SDK 开发方式→

目录
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式