1 前言

        之前把云渲染Unity Render Streaming的配置好并把Demo跑起来了(云渲染),这次是对这个包进行了一些测试,对问题进行一些记录。

        另外,若使用云渲染,输入系统需要使用Input System。可以参考这个教程视频:Unity Input System

2 测试Demo场景

2.1 场景情况

        场景是基于Unity Render Streaming包所提供的的示例中Broadcast场景修改的。保留了场景原本右侧调整分辨率等属性的UI,以及开关灯UI,同时添加了一些文本框展示射线检测对象、鼠标位置、鼠标移动数值、从Web端接收的消息。场景中放了一个地面与一个Cube。

场景中脚本组成:

  1. SceneController.cs(自己创建,相当于Broadcast场景中的BroadcastSample脚本)
  2. PlayerController.cs(自己创建,挂载到Cube身上的脚本)
  3. MyRenderStreamingSettings.cs(自己创建,基本与官方的RenderStreamingSettings脚本一致)
  4. SignalingManager.cs(包中脚本)
  5. Broadcast.cs(包中脚本)
  6. VideoStreamSender.cs(包中脚本)
  7. AudioStreamSender.cs(包中脚本)
  8. InputReceiver.cs(包中脚本)

2.2 代码

SceneController.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.RenderStreaming;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

static class InputReceiverExtension
{
    public static void CalculateInputRegion(this InputReceiver reveiver, Vector2Int size)
    {
        reveiver.CalculateInputRegion(size, new Rect(0, 0, Screen.width, Screen.height));
    }
}

static class InputActionExtension
{
    public static void AddListener(this InputAction action, Action<InputAction.CallbackContext> callback)
    {
        action.started += callback;
        action.performed += callback;
        action.canceled += callback;
    }
}

/// <summary>
/// 场景控制类。
/// 
/// -------------------------Unity内存增加问题讨论!!!-------------------------
/// (UnityRenderStreaming包版本3.1.0-exp.9)
/// 在编辑器状态下:
/// 1、内存有时会出现逐渐快速增加的问题,最高达到过10G左右,也许会更高,但当鼠标点击编辑器,即聚焦编辑器时,内存又会快速下降恢复至正常。
/// 2、然后是发送消息问题,从Web向Unity发送消息,会在UnityStreaming包的Receiver.cs的OnMessage方法里接收到消息。这里本来是对Input消息接收处理的。
///    但若我们在web端通过通道发送消息,这里也能接收到。那么问题来了,若是我们自己的消息过来在OnMessage中被发送向下处理了(期间无报错),则Unity的内存占用会突然增加。
///    需要注意,若发送的message是重新创建的默认对象(InputRemoting.Message msg = default;),向下处理后,不会有这种问题。
///    这里自己的Demo是突然增加了几百到1G左右,且发送多次不一定哪次就会增加,增加到一定数值再多次测试都没有增加了(也可能是测试不够多?)。
///    并且这和前面自动增加不同,即使聚焦到编辑器,自动增加的内存会下降,但发送增加的内存却会保持住。
/// 在打包状态下:
/// 1、运行状态下内存会缓慢增加,逐渐累计,增加内存属于Unity划分到unknown类别的内存。经多天研究可以确定是包的问题,且尝试多种方法均无法解决,只有Unity程序关闭内存才能被释放掉。(版本3.1.0-exp.7也有此问题,不过3.1.0-exp.6无此问题)
/// 2、发送消息内存增加问题同编辑器。
/// 结论:
/// 1、编辑器状态下若内存增加,鼠标点击编辑器把焦点聚焦回来。
/// 2、打包运行Unity程序,内存将累计,勉强能使用。
/// 3、确保从Web发送给Unity的自定义消息在UnityStreaming包的Receiver.cs的OnMessage方法里不被onMessage执行。
///   (建议在OnMessage直接过滤掉自己的消息,自己的消息可以在InputReceiver.Channel.OnMessage这里监听处理)
/// PS:
/// 1、Mono、IL2CPP下,情况一致。
/// 
/// (UnityRenderStreaming包版本3.1.0-exp.6)
/// 编辑器状态下:
/// 1、内存不会自动增加。
/// 2、发送消息内存增加问题同版本9.
/// 3、刷新Web页面,内存有时候会增加一点,小到不到1MB,大到几MB,但此内存有时候会出现释放的情况,目前出现过释放部分的情况。(这虽然是个问题,但基本可以忽略)
/// 打包状态下:
/// 1、同编辑器。
/// 结论:
/// 1、与9版本相比,6版本没有了内存自动增加的问题,但有刷新Web页面,内存可能增加的问题(9版本也许也有,但在内存自动增加问题前可以忽略不计)。不过该问题在实际应用中可以忽略不计。
/// PS:
/// 1、编辑器下测试了Mono、IL2CPP。
/// 2、打包下只测了IL2CPP。(Mono大概率一样,实在是懒得测了)
/// 
/// 总结:
/// 1、用UnityRenderStreaming包版本3.1.0-exp.6。
/// 2、在Receiver.cs的OnMessage里做好数据过滤。
/// ---------------------------------------------------------------------------
/// 
/// -------------------------Receiver.cs中的OnMessage函数问题-------------------------
///  具体见脚本里函数的注释,说明已在那里添加。
/// ---------------------------------------------------------------------------
/// 
/// 
/// </summary>
public class SceneController : MonoBehaviour
{
    //对象
    [SerializeField] private SignalingManager renderStreaming;
    [SerializeField] private InputReceiver inputReceiver;
    [SerializeField] private VideoStreamSender videoStreamSender;
    [SerializeField] private Dropdown videoSourceTypeSelector;
    [SerializeField] private Dropdown bandwidthSelector;
    [SerializeField] private Dropdown scaleResolutionDownSelector;
    [SerializeField] private Dropdown framerateSelector;
    [SerializeField] private Dropdown resolutionSelector;
    [SerializeField] private PlayerController playerController;
    [SerializeField] private Text clickGameObjectName;
    [SerializeField] private Text mousePosition;
    [SerializeField] private Text mouseDelta;
    [SerializeField] private Text messageFromWeb;
    [SerializeField] private Camera theCamera;
    private NewControls newControls;            //行为控制器
    private MyRenderStreamingSettings settings; //渲染流配置

