概述
# 概述
IoT3D与Unity的关系
IoT3D 是基于Unity开发的数字孪生平台,服务端使用的IoTCenter物联网平台。
# 为什么要使用IoT3D
详情
为什么要这样开发? 直接在Unity编辑器中开发不是更快速吗? 如下列举几个常见的场景:
- 客户需要删除、改装或更新现有设备。
- 客户有新的设备需要部署。
- 客户想更新设备控制。
- UI界面需要更新数据。
- 客户想要删除某个模块的UI界面。
- 客户想换一种风格和布局。
- 客户想修改UI分辨率。
- 客户想改个项目名和场景去演示。
诸如此类常见的需求是要在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打包方式;
- Configure:三维场景、三维设备、UI面板、UI控件等资源管理。
- Build:打包三维场景、三维设备、UI面板、UI控件及脚本到IoTCenter 3D平台。
- Build Target :打包的平台。
- Output Path :IoTCenter 3D所在的路径。
- Compression :一般使用LZ4的压缩方式。
- Force Rebuild :强制重新打包。
- Build : 全量
- OnlyCopyScripts:仅打包脚本。
- RunIoTCenter3D :运行IoTCenter3D平台。
# 资源设置
详情
资源设置方式有两种:
- 在选中Assets的文件,Inspector 面板的最下端,在AssetLabels 中设置Assetbundle资源。
- 拖动Assets下的文件,到AssetBundle Browser 对应的Asset下既可。
# 资源说明
详情
打包资源主要分为:三维设备,UI控件,UI面板,三维场景。 deviceres : 三维设备的资源包。 uicontrols :UI控件的资源包。 uiforms :UI面板资源包。 以上三个包是固定的名称不可更改。 scene_1 : 场景包,可跟随项目更改名称。
# 脚本设置
详情
AssetBundle 打包是不包含脚本的。所有脚本需要我们自己关联到 IoT.Module.Scene 这个库中,有两种关联方式。
- 如下图, 保证你的脚本和 IoT.Module.Scene 在相同目录中。
- 如下图,重新创建文件夹,右键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场景,将模型拖入场景中然后对场景模型位置、地形、场景结构等进行调整。
# 结构说明
详情
- Effect:场景后期效果。
- Lights:灯光和反射探头。
- Objects:场景静态模型文件包括地形、周边建筑等。
- SceneRoot:场景设备组包括建筑、楼层、园区设备等。
如下图:
# 场景渲染
主要对场景进行模型材质、天空盒、光照、反射探头进行调整
# 楼层动画
# 楼层结构
详情
建筑模型需要按照如下图的建筑结构建模。# 添加楼层脚本
详情
- 在ScenRoot上添加脚本 ScenRoot、MetaDataComponent,MetaDataComponent 脚本属性AssetType 设置为Instance(注:在Unity 场景编辑器中添加的设备都需要设置为 Instance 代表它是静态不可删除的)。
- 在 A塔 上添加Dev_Building 脚本,将 外墙 赋值到WallMesh属性上 ,设置属性Icon建筑图标文件。
- 在 楼层 上添加 MetaDataComponent 并设置Instance。
- 在 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,添加到设备上并保存预制体。
# 设备属性说明
详情
设备脚本属性如下图:
- DataType:IoT设备类型 Equip:标准设备;YC:遥测设备;YX:遥信设备。
- EquipNo:关联IoT设备号。
- Name:设备名称。
- Type:设备类型。
- ValueCMD:设备值命令配置。
- 设置为当前视角:可在Unity编辑器里设备设备默认视角。
- 查看设备视角:可查看设置的视角。
- CanLocat:是否可定位设备。
- ExceedDistance:设备取消聚焦的距离
- Animation:设备动画。
- MovedSpeed : 摄像机聚焦设备的速度。
- IsCreatIcon:是否创建设备图标。
- Icon:设备图标资源。
- DeviceIconTemplate : 设备图标模板,定制化的图标可以在此项赋值。
- DeviceFormId:设备聚焦后的弹窗面板。
- ClassifyShow:是否在大楼/楼层的统计面板中显示分类。
- 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设备关联动画
详情
- 这里使用IoTCenter平台已创建好的IoT设备。它有一个遥信状态:开关状态,两个设置项:开门/关门。
- 三维设备关联IoT设备号时可获得IoT的实时数据。
- 通过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控件
详情
- 创建一个列表控件用于展示Equip数据表信息。
- 在场景创建一个Canvas,创建UI->Image 然后改名 UI_EquipDataList, 将列表控件ListView拖到 'UI_EquipDataList'下面并设置RectTransform属性如下:
- 拖到UIControls目录下构建成预制体。
# 脚本创建
详情
- 创建'UI_EquipDataList'脚本文件,继承UIControl。
- 挂载到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数据对接
详情
- 创建一个EquipDataItem脚本用于显示数据库数据。并挂载到列表Item上如下操作。
- 创建一个类 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; }
}
- 修改表头和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)
{
}
}
# 绑定设备数据
详情
- 在枚举DeviceFormId脚本中添加 DoorInfoForm
- 编辑DoorInfoForm面板,新增一个Slider和Text 用于控制和显示门的状态。
- 编写脚本前我们需要去IoTCenter 平台上获取设备的控制信息,开门的设置点是1,关门是6。
- 编写脚本如下:
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);
}
}
- 打包运行IoT3D 选中在场景已部署的大门设备 DeviceFormId设置为DoorInfoForm
# 打包设置
详情
同场景AB打包流程一致。需要注意的是 AssetBundle 名称设置为uiforms
# 进阶
# 场景漫游
# 场景事件
详情
通过订阅以下场景事件做一些定制化的业务。例如:当切换到某个场景时立即开始漫游。
//当场景切换后响应
SceneModule.AddSceneChangedListener(OnSceneChanged);
//当场景切换前响应
SceneModule.AddSceneChangedBeforeListener(OnSceneChangedBefore);
private void OnSceneChenged(SceneNode sceneNode)
{
if (sceneNode.SceneName == "安防态势")
{
Debug.Log("Do something");
}
}
# 第一人称视角
详情
- 项目中搜索 FirstPlayer 然后拖入场景中,调整角度和位置->隐藏后->在SceneRoot的Player上赋值这个对象。
- 在IoT3D运行时,按F2即可使用第一人称视角了。再次按F2既可还原视角。
- 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控件与三维场景交互
详情
- 我们先创建一个大门的设备列表。过程是标准的UI控件开发方式,需要创建如下几个文件和UI界面。
- 获取场景内已部署的三维设备。
编写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;
}
- 然后控制大门开关动画。
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 ? "开" : "关";
}
}
- 部署到场景看看效果。
# 常见问题
# 打包后代码丢失
检查脚本设置,在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):客户端部署云端渲染,渲染结果以视频流的方式呈现到浏览器,这样就实现了多终端交互。