文章目录
嗨,大家好,我是新发。
有同学私信我,问我能不能写一篇Unity手游第一人称视角控制的教程,

那么,今天就来做个Demo吧~
注:
Demo工程源码见文章末尾
最终效果如下:



第一人称视角的游戏大家应该不陌生,比如《无主之地》,

不过它是PC平台的,使用WASD控制移动,使用鼠标来控制镜头角度,单击鼠标左键开枪。注:你也可以接手柄来操作~
那么,如果我们想做移动端(手机端)的第一人称视角,如何做角色控制呢?
手机端比较常见的就是摇杆控制了,我之前在几篇博客中都有做过摇杆控制,
《【游戏开发创新】用Unity等比例制作广州地铁,广州加油,早日战胜疫情(Unity | 地铁地图 | 第三人称视角)》

《【游戏开发创新】上班通勤时间太长,做一个任意门,告别地铁与塞车(Unity | 建模 | ShaderGraph | 摇杆 | 角色控制)》

《【游戏开发实战】新发教你做游戏(六):教你2个步骤实现摇杆功能》

我上面做的都是第三人称视角的摇杆控制,我们改成第一人称视角即可,也就是第一人称视角+摇杆控制,像这样子,(图片说明:下图是我在《无主之地》游戏截图中P了摇杆的UI)

我没有《无主之地》的资源,没关系,我们去Unity的AssetStore上找一下FPS射击游戏的资源,
注:
Unity AssetStore地址:https://assetstore.unity.com/
关于资源的搜索,我之前写过一篇文章:《Unity游戏开发——新发教你做游戏(二):60个Unity免费资源获取网站》
搜索关键字FPS Pack,马上就搜到了一个免费的资源Low Poly FPS Pack,

我们点击添加至我的资源(注意需要先登录你的Unity账号),

然后回到Unity编辑器中,点击菜单Windows / Package Manager,打开PackageManager窗口,就可以看到我们刚刚在AssetStore中添加的资源啦,我们把资源包下载并导入我们的工程即可。

注:你得先创建一个空工程,然后再导入资源包。我之前写过《学Unity的猫》系列教程,其中第三章有讲创建工程的步骤,
《【学Unity的猫】——第三章:第一个Unity工程,你好喵星人》)
Low Poly FPS Pack资源包中已经帮我们做好了一个简单的第一人称FPS游戏Demo,我们打开Assault_Rifle_01_Demo场景,如下

运行,测试效果如下

如你所见,经典的PC平台FPS射击游戏玩法,使用WASD控制移动,使用鼠标来控制镜头角度,单击鼠标左键开枪。
接下来,我们要给它做下手术,改成 摇杆 和 按钮 控制。
摇杆图片简单处理,用一个圆就可以了,然后我们还需要一些按钮图标,比如开枪、丢手雷、跳跃、装子弹等,这里推荐我平时经常用的一个查找图标资源的网站,阿里图标库:https://www.iconfont.cn/
比如我搜关键字:枪,就可以看到枪的图标啦~

可以直接免费下载,而且还可以事先修改图片颜色,建议改成白色,这样方便在Unity中设置其他颜色,

根据你自身的需要下载一些图标资源,我下载的图标如下,

注意,因为我们要在UGUI中显示这些图标,需要将它们的Texture Type设置为Sprite (2D and UI),如下

建议UI的显示使用一个单独的摄像机来渲染,我们在场景中创建一个Camera,重命名为UICamera,
注:创建摄像机的操作步骤:在
Hierarchy视图中鼠标右键,然后点击菜单Camera即可。

设置摄像机的Clear Flags为Depth only,设置Culling Mask只渲染UI层,设置Projection为Orthographic(正交模式),设置Depth为1(确保UI摄像机的比3D摄像机后渲染),

接下来,我们在Hierarchy视图中鼠标右键,点击菜单UI / Canvas,创建一个Canvas,
设置一下参数,如下,目的是让UICamera来渲染Canvas的内容,并设置分辨率适配规则,

我们在Canvas子节点下创建一个Panel,重命名为GamePanel,并把Image组件禁用,

下面我们再在GamePanel下去创建UI对象。
我们先做移动控制的摇杆,在GamePanel子节点下创建一个Image,重命名为moveJointedArm,

设置左下角对齐,并调整坐标和尺寸,

像这样,它就是我们摇杆的检测区域,

我们把它的Image组件的颜色的Alpha通道设置为0,这样我们就看不见它了,

我们在moveJointedArm子节点下再创建两个Image,分别命名为bg和center,