    //字段
    private Vector2 m_mousePosition;    //鼠标位置
    private Vector2 m_mouseDelta;       //鼠标动态
    private int lastWidth;  //窗口(Screen)最后宽度
    private int lastHeight; //窗口(Screen)最后高度

    #region 容器
    //视频资源类型选项
    private Dictionary<string, VideoStreamSource> videoSourceTypeOptions = new Dictionary<string, VideoStreamSource>
        {
            {"Screen", VideoStreamSource.Screen },
            {"Camera", VideoStreamSource.Camera },
            {"Texture", VideoStreamSource.Texture },
            {"WebCam", VideoStreamSource.WebCamera }
        };

    //带宽可选项
    private Dictionary<string, uint> bandwidthOptions =
        new Dictionary<string, uint>()
        {
                { "10000", 10000 },
                { "2000", 2000 },
                { "1000", 1000 },
                { "500",  500 },
                { "250",  250 },
                { "125",  125 },
        };

    //分辨率缩放选项
    private Dictionary<string, float> scaleResolutionDownOptions =
        new Dictionary<string, float>()
        {
                { "Not scaling", 1.0f },
                { "Down scale by 2.0", 2.0f },
                { "Down scale by 4.0", 4.0f },
                { "Down scale by 8.0", 8.0f },
                { "Down scale by 16.0", 16.0f }
        };

    //帧数选项
    private Dictionary<string, float> framerateOptions =
        new Dictionary<string, float>
        {
                { "90", 90f },
                { "60", 60f },
                { "30", 30f },
                { "20", 20f },
                { "10", 10f },
                { "5", 5f },
        };

    //分辨率选项
    private Dictionary<string, Vector2Int> resolutionOptions =
        new Dictionary<string, Vector2Int>
        {
                { "640x480", new Vector2Int(640, 480) },
                { "1280x720", new Vector2Int(1280, 720) },
                { "1600x1200", new Vector2Int(1600, 1200) },
                { "1920x1200", new Vector2Int(1920, 1200) },
                { "2560x1440", new Vector2Int(2560, 1440) },
        };
    #endregion

    



    #region 生命周期函数
    private void Awake()
    {
#if URS_USE_AR_FOUNDATION
            InputSystem.RegisterLayout<UnityEngine.XR.ARSubsystems.HandheldARInputDevice>(
                matches: new InputDeviceMatcher()
                    .WithInterface(XRUtilities.InterfaceMatchAnyVersion)
            );
#endif
        //----------------------------初始配置----------------------------
        //获取渲染流配置
        settings = new MyRenderStreamingSettings();
        //settings.SenderVideoCodec = VideoStreamSender.GetAvailableCodecs().ElementAt(index - 1);
        //若有配置,则进行设置,也可以自行设置
        if (settings != null)
        {
            //若不为Texture源
            if (videoStreamSender.source != VideoStreamSource.Texture)
            {
                //宽高
                videoStreamSender.width = (uint)settings.StreamSize.x;
                videoStreamSender.height = (uint)settings.StreamSize.y;
            }
            //编码
            //videoStreamSender.SetCodec(settings.SenderVideoCodec);
        }

        //----------------------------对Option按钮进行初始化与绑定----------------------------
        //视频源
        videoSourceTypeSelector.options = videoSourceTypeOptions
            .Select(pair => new Dropdown.OptionData { text = pair.Key })
            .ToList();
        videoSourceTypeSelector.onValueChanged.AddListener(ChangeVideoSourceType);

        //带宽
        bandwidthSelector.options = bandwidthOptions
            .Select(pair => new Dropdown.OptionData { text = pair.Key })
            .ToList();
        bandwidthSelector.options.Add(new Dropdown.OptionData { text = "Custom" });
        bandwidthSelector.onValueChanged.AddListener(ChangeBandwidth);

        //分辨率缩放
        scaleResolutionDownSelector.options = scaleResolutionDownOptions
            .Select(pair => new Dropdown.OptionData { text = pair.Key })
            .ToList();
        scaleResolutionDownSelector.options.Add(new Dropdown.OptionData { text = "Custom" });
        scaleResolutionDownSelector.onValueChanged.AddListener(ChangeScaleResolutionDown);

        //帧数
        framerateSelector.options = framerateOptions
            .Select(pair => new Dropdown.OptionData { text = pair.Key })
            .ToList();
        framerateSelector.options.Add(new Dropdown.OptionData { text = "Custom" });
        framerateSelector.onValueChanged.AddListener(ChangeFramerate);

        //分辨率
        resolutionSelector.options = resolutionOptions
            .Select(pair => new Dropdown.OptionData { text = pair.Key })
            .ToList();
        resolutionSelector.options.Add(new Dropdown.OptionData { text = "Custom" });
        resolutionSelector.onValueChanged.AddListener(ChangeResolution);


        ////测试
        //var dic = VideoStreamReceiver.GetAvailableCodecs();
        //foreach(var d in dic)
        //{
        //    Debug.Log("code:" + d);
        //}
    }