分别设置一下尺寸,图片,颜色,如下

效果

同理,做一下右摇杆,

效果

除了移动和旋转,我们还有开枪、丢手雷、跳跃、装子弹的操作,配套需要制作对应的按钮。安排上,

效果

到这里,我们的UI界面就基本做好啦,下面就是写代码的环节了~
摇杆的逻辑实现,我之前写过一篇文章讲过原理:《Unity使用ScrollRect制作摇杆(UGUI)》,这里我就不过对赘述,直接说下操作流程。
创建一个C#脚本,重命名为JointedArm.cs,代码如下:
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System;
// 摇杆逻辑
public class JointedArm : ScrollRect, IPointerDownHandler
{
public Action<Vector2> onDragCb;
public Action onStopCb;
protected float mRadius = 0f;
private Transform trans;
private RectTransform bgTrans;
private Camera uiCam;
private Vector3 originalPos;
protected override void Awake()
{
base.Awake();
trans = transform;
bgTrans = trans.Find("bg") as RectTransform;
uiCam = GameObject.Find("UICamera").GetComponent<Camera>();
originalPos = trans.localPosition;
}
void Update()
{
if (Input.GetMouseButtonUp(0))
{
//松手时,摇杆复位
trans.localPosition = originalPos;
this.content.localPosition = Vector3.zero;
}
}
protected override void Start()
{
base.Start();
//计算摇杆块的半径
mRadius = bgTrans.sizeDelta.x * 0.5f;
}
public override void OnDrag(PointerEventData eventData)
{
base.OnDrag(eventData);
var contentPostion = this.content.anchoredPosition;
if (contentPostion.magnitude > mRadius)
{
contentPostion = contentPostion.normalized * mRadius;
SetContentAnchoredPosition(contentPostion);
}
//Debug.Log("摇杆滑动,方向:" + contentPostion);
if(null != onDragCb)
onDragCb(contentPostion);
}
public override void OnEndDrag(PointerEventData eventData)
{
base.OnEndDrag(eventData);
//Debug.Log("摇杆拖动结束");
if (null != onStopCb)
onStopCb();
}
public void OnPointerDown(PointerEventData eventData)
{
//点击到摇杆的区域,摇杆移动到点击的位置
trans.position = uiCam.ScreenToWorldPoint(eventData.position);
trans.localPosition = new Vector3(trans.localPosition.x, trans.localPosition.y, 0);
}
}
给moveJointedArm节点挂JointedArm脚本,并设置Content为center节点,如下,

同理设置右摇杆rotateJointedArm。到此,我们的摇杆就有交互效果了,如下

我们创建一个GamePanel.cs脚本,声明UI对象,如下
using UnityEngine;
public class GamePanel : MonoBehaviour
{
/// <summary>
/// 移动摇杆
/// </summary>
public JointedArm moveJointedArm;
/// <summary>
/// 旋转摇杆
/// </summary>
public JointedArm rotateJointedArm;
/// <summary>
/// 开枪按钮
/// </summary>
public GameObject fireBtn;
/// <summary>
/// 丢手雷按钮
/// </summary>
public GameObject bombBtn;
/// <summary>
/// 跳跃按钮
/// </summary>
public GameObject jumpBtn;
/// <summary>
/// 装子弹按钮
/// </summary>
public GameObject bulletBtn;
void Start()
{
// TODO 关联UI交互事件
}
}
把它挂到GamePanel节点上,并设置变量对象,如下