    #region Option按钮ValueChanged事件监听者
    private void ChangeVideoSourceType(int index)
    {
        var source = videoSourceTypeOptions.Values.ElementAt(index);
        videoStreamSender.source = source;
    }

    private void ChangeBandwidth(int index)
    {
        var bitrate = bandwidthOptions.Values.ElementAt(index);
        videoStreamSender.SetBitrate(bitrate, bitrate);
    }

    private void ChangeScaleResolutionDown(int index)
    {
        var scale = scaleResolutionDownOptions.Values.ElementAt(index);
        videoStreamSender.SetScaleResolutionDown(scale);//修改分辨率缩放
        CalculateInputRegion();//计算输入区域
    }

    private void ChangeFramerate(int index)
    {
        var framerate = framerateOptions.Values.ElementAt(index);
        videoStreamSender.SetFrameRate(framerate);
    }

    private void ChangeResolution(int index)
    {
        var resolution = resolutionOptions.Values.ElementAt(index);

        videoStreamSender.SetTextureSize(resolution);//修改分辨率
        CalculateInputRegion();//计算输入区域
    }
    #endregion

    private void Start()
    {
        //同步显示VideoSender参数
        SyncDisplayVideoSenderParameters();

        //若渲染流(信号管理者)自启动,则退出
        if (renderStreaming.runOnAwake)
            return;

        //settings及相关属性不为空,则进行设置(可能需要我们自行设置)
        if (settings != null)
            renderStreaming.useDefaultSettings = settings.UseDefaultSettings;
        if (settings?.SignalingSettings != null)
            renderStreaming.SetSignalingSettings(settings.SignalingSettings);
        //开启
        renderStreaming.Run();

        //绑定相关监听者
        inputReceiver.OnStartedChannel += OnStartedChannel; //通道开启
        inputReceiver.OnStoppedChannel += OnStoppedChannel; //通道关闭

        //获取输入接收者当前行为图,并对行为进行绑定
        //(注意,这里是对inputReceive上的行为控制器绑定,然后是只针对接受的,即只针对Web上的行为触发事件处理,不会触发PC本地的。)
        //(创建行为控制类,然后Enable的方式,会在PC本地以及Web触发,但测试发现鼠标位置事件不会在Web触发。)
        //(对于鼠标位置事件,注意要在远程Web触发,同时也不要在本地触发,否则位置会不准确。因为两个会同时检测,PC上即使没聚焦也会检测,至于是否可以设置为非聚焦不检测,没研究。
        //以后可以考虑设置为在连接服务器使用云渲染时关闭本地的鼠标位置检测,不使用云渲染时再打开,保证在PC上也可正常操作)
        var map = inputReceiver.currentActionMap;
        map["Point"].performed += Point;//只有是有web端的检测,目前没给PC加


        ////获取输入接收者当前行为图,并对行为进行绑定(使用的官方提供的InputAction)
        //var map = inputReceiver.currentActionMap;
        //map["Movement"].AddListener(cameraController.OnMovement);
        //map["Look"].AddListener(cameraController.OnLook);
        //map["ResetCamera"].AddListener(cameraController.OnResetCamera);
        //map["Rotate"].AddListener(cameraController.OnRotate);
        //map["Position"].AddListener(cameraController.OnPosition);
        //map["Point"].AddListener(uiController.OnPoint);
        //map["Press"].AddListener(uiController.OnPress);
        //map["PressAnyKey"].AddListener(uiController.OnPressAnyKey);


        //创建输入行为类对象实例(自己创建的InputAction)
        newControls = new NewControls();
        //激活行为图
        newControls.player.Enable();
        //绑定事件
        newControls.player.Jump.performed += Jump;
        newControls.player.Move.performed += Move;
        newControls.player.Click.performed += Click;
        newControls.player.Look.AddListener(Look);

        newControls.ui.Submit.performed += Submit;

        #region 交互式重新绑定按键(会等待监听按键输入,进行重新绑定,代码不会阻塞)
        ////注意这里的绑定并不会保存,需要自行进行保存,两种方法:
        ////1、自行根据按键输出的overridePath对相关行为绑定进行赋值(见视频42:33处)。
        ////2、1.1及以上版本,可通过Json进行保存与加载,(见视频42:59处)。
        ////   可用PlayerInput类,但这个要挂载,感觉不好。纯代码则使用InputAction实例:newControls.SaveBindingOverridesAsJson();newControls.LoadBindingOverridesFromJson();

        ////绑定前先失活
        //newControls.player.Disable();
        ////绑定(这里绑定的是玩家跳跃)
        //newControls.player.Jump.PerformInteractiveRebinding()
        //    .WithControlsExcluding("Mouse")//排除鼠标按键
        //    .OnComplete(callback =>//完成
        //    {
        //        Debug.Log(callback.action.bindings[0].overridePath);
        //        callback.Dispose();//释放
        //        newControls.player.Enable();//激活
        //    })
        //    .Start();//启用
        #endregion
    }


    private void FixedUpdate()
    {
        //不在监听函数读取了,直接外部一直读取
        Vector2 inputVector = newControls.player.Move.ReadValue<Vector2>();
        float speed = 10f;
        playerController.M_Rb.AddForce(new Vector3(inputVector.x, 0, inputVector.y) * speed, ForceMode.Force);
    }

    // Parameters can be changed from the Unity Editor inspector when in Play Mode,
    // So this method monitors the parameters every frame and updates scene UI.
    private void Update()
    {
#if UNITY_EDITOR
        SyncDisplayVideoSenderParameters();
#endif
        // Call SetInputChange if window size is changed.
        var width = Screen.width;
        var height = Screen.height;
        //不一样则更新,并计算输入范围
        if (lastWidth != width || lastHeight != height)
        {
            lastWidth = width;
            lastHeight = height;

            CalculateInputRegion();
        }
        
        #region 测试
        ////测试,按下P键计算输入区域
        //if (Keyboard.current.pKey.wasPressedThisFrame)
        //{
        //    CalculateInputRegion();
        //}

        ////简单监测输入(一般是测试使用,正规还是要使用Action)
        //if (Mouse.current.leftButton.wasPressedThisFrame)
        //{
        //    //Mouse Clicked!
        //}

        if (Keyboard.current.qKey.wasPressedThisFrame)
        {
            //Q Clicked!
            newControls.player.Disable();
            newControls.ui.Enable();
            Debug.Log("仅激活ui");
        }
        if (Keyboard.current.eKey.wasPressedThisFrame)
        {
            //E Clicked!
            newControls.player.Enable();
            newControls.ui.Disable();
            Debug.Log("仅激活player");
        }

        if (Keyboard.current.zKey.wasPressedThisFrame)
        {
            Debug.Log("向Web端发送消息。");
            F_SendMessageToWeb("Unity发送给Web的一条消息。");
        }

        //if (Keyboard.current.cKey.wasPressedThisFrame)
        //{
        //    //messageFromWeb.text = "GC.Collect操作";
        //    //GC.Collect();
        //    //messageFromWeb.text = "Stop!!!!!!!!!!!!!!!!!!!!!!";
        //    //renderStreaming.Stop();

        //    SceneManager.LoadScene(1);

        //}
        //if (Keyboard.current.vKey.wasPressedThisFrame)
        //{
        //    //messageFromWeb.text = "Resources.UnloadUnusedAssets操作";
        //    //Resources.UnloadUnusedAssets();
        //    //messageFromWeb.text = "Run!!!!!!!!!!!!!!!!!!!!!!";
        //    //renderStreaming.Run();

        //    //GC.Collect();
        //    //Debug.Log("Transceivers长度:" + videoStreamSender.Transceivers.Count);
        //    //foreach(var v in videoStreamSender.Transceivers)
        //    //{
        //    //    v.Value.Sender.Dispose();
        //    //    v.Value.Receiver.Dispose();
        //    //    v.Value.Stop();
        //    //    v.Value.Dispose();
        //    //}
        //    //Application.Unload();



        //    //// 删除临时目录下的文件(需谨慎,确保不删除正在使用的文件)
        //    //if (Directory.Exists(Application.temporaryCachePath))
        //    //{
        //    //    Directory.Delete(Application.temporaryCachePath, recursive: true);
        //    //    //Debug.Log("地址:" + Application.temporaryCachePath);
        //    //}

        //    Resources.UnloadUnusedAssets();
        //    // 强制垃圾回收
        //    System.GC.Collect();
        //    // 等待所有终结器执行完毕
        //    System.GC.WaitForPendingFinalizers();
        //    // 再次回收(确保回收终结器释放的对象)
        //    UnityEngine.Caching.ClearCache();
        //}

        #endregion
    }

    private void LateUpdate()
    {
        ////按下鼠标滚轮,移动鼠标来移动摄像机
        //if (!Input.GetMouseButtonDown(2) && Input.GetMouseButton(2))//后者包含按下,但我们不要按下那次
        //{
        //    //获取鼠标移动
        //    float horizontal = -Input.GetAxis("Mouse X") * m_speed_MouseMoveX;
        //    float vertical = -Input.GetAxis("Mouse Y") * m_speed_MouseMoveY;
        //    //相机移动
        //    this.transform.Translate(Vector3.right * horizontal);
        //    this.transform.Translate(Vector3.up * vertical);
        //}


        //旋转相机
        //计算新角度
        float angleX = theCamera.transform.localEulerAngles.x - m_mouseDelta.y;
        float angleY = theCamera.transform.localEulerAngles.y + m_mouseDelta.x;
        //相机旋转
        theCamera.transform.localEulerAngles = new Vector3(angleX, angleY, 0.0f);
    }

    private void OnDestroy()
    {
        //失活行为图
        newControls.player.Disable();
        
    }

    #endregion


    #region 一般方法
    /// <summary>
    /// InputReceiver通道开启监听。
    /// 此时,Channel存在。
    /// </summary>
    /// <param name="connectionId"></param>
    private void OnStartedChannel(string connectionId)
    {
        Debug.LogWarning("绑定前:" + inputReceiver.Channel.OnMessage.GetInvocationList().Length);
        Debug.Log("InputReceiver通道开启。");
        //绑定监听
        inputReceiver.Channel.OnMessage += F_OnMessage;//接收消息事件
        //计算输入区域
        CalculateInputRegion();

        Debug.LogWarning("绑定后:" + inputReceiver.Channel.OnMessage.GetInvocationList().Length);
    }

    /// <summary>
    /// InputReceiver通道关闭监听。
    /// 此时,Channel不存在。故不需要对Channel解绑监听。
    /// </summary>
    /// <param name="connectionId"></param>
    private void OnStoppedChannel(string connectionId)
    {
        Debug.Log("InputReceiver通道关闭。");
    }

    /// <summary>
    /// 计算输入区域。
    /// </summary>
    private void CalculateInputRegion()
    {
        //若输入接收者未连接,则退出
        if (!inputReceiver.IsConnected)
            return;
        //计算视频流发送者最终的宽高
        var width = (int)(videoStreamSender.width / videoStreamSender.scaleResolutionDown);
        var height = (int)(videoStreamSender.height / videoStreamSender.scaleResolutionDown);
        //输入接收者计算输入区域
        inputReceiver.CalculateInputRegion(new Vector2Int(width, height));
        //设置“允许输入位置校正”
        inputReceiver.SetEnableInputPositionCorrection(true);
    }