在Start函数中添加摇杆的委托,如下
// GamePanel.cs
void Start()
{
// 移动控制摇杆
moveJointedArm.onDragCb = (direction) =>
{
// TODO 抛出事件
};
moveJointedArm.onStopCb = () =>
{
// TODO 抛出事件
};
// 旋转控制摇杆
rotateJointedArm.onDragCb = (direction) =>
{
// TODO 抛出事件
};
rotateJointedArm.onStopCb = () =>
{
// TODO 抛出事件
};
// ...
}
我们要抛出一些事件,这里要封装一个事件管理器。
我在之前的多篇文章中都有到和用到事件管理器,欢迎阅读我之前写的这些文章,里面都有用到事件管理器,
《【游戏开发框架】自制Unity通用游戏框架UnityXFramework,详细教程(Unity3D技能树 | tolua | 框架 | 热更新)》
《【游戏开发创新】用Unity等比例制作广州地铁,广州加油,早日战胜疫情(Unity | 地铁地图 | 第三人称视角)》
《【游戏开发实战】使用Unity 2019制作仿微信小游戏飞机大战(二):搭建基础游戏框架》
《【游戏开发实战】使用Unity制作水果消消乐游戏教程(三):水果拖动与交换逻辑》
《【游戏开发实战】使用Unity制作像天天酷跑一样的跑酷游戏——第七篇:游戏界面的基础UI》
《【学Unity的猫】第十二章:使用Unity制作背包,皮皮的梦想背包》
EventDispatcher脚本代码:
using UnityEngine;
using System.Collections.Generic;
public delegate void MyEventHandler(params object[] objs);
/// <summary>
/// 游戏事件管理器
/// </summary>
public class EventDispatcher
{
/// <summary>
/// 注册事件
/// </summary>
/// <param name="evt">事件名</param>
/// <param name="handler">响应函数</param>
public void Regist(string evt, MyEventHandler handler)
{
if (handler == null)
return;
if (listeners.ContainsKey(evt))
{
//这里涉及到Dispath过程中反注册问题,必须使用listeners[type]+=..
listeners[evt] += handler;
}
else
{
listeners.Add(evt, handler);
}
}
/// <summary>
/// 注销事件
/// </summary>
/// <param name="evt">事件名</param>
/// <param name="handler">响应函数</param>
public void UnRegist(string evt, MyEventHandler handler)
{
if (handler == null)
return;
if (listeners.ContainsKey(evt))
{
//这里涉及到Dispath过程中反注册问题,必须使用listeners[type]-=..
listeners[evt] -= handler;
if (listeners[evt] == null)
{
//已经没有监听者了,移除.
listeners.Remove(evt);
}
}
}
/// <summary>
/// 抛出事件
/// </summary>
/// <param name="evt">事件名</param>
/// <param name="objs">参数</param>
public void DispatchEvent(string evt, params object[] objs)
{
try
{
if (listeners.ContainsKey(evt))
{
MyEventHandler handler = listeners[evt];
if (handler != null)
handler(objs);
}
}
catch (System.Exception ex)
{
Debug.LogErrorFormat(szErrorMessage, evt, ex.Message, ex.StackTrace);
}
}
public void ClearEvents(string key)
{
if (listeners.ContainsKey(key))
{
listeners.Remove(key);
}
}
private Dictionary<string, MyEventHandler> listeners = new Dictionary<string, MyEventHandler>();
private readonly string szErrorMessage = "DispatchEvent Error, Event:{0}, Error:{1}, {2}";
private static EventDispatcher s_instance;
public static EventDispatcher instance
{
get
{
if (null == s_instance)
s_instance = new EventDispatcher();
return s_instance;
}
}
}
我们再创建一个EventNameDef.cs脚本,用于定义事件名,如下
/// <summary>
/// 事件名定义
/// </summary>
public class EventNameDef
{
/// <summary>
/// 移动
/// </summary>
public const string MOVE = "MOVE";
/// <summary>
/// 旋转
/// </summary>
public const string ROTATE = "ROTATE";
/// <summary>
/// 开枪
/// </summary>
public const string FIRE = "FIRE";
/// <summary>
/// 丢手榴弹
/// </summary>
public const string BOMB = "BOMB";
/// <summary>
/// 跳跃
/// </summary>
public const string JUMP = "JUMP";
/// <summary>
/// 装子弹
/// </summary>
public const string BULLET = "BULLET";
}
我们回到GamePanel.cs脚本,在摇杆的委托中抛出事件,
// GamePanel.cs
void Start()
{
// 移动控制摇杆
moveJointedArm.onDragCb = (direction) =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.MOVE, new Vector3(direction.x, 0, direction.y).normalized, true);
};
moveJointedArm.onStopCb = () =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.MOVE, Vector3.zero, false);
};
// 旋转控制摇杆
rotateJointedArm.onDragCb = (direction) =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.ROTATE, new Vector3(direction.x, 0, direction.y).normalized);
};
rotateJointedArm.onStopCb = () =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.ROTATE, Vector3.zero);
};
// ...
}
摇杆抛出的事件,最终的响应逻辑就是角色移动和旋转,那么我们就要在原来控制角色移动和旋转的脚本中添加事件订阅。
逻辑在哪里呢?逻辑在FPSControllerLPFP.cs脚本和AutomaticGunScriptLPFP.cs脚本中。
画个图,方便大家理解,