    /// <summary>
    /// 同步显示VideoSender参数。
    /// 根据VideoSender参数,调整Option按钮的当前选项,不会触发事件。
    /// </summary>
    private void SyncDisplayVideoSenderParameters()
    {
        if (videoStreamSender == null)
        {
            return;
        }


        //获取当前带宽下标,之类min maxBitrate是一样的,通过Option按钮设置的
        var bandwidthIndex = Array.IndexOf(bandwidthOptions.Values.ToArray(), videoStreamSender.maxBitrate);
        if (bandwidthIndex < 0)
        {
            //Debug.Log("Custom maxBitrate:" + videoStreamSender.maxBitrate);
            bandwidthSelector.SetValueWithoutNotify(bandwidthSelector.options.Count - 1);//选项的最后一个
        }
        else
        {
            bandwidthSelector.SetValueWithoutNotify(bandwidthIndex);
        }

        //分辨率缩放
        var scaleDownIndex = Array.IndexOf(scaleResolutionDownOptions.Values.ToArray(), videoStreamSender.scaleResolutionDown);
        if (scaleDownIndex < 0)
        {
            //Debug.Log("Custom scaleResolutionDown:" + videoStreamSender.scaleResolutionDown);
            scaleResolutionDownSelector.SetValueWithoutNotify(bandwidthSelector.options.Count - 1);
        }
        else
        {
            scaleResolutionDownSelector.SetValueWithoutNotify(scaleDownIndex);
        }

        //帧数
        var framerateIndex = Array.IndexOf(framerateOptions.Values.ToArray(), videoStreamSender.frameRate);
        if (framerateIndex < 0)
        {
            //Debug.Log("Custom frameRate:" + videoStreamSender.frameRate);
            framerateSelector.SetValueWithoutNotify(framerateSelector.options.Count - 1);
        }
        else
        {
            framerateSelector.SetValueWithoutNotify(framerateIndex);
        }

        //分辨率
        var target = new Vector2Int((int)videoStreamSender.width, (int)videoStreamSender.height);
        var resolutionIndex = Array.IndexOf(resolutionOptions.Values.ToArray(), target);
        if (resolutionIndex < 0)
        {
            //Debug.Log("Custom Size:" + target);
            resolutionSelector.SetValueWithoutNotify(resolutionSelector.options.Count - 1);
        }
        else
        {
            resolutionSelector.SetValueWithoutNotify(resolutionIndex);
        }
    }

    /// <summary>
    /// 射线检测代码。(参考)
    /// </summary>
    private void CameraRayCast()
    {
        #region 单目标检测
        ////场景射线结构体,存储射线起始点、方向      
        ////Ray ray = new Ray(this.transform.position, this.transform.forward);//自定义射线
        //Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//相机射线

        ////创建一个RaycastHit结构体,用于存储碰撞信息
        //RaycastHit hitInfo;

        ////射线检测
        //if (Physics.Raycast(ray, out hitInfo))
        //{
        //    //hitInfo中存储的collider即为检测到的物体碰撞体
        //    Debug.Log(hitInfo.collider.gameObject.name);
        //}
        #endregion

        #region 多目标检测
        ////场景射线结构体,存储射线起始点、方向      
        //Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//相机射线

        ////创建一个RaycastHit结构体,用于存储碰撞信息
        //RaycastHit[] hitInfo;

        ////射线检测
        //hitInfo = Physics.RaycastAll(ray);
        //for (int i = 0; i < hitInfo.Length; i++)
        //{
        //    //hitInfo中存储的collider即为检测到的物体碰撞体
        //    Debug.Log(hitInfo[i].collider.gameObject.name);
        //}
        #endregion
    }

    /// <summary>
    /// 射线检测。
    /// </summary>
    /// <param name="mousePosition"></param>
    /// <returns></returns>
    private GameObject F_CamerRayCast(Vector3 mousePosition)
    {
        Ray ray = Camera.main.ScreenPointToRay(mousePosition);//相机射线

        //创建一个RaycastHit结构体,用于存储碰撞信息
        RaycastHit[] hitInfo;

        //射线检测
        hitInfo = Physics.RaycastAll(ray);
        for (int i = 0; i < hitInfo.Length; i++)
        {
            //hitInfo中存储的collider即为检测到的物体碰撞体
            Debug.Log(hitInfo[i].collider.gameObject.name);
        }

        if (hitInfo.Length > 0)
        {
            return hitInfo[0].collider.gameObject;
        }
        else
        {
            return null;
        }
    }

    /// <summary>
    /// 发送消息到Web。
    /// </summary>
    /// <param name="message"></param>
    private void F_SendMessageToWeb(string message)
    {
        inputReceiver.Channel.Send(message);
    }

    /// <summary>
    /// Web端消息接收。
    /// 操作数据,web端发送的数据都接收。
    /// 与Receiver.cs中的OnMessage接收到的数据是一致的。
    /// 与Receiver.cs中的OnMessage应该是平行关系,非深度关系。顺序因该是Receiver的在前,这里的在后。
    /// </summary>
    /// <param name="bytes"></param>
    private void F_OnMessage(byte[] bytes)
    {
        string msg = System.Text.Encoding.Default.GetString(bytes);
        messageFromWeb.text = bytes.Length + "-" + msg;
    }
    #endregion


    #region 行为监听者
    private void Jump(InputAction.CallbackContext ctx)
    {
        Debug.Log("状态:" + ctx.phase);

        if (ctx.performed)
        {
            playerController.M_Rb.AddForce(Vector3.up * 5f, ForceMode.Impulse);
        }

        //if (ctx.phase == InputActionPhase.Started)
        //{
        //    Debug.Log("开始-跳跃。");
        //}
        //else if(ctx.phase == InputActionPhase.Performed)
        //{
        //    Debug.Log("执行-跳跃。");
        //}
        //else if (ctx.phase == InputActionPhase.Canceled)
        //{
        //    Debug.Log("取消-跳跃。");
        //}
    }

    private void Move(InputAction.CallbackContext ctx)
    {
        //Debug.Log(ctx);
        //Vector2 inputVector = ctx.ReadValue<Vector2>();
        //float speed = 100f;
        //m_rb.AddForce(new Vector3(inputVector.x,0,inputVector.y)*speed,ForceMode.Force);
    }

    private void Submit(InputAction.CallbackContext ctx)
    {
        Debug.Log(ctx);
        playerController.transform.position = new Vector3(0f, 1f, 0f);
        playerController.transform.rotation = Quaternion.identity;
        playerController.M_Rb.isKinematic = true;
        playerController.M_Rb.isKinematic = false;
    }

    private void Point(InputAction.CallbackContext ctx)
    {
        mousePosition.text = "鼠标位置:" + ctx.ReadValue<Vector2>().ToString();
        m_mousePosition = ctx.ReadValue<Vector2>();
        //Debug.Log("Mouse Position:" + ctx.ReadValue<Vector2>());
    }

    private void Click(InputAction.CallbackContext ctx)
    {
        //Debug.Log("鼠标位置(旧):" + Input.mousePosition);//不准确,不能用
        Debug.Log("鼠标点击!当前鼠标位置(新):" + m_mousePosition);

        GameObject obj = F_CamerRayCast(m_mousePosition);//射线检测鼠标位置,以这里的鼠标位置为准,是基于PC端屏幕的尺寸给的位置。
        if (obj != null)
        {
            clickGameObjectName.text = "射线检测对象:" + obj.name;
            //调用检测到的对象身上的某一方法(这里是规定的射线检测处理方法),不要求接收者参数意味着对象若没有此方法也不会报错。
            //之前用于检测对象碰撞体被鼠标点击的方法“OnMouseUpAsButton”之后就不考虑使用了,包括PC端开发也不考虑了,都用射线。
            //这样万一要转为云渲染也方便。
            obj.SendMessage("F_RayCastHandler", SendMessageOptions.DontRequireReceiver);
        }
        else
        {
            clickGameObjectName.text = "Null";
        }


        //这里m_rectTransform是指Canvas的。这里是将屏幕鼠标位置映射为在Canvas中的位置。
        //通常我们是不用映射的,屏幕怎么变(PC端运行程序窗口尺寸改变),我们就用屏幕尺寸下的鼠标坐标就行,如射线检测,就直接使用映射前的坐标值(参考上面射线检测)。
        //但,Canvas设置为Scale With Screen Size后,其宽高与屏幕的尺寸可能不同。
        //所以如果涉及到UI根据鼠标位置操作的行为,那么我们就需要先把鼠标坐标值映射为Canvas下的值,然后再拿来使用。
        //转换代码:
        //if (m_rectTransform == null)
        //    return;
        //var position = context.ReadValue<Vector2>();
        //Debug.Log("Old Point:" + position);
        //var screenSize = new Vector2Int(Screen.width, Screen.height);
        //position = position / screenSize * new Vector2(m_rectTransform.rect.width, m_rectTransform.rect.height);
        //pointer.rectTransform.anchoredPosition = position;
        //Debug.Log("New Point:" + position);
    }

    private void Look(InputAction.CallbackContext ctx)
    {
        Vector2 vc =  ctx.ReadValue<Vector2>();
        mouseDelta.text = "Mouse Delta:" + vc;
        m_mouseDelta = vc;
    }

    #endregion

}

PlayerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class PlayerController : MonoBehaviour
{
    private Rigidbody m_rb;

    public Rigidbody M_Rb { get { return m_rb; } }
    

    private void Awake()
    {
        m_rb = this.GetComponent<Rigidbody>();
    }


    //web端无法使用
    //private void OnMouseUpAsButton()
    //{
    //    Debug.Log("点击了Cube。");
    //}


    private void F_RayCastHandler()
    {
        Debug.Log(this.gameObject.name + "的射线检查回应!");
    }
}

MyRenderStreamingSettings.cs

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.RenderStreaming;
using UnityEngine;

/// <summary>
/// 渲染流配置类。
/// </summary>
public class MyRenderStreamingSettings
{
    /// <summary>
    /// 默认流宽度。
    /// </summary>
    public const int DefaultStreamWidth = 1920;
    /// <summary>
    /// 默认流高度。
    /// </summary>
    public const int DefaultStreamHeight = 1080;

    private bool useDefaultSettings = false;
    private SignalingType signalingType = SignalingType.WebSocket;
    private string signalingAddress = "127.0.0.1:8088";
    private int signalingInterval = 5000;
    private bool signalingSecured = false;
    private Vector2Int streamSize = new Vector2Int(DefaultStreamWidth, DefaultStreamHeight);
    private VideoCodecInfo receiverVideoCodec = null;
    private VideoCodecInfo senderVideoCodec = null;

    #region 属性
    /// <summary>
    /// 是否使用默认配置。
    /// </summary>
    public bool UseDefaultSettings
    {
        get { return useDefaultSettings; }
        set { useDefaultSettings = value; }
    }