我们分别在FPSControllerLPFP.cs脚本和AutomaticGunScriptLPFP.cs脚本中添加事件订阅和注销,如下
// FPSControllerLPFP.cs
private void Start()
{
// ...
// 订阅事件
EventDispatcher.instance.Regist(EventNameDef.MOVE, OnEventMove);
EventDispatcher.instance.Regist(EventNameDef.ROTATE, OnEventRotate);
// ...
}
private void OnDestroy()
{
// 注销事件
EventDispatcher.instance.UnRegist(EventNameDef.MOVE, OnEventMove);
EventDispatcher.instance.UnRegist(EventNameDef.ROTATE, OnEventRotate);
// ...
}
// AutomaticGunScriptLPFP.cs
private void Start()
{
// ...
// 订阅事件
EventDispatcher.instance.Regist(EventNameDef.MOVE, OnEventMove);
// ...
}
private void OnDestroy()
{
// 注销事件
EventDispatcher.instance.UnRegist(EventNameDef.MOVE, OnEventMove);
// ...
}
流程如下,

摇杆通过MOVE事件传递了移动方向过来,我们在FPSControllerLPFP.cs脚本中把它缓存到m_moveDirection变量中,如下
// FPSControllerLPFP.cs
Vector3 _moveDirection;
private void OnEventMove(params object[] args)
{
_moveDirection = (Vector3)args[0];
}
在FixedUpdate函数中执行MoveCharacter方法,在MoveCharacter方法中根据m_moveDirection去计算移动,逻辑如下,(部分函数此处没有列出,可下载工程源码进行查看)
// FPSControllerLPFP.cs
/// <summary>
/// 移动角色
/// </summary>
private void MoveCharacter()
{
// 转为世界坐标系下的方向
var worldDirection = transform.TransformDirection(_moveDirection);
// 移动速度
var velocity = worldDirection * (input.Run ? runningSpeed : walkingSpeed);
// 检查碰撞,以便角色在跳墙时不会卡住
var intersectsWall = CheckCollisionsWithWalls(velocity);
if (intersectsWall)
{
_velocityX.Current = _velocityZ.Current = 0f;
return;
}
// 平滑运算
var smoothX = _velocityX.Update(velocity.x, movementSmoothness);
var smoothZ = _velocityZ.Update(velocity.z, movementSmoothness);
// 获取当前刚体速度
var rigidbodyVelocity = _rigidbody.velocity;
// 计算速度差
var force = new Vector3(smoothX - rigidbodyVelocity.x, 0f, smoothZ - rigidbodyVelocity.z);
// 给刚体施加一个力
_rigidbody.AddForce(force, ForceMode.VelocityChange);
}
移动的同时,还需要播放走路动画,逻辑在AutomaticGunScriptLPFP.cs脚本中,
// AutomaticGunScriptLPFP.cs
private Animator anim;
private bool isWalking;
private void OnEventMove(params object[] args)
{
isWalking = (bool)args[1];
}
private void Update()
{
// ...
if (isWalking && !isRunning)
{
anim.SetBool("Walk", true);
}
else
{
anim.SetBool("Walk", false);
}
// ...
}
此时效果

同理,旋转控制也是通过事件的响应函数来触发,流程如下,

// FpsControllerLPFP.cs
/// <summary>
/// 旋转角色
/// </summary>
private void RotateCameraAndCharacter()
{
// 平滑运算
var rotationX = _rotationX.Update(RotationXRaw, rotationSmoothness);
var rotationY = _rotationY.Update(RotationYRaw, rotationSmoothness);
// 限制竖直方向的旋转角度:
var clampedY = RestrictVerticalRotation(rotationY);
_rotationY.Current = clampedY;
// 将世界坐标系下的up方向转为相对手臂的局部坐标系下的方向
var worldUp = arms.InverseTransformDirection(Vector3.up);
// 计算最终角度(四元数)
var rotation = arms.rotation *
Quaternion.AngleAxis(rotationX, worldUp) *
Quaternion.AngleAxis(clampedY, Vector3.left);
// 父节点只沿着y轴旋转,容易漏掉此步,如果没有此步,计算移动的时候会出问题
transform.eulerAngles = new Vector3(0f, rotation.eulerAngles.y, 0f);
// 手臂自由旋转
arms.rotation = rotation;
}
此时效果


因为开枪是一个连续过程,我们要检测是否长按了开枪按钮,而UGUI的Button的onClick只能监听点击事件,所以我们需要另外实现长按事件的监听。
UnityEngine.EventSystems命名空间下有个EventTrigger类,它基本提供了所有UI事件,