    /// <summary>
    /// 信号类型。
    /// </summary>
    public SignalingType SignalingType
    {
        get { return signalingType; }
        set { signalingType = value; }
    }

    /// <summary>
    /// 信号地址。
    /// </summary>
    public string SignalingAddress
    {
        get { return signalingAddress; }
        set { signalingAddress = value; }
    }

    /// <summary>
    /// 信号安全。
    /// </summary>
    public bool SignalingSecured
    {
        get { return signalingSecured; }
        set { signalingSecured = value; }
    }

    /// <summary>
    /// 信号时间间隔。
    /// </summary>
    public int SignalingInterval
    {
        get { return signalingInterval; }
        set { signalingInterval = value; }
    }

    /// <summary>
    /// 信号配置。
    /// </summary>
    public SignalingSettings SignalingSettings
    {
        get
        {
            switch (signalingType)
            {
                case SignalingType.WebSocket:
                    {
                        var schema = signalingSecured ? "wss" : "ws";
                        return new WebSocketSignalingSettings
                        (
                            url: $"{schema}://{signalingAddress}",
                            iceServers: new[]{
                                new IceServer (urls: new[] {"stun:stun.l.google.com:19302"})
                            }
                        );
                    }
                case SignalingType.Http:
                    {
                        var schema = signalingSecured ? "https" : "http";
                        return new HttpSignalingSettings
                        (
                            url: $"{schema}://{signalingAddress}",
                            interval: signalingInterval,
                            iceServers: new[]{
                                new IceServer (urls: new[] {"stun:stun.l.google.com:19302"})
                            }
                        );
                    }
            }
            throw new InvalidOperationException();
        }
    }

    /// <summary>
    /// 流尺寸。
    /// </summary>
    public Vector2Int StreamSize
    {
        get { return streamSize; }
        set { streamSize = value; }
    }

    /// <summary>
    /// 接收者视频编码。
    /// </summary>
    public VideoCodecInfo ReceiverVideoCodec
    {
        get { return receiverVideoCodec; }
        set { receiverVideoCodec = value; }
    }

    /// <summary>
    /// 发送者视频编码。
    /// </summary>
    public VideoCodecInfo SenderVideoCodec
    {
        get { return senderVideoCodec; }
        set { senderVideoCodec = value; }
    }
    #endregion
}

/// <summary>
/// 信号类型枚举。
/// </summary>
public enum SignalingType
{
    WebSocket,
    Http,
}

3 问题

        一些问题已经在代码中的注释进行了说明,不过这里还是要单独再说明下。

3.1 鼠标位置检测

        使用InputSystem,通常我们创建InputAction实例在绑定行为监听者,这种方式一般可以同时监听PC、Web上的行为。代码示例:

      //创建输入行为类对象实例
      newControls = new NewControls();
      //激活行为图
      newControls.player.Enable();
      //绑定事件
      newControls.player.Jump.performed += Jump;
      newControls.player.Move.performed += Move;
      newControls.player.Click.performed += Click;
      newControls.player.Look.AddListener(Look);
      newControls.ui.Submit.performed += Submit;

        但测试发现对于鼠标位置行为的检测,这种方式无法检测到Web上的行为(也许是Web和PC同时检测,但PC检测的监听函数处理靠后覆盖了Web的?没测试,只是猜测一种可能)。所以只能在InputReceiver组件上挂载我们的InputAction资源,这样才能检测到Web端鼠标位置行为,然后在代码中通过InputReceiver对象获取当前的ActionMap,进而监听鼠标位置。示例代码:

var map = inputReceiver.currentActionMap;
map["Point"].performed += Point;

        不过需要注意,若此时原来的检测方式仍在使用,则会同时检测PC和Web上的鼠标位置行为。按理说PC应该在不聚焦程序时不检测鼠标位置,但发现即使不聚焦程序窗口,仍在检测,具体是否有设置方法来禁用非聚焦检测,我并未研究,可自行查找。转回话题,若两者同时检测是没什么事情的,因为分属两个InputAction对象,为了不相互影响,需要保证之前的检测方式中,鼠标位置行为不要绑定监听者,这样我们只对Web端检测进行处理,就可保证Web端的正确性。不过这里我建议可以在PC端Unity与服务器连接时注销PC鼠标位置检测的相关监听,并注册Web端鼠标位置相关监听,在未连接服务器时则反向处理,确保使用云渲染时可正常使用,在未使用云渲染时PC端也可以正常使用。

3.2 内存增加

        直接复制代码中的注释:

(UnityRenderStreaming包版本3.1.0-exp.9)
在编辑器状态下:

  1. 内存有时会出现逐渐快速增加的问题,最高达到过10G左右,也许会更高,但当鼠标点击编辑器,即聚焦编辑器时,内存又会快速下降恢复至正常。
  2. 然后是发送消息问题,从Web向Unity发送消息,会在UnityStreaming包的Receiver.cs的OnMessage方法里接收到消息。这里本来是对Input消息接收处理的。但若我们在web端通过通道发送消息,这里也能接收到。那么问题来了,若是我们自己的消息过来在OnMessage中被发送向下处理了(期间无报错),则Unity的内存占用会突然增加。需要注意,若发送的message是重新创建的默认对象(InputRemoting.Message msg = default;),向下处理后,不会有这种问题。这里自己的Demo是突然增加了几百到1G左右,且发送多次不一定哪次就会增加,增加到一定数值再多次测试都没有增加了(也可能是测试不够多?)。并且这和前面自动增加不同,即使聚焦到编辑器,自动增加的内存会下降,但发送增加的内存却会保持住。

在打包状态下:

  1. 运行状态下内存会缓慢增加,逐渐累计,增加内存属于Unity划分到unknown类别的内存。经多天研究可以确定是包的问题,且尝试多种方法均无法解决,只有Unity程序关闭内存才能被释放掉。(版本3.1.0-exp.7也有此问题,不过3.1.0-exp.6无此问题)
  2. 发送消息内存增加问题同编辑器。

结论:

  1. 编辑器状态下若内存增加,鼠标点击编辑器把焦点聚焦回来。
  2. 打包运行Unity程序,内存将累计,勉强能使用。或更换exp.6版本。
  3. 确保从Web发送给Unity的自定义消息在UnityStreaming包的Receiver.cs的OnMessage方法里不被onMessage执行。(建议在OnMessage直接过滤掉自己的消息,自己的消息可以在InputReceiver.Channel.OnMessage这里监听处理)

PS:

  1. Mono、IL2CPP下,情况一致。


(UnityRenderStreaming包版本3.1.0-exp.6)
编辑器状态下:

  1. 内存不会自动增加。
  2. 发送消息内存增加问题同版本9.
  3. 刷新Web页面,内存有时候会增加一点,小到不到1MB,大到几MB,但此内存有时候会出现释放的情况,目前出现过释放部分的情况。(这虽然是个问题,但基本可以忽略)

打包状态下:

  1. 同编辑器。

结论:

  1. 与9版本相比,6版本没有了内存自动增加的问题,但有刷新Web页面,内存可能增加的问题(9版本也许也有,但在内存自动增加问题前可以忽略不计)。不过该问题在实际应用中可以忽略不计。

PS:

  1. 编辑器下测试了Mono、IL2CPP。
  2. 打包下只测了IL2CPP。(Mono大概率一样,实在是懒得测了)


总结:

  1. 用UnityRenderStreaming包版本3.1.0-exp.6。
  2. 在Receiver.cs的OnMessage里做好数据过滤。

情况就是这么个情况,个人觉得刷新导致的内存增加没什么影响,而且也出现过释放的情况,个人是将其归为安全的。

3.3 自定义Web端数据接收(Receiver.cs)

        这里主要针对自定义Web端数据接收中存在的问题该如何解决进行说明。首先问题出现在Receiver.cs脚本的OnMessage函数中,该函数会接收Web端通过通道发送过来的数据,包括行为数据、我们自定义的数据。对于行为数据的接收处理上是没问题的,但对于自定义的数据,处理上会出现两种问题:报错(一般是解析错误)、内存增加(前面已提过,这里不再多说)。

        解决方法:在OnMessage中,过滤我们的自定义数据不被处理,同时可以给OnMessage中原本的处理方法加上trycatch,防止漏网之鱼导致错误异常。

        代码中的注释有详细的说明,示例代码:

        private void OnMessage(byte[] bytes)
        {
            //··········· 过滤数据


            //这里会接收各种数据,包括网页端的Input数据,也包括我们自己发送的数据。
            //我们可以在这里对这些数据进行区分,自己的数据自己处理,Input数据交给onMessage处理。
            //因为:
            //1、Input数据需要向下执行(被Deserialize、onMessage处理),向下执行后,web端的操作才能产生影响。
            //2、InputReceiver.Channel.OnMessage也可以接收Input与自己的数据,我们可以监听处理,且这种使用方式更规范。
            //3、本函数若出异常且没有Catch的话,OnMessage后续都不会再监听了,且InputReceiver.Channel.OnMessage也将无法接收到数据。即影响1、2.
            //4、若自己的数据被拿去向下执行,则情况如下:
            //  (1)数据不满足解析条件,异常。
            //  (2)数据满足解析条件,被onMessage处理,异常。
            //  (3)数据满足解析条件,被onMessage处理,无异常,但影响未知。(已知影响:可能会导致内存暴增)
            //所以:
            //1、本函数中,我们需要过滤自己的数据,防止自己的数据向下执行。
            //2、对“Deserialize、onMessage”代码try catch,防止自己消息的漏网之鱼在被处理时造成的异常影响。
            //PS:
            //在try catch后,自己数据向下执行时,可能出现的情况详情:
            //1、数据不满足解析条件,异常Catch。异常:超出流读取边界。
            //2、数据不满足解析条件,异常Catch。异常:解析长度异常导致读取异常。
            //3、数据不满足解析条件,异常Catch。异常:其他异常。
            //4、数据满足解析条件,被onMessage处理,异常Catch。异常:其他异常。
            //5、数据满足解析条件,被onMessage处理,无异常,但影响未知。(已知影响:可能会导致内存暴增)
            try
            {
                MessageSerializer.Deserialize(bytes, out var message);
                onMessage?.Invoke(message);
            }
            catch (EndOfStreamException ex)//捕获长度不够超出读取范围的异常
            {
                UnityEngine.Debug.LogWarning(
                    $"Catch!Receiver的OnMessage接收消息并处理时异常!" +
                    $"数据流读取边界异常。" +
                    $"不可读取或长度不够(position>=length),故无法读取。" +
                    $"信息:{ex}");
            }
            catch (ArgumentOutOfRangeException ex)
            {
                UnityEngine.Debug.LogWarning($"Catch!Receiver的OnMessage接收消息并处理时异常!读取长度异常。信息:{ex}");
            }
            catch (Exception ex)
            {
                UnityEngine.Debug.LogError($"Catch!Receiver的OnMessage接收消息并处理时异常!其他异常。信息:{ex}");
            }
        }

4 后记

        exp7、exp9的包尝试了好多方法都无法解决内存问题,无奈更换版本,好在exp6和它们的差距不大。这个包目前也不再更新了,将就用吧。

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