我们封装一个EventTriggerListener.cs,继承EventTrigger,如下,
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// UI事件触发器
/// </summary>
public class EventTriggerListener : UnityEngine.EventSystems.EventTrigger
{
public delegate void VoidDelegate(GameObject go);
public delegate void BoolDelegate(GameObject go, bool state);
public delegate void FloatDelegate(GameObject go, float delta);
public delegate void VectorDelegate(GameObject go, Vector2 delta);
public delegate void ObjectDelegate(GameObject go, GameObject obj);
public delegate void KeyCodeDelegate(GameObject go, KeyCode key);
public VoidDelegate onClick;
public VoidDelegate onDown;
public VoidDelegate onEnter;
public VoidDelegate onExit;
public VoidDelegate onUp;
public VoidDelegate onSelect;
public VoidDelegate onUpdateSelect;
static public EventTriggerListener Get(GameObject go)
{
EventTriggerListener listener = go.GetComponent<EventTriggerListener>();
if (listener == null) listener = go.AddComponent<EventTriggerListener>();
return listener;
}
static public EventTriggerListener Get(Transform transform)
{
EventTriggerListener listener = transform.GetComponent<EventTriggerListener>();
if (listener == null) listener = transform.gameObject.AddComponent<EventTriggerListener>();
return listener;
}
public override void OnPointerClick(PointerEventData eventData)
{
if (onClick != null) onClick(gameObject);
}
public override void OnPointerDown(PointerEventData eventData)
{
if (onDown != null) onDown(gameObject);
}
public override void OnPointerEnter(PointerEventData eventData)
{
if (onEnter != null) onEnter(gameObject);
}
public override void OnPointerExit(PointerEventData eventData)
{
if (onExit != null) onExit(gameObject);
}
public override void OnPointerUp(PointerEventData eventData)
{
if (onUp != null) onUp(gameObject);
}
public override void OnSelect(BaseEventData eventData)
{
if (onSelect != null) onSelect(gameObject);
}
public override void OnUpdateSelected(BaseEventData eventData)
{
if (onUpdateSelect != null) onUpdateSelect(gameObject);
}
}
在GamePanel.cs中添加按钮长按onDown和按钮抬起onUp的监听并抛出FIRE事件,如下
// GamePanel.cs
// 开炮
void Start()
{
// ...
EventTriggerListener.Get(fireBtn).onDown += (btn) =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.FIRE, true);
};
EventTriggerListener.Get(fireBtn).onUp += (btn) =>
{
EventDispatcher.instance.DispatchEvent(EventNameDef.FIRE, false);
};
// ...
}
开枪的逻辑在AutomaticGunScriptLPFP.cs脚本中,流程如下
// AutomaticGunScriptLPFP.cs
bool _fire;
private void OnEventFire(params object[] args)
{
_fire = (bool)args[0];
}
private void Update()
{
// ...
if (_fire && !outOfAmmo && !isReloading && !isInspecting && !isRunning)
{
if (Time.time - lastFired > 1 / fireRate)
{
lastFired = Time.time;
// 执行开枪
DoFire();
}
}
// ...
}
void DoFire()
{
// 以下具体代码见工程代码,此处不展开了
// 播放开枪音效
// 播放开炮动画
// 播放枪口粒子
// 实例化子弹并给子弹一个力
// 实例化弹壳
}
此时效果,

同理,通过事件订阅触发丢手雷、跳跃、装子弹等逻辑。



给这位提问的同学一次上电视的机会,我把他贴到墙上,

初始的时候图片半透明,角色靠近的时候图片完全不透明,用到的是触发器,

关于触发器的教程,我之前写过一些文章,《【学Unity的猫】第十章:Unity的物理碰撞,流浪喵星计划》
这里的检测逻辑如下,
using UnityEngine;
using UnityEngine.UI;
public class TipsBoard : MonoBehaviour
{
public Image board;
private void Start() {
board.color = new Color(1, 1, 1, 0.3f);
}
private void OnTriggerEnter(Collider other)
{
if ("Player" != other.tag) return;
board.color = new Color(1, 1, 1, 1);
}
private void OnTriggerExit(Collider other)
{
if ("Player" != other.tag) return;
board.color = new Color(1, 1, 1, 0.3f);
}
}
本文工程源码我已上传到CODE CHINA,感兴趣的同学可自行下载学习,
地址:https://codechina.csdn.net/linxinfa/FirstPersonGame
注:我使用的Unity版本是2021.1.7f1c1,如果你使用的版本与我的不同,可能会有一些兼容问题。

好啦,就到这里吧~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame
?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit