在上一节中我们给球体添加了纹理,并且为场景设置了一个目的区域。当球体处于目的区域内时,其将变色。在这一节里,我们将完善球体在到达目的区域时游戏的反馈,增加音乐和UI显示。
增加小球进入目的区域的音效。
增加计分的UI界面。
-本节相关内容请读者参考:
-https://docs.unity.cn/cn/current/ScriptReference/Resources.html,《Resources》
-https://docs.unity.cn/cn/current/Manual/class-AudioSource.html,《音频源》
我们可以用两个方法将音效素材导入到游戏。第一个就是按照上一节绑定变量的方法,将音效素材作为JudgeController的属性绑定到类中;第二个是使用Resources文件夹。同时,为了让JudgeController可以播放音乐,我们需要为其添加一个Audio Source组件。
Resources文件夹是一类文件夹,其文件夹名为Resources,位于工程目录下的Assets文件夹内(不需要位于根目录下,比如Assets\a\Resources也有效)。我们可以在脚本中使用Resources.Load来访问Resources文件夹下的文件。
第一种方法因为已经在前面使用过了,所以在这里我们不多赘述,来看第二种方法。由于unity预设建立的项目中没有Resources文件夹,我们先在Assets文件夹下建立一个Resources文件夹,然后将音效文件放进去。
然后码代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class JudgeController : MonoBehaviour
{
public GameObject sphere; //球体的引用
//近点和远点分别是目的区域离原点最近和最远的点
public Vector3 nearP=new Vector3(2.5f,0,2.5f); //近点
public Vector3 farP=new Vector3(5,0,5); //远点
private AudioClip m;
bool hasClip=false; //标志,球体在一次进出是否播放过音效
// Start is called before the first frame update
void Start()
{
m=Resources.Load<AudioClip>("001"); //加载Assets/.../Resources/001.*
}
// Update is called once per frame
void Update()
{
Vector3 p=sphere.GetComponent<Transform>().position; //位置
if(p.x>nearP.x && p.x<farP.x && p.z>nearP.z &&p.z<farP.z){
//小球中心点在目的区域内
GetComponent<AudioSource>().clip=m;
if(!hasClip){
GetComponent<AudioSource>().Play(); //播放
hasClip=true;
}
sphere.GetComponent<Sphere>().changeMaterial(true); //调用Sphere的函数
}else{
//不在区域内,变回来
hasClip=false;
sphere.GetComponent<Sphere>().changeMaterial(false);
}
}
}
-本节相关内容请读者参考:
-https://docs.unity.cn/cn/current/Manual/UISystem.html,《UI》
-https://docs.unity.cn/cn/current/Manual/UICanvas.html,《画布》
UI是User Interface(用户界面)的缩写。大多数游戏都会有UI,用于记录游戏的得分情况等信息。在unity中,UI必须作为Canvas(画布)的子项存在。如果直接创建UI,unity会在此之前自动创建一个Canvas并将被创建的UI作为子项。
Hierarchy->UI->Text,创建一个TextUI,我们命名为Bonus。设定初始分数为0,球体进入目的区域时分数加1。改写JudgeController的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class JudgeController : MonoBehaviour
{
public GameObject sphere; //球体的引用
public GameObject bonusUI; //显示分数的UI
//近点和远点分别是目的区域离原点最近和最远的点
public Vector3 nearP=new Vector3(2.5f,0,2.5f); //近点
public Vector3 farP=new Vector3(5,0,5); //远点
private AudioClip m; //音效文件
private int bonus; //分数
bool hasClip=false; //标志,球体在一次进出是否播放过音效
// Start is called before the first frame update
void Start()
{
bonus=0;
m=Resources.Load<AudioClip>("001"); //加载Assets/.../Resources/001.*
}
// Update is called once per frame
void Update()
{
Vector3 p=sphere.GetComponent<Transform>().position; //位置
if(p.x>nearP.x && p.x<farP.x && p.z>nearP.z &&p.z<farP.z){
//小球中心点在目的区域内
GetComponent<AudioSource>().clip=m;
if(!hasClip){
bonus++;
bonusUI.GetComponent<Text>().text=bonus.ToString(); //更新分数
GetComponent<AudioSource>().Play(); //播放
hasClip=true;
}
sphere.GetComponent<Sphere>().changeMaterial(true); //调用Sphere的函数
}else{
//不在区域内,变回来
hasClip=false;
sphere.GetComponent<Sphere>().changeMaterial(false);
}
}
}
可以看到我们增加了一个引用bonusUI,将这个变量与Bonus绑定。然后调整Bonus的大小和其Text的内容(由于后续内容由JudgeController控制,这里只需要将Text置为0即可)。
同样是UI,TextMeshPro可以被看做是Text的升级版。因为文档里没有与其相关的资料,所以在这里笔者会简单描述一下与它有关的信息。
TextMeshPro又称为TMP,一开始是一个外部插件,在最近的版本中才被包含进unity本体中。TMP采用了SDF文字渲染技术,相比原生的Text组件它能保证文字在缩放数倍后仍然保持平滑(其实就是矢量绘图)。
但是保持文字的平滑自然需要代价,TMP会为字体创建一个纹理集,而此纹理集在字体所属语言为中文的情况下会占用较大的空间。
而TMP也不只有这一个长处,TMP还可以设置文字的描边颜色渐变等,并且可以图文混用。
-本节相关内容请读者参考:
-https://docs.unity.cn/cn/current/ScriptReference/Random.html,《Random》
既然已经有了一套基础的得分系统,不妨在之前的基础上增加一个得分点,比如说,让平面上的正方体在进入得分区域时重置位置并且得分。
按照之前的思路,修改JudgeController,并给Cube添加脚本:
JudgeController.cs(注:前文中对于近点/远点的定义有误,在这里更正)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class JudgeController : MonoBehaviour
{
public GameObject sphere; //球体的引用
public GameObject bonusUI; //显示分数的UI
public GameObject cube; //正方体的引用
//近点和远点分别是目的区域离xy最大的点和xy最小的点
public Vector3 nearP=new Vector3(2.5f,0,2.5f); //近点
public Vector3 farP=new Vector3(5,0,5); //远点
private AudioClip m; //音效文件
private int bonus; //分数
bool hasClip=false; //标志,球体在一次进出是否播放过音效
// Start is called before the first frame update
void Start()
{
bonus=0;
m=Resources.Load<AudioClip>("001"); //加载Assets/.../Resources/001.*
}
bool comPosition(Vector3 p)
{ //比较传入点与近点/远点的相对位置,内部函数
if(p.x>nearP.x && p.x<farP.x && p.z>nearP.z &&p.z<farP.z){
return true;
}else{
return false;
}
}
// Update is called once per frame
void Update()
{
Vector3 p=sphere.GetComponent<Transform>().position; //位置
if(this.comPosition(p)){
//小球中心点在目的区域内
GetComponent<AudioSource>().clip=m;
if(!hasClip){
bonus++;
bonusUI.GetComponent<Text>().text=bonus.ToString(); //更新分数
GetComponent<AudioSource>().Play(); //播放
hasClip=true;
}
sphere.GetComponent<Sphere>().changeMaterial(true); //调用Sphere的函数
}
else{
//不在区域内,变回来
hasClip=false;
sphere.GetComponent<Sphere>().changeMaterial(false);
}
Vector3 c=cube.GetComponent<Transform>().position;
if(this.comPosition(c)){
//一旦抵达目标地点,就开始传送,所以不需要额外标志,也没有else
cube.GetComponent<Cube>().transmit(nearP,farP);
bonus+=2;
bonusUI.GetComponent<Text>().text=bonus.ToString(); //更新分数
}
}
}
Cube.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random=UnityEngine.Random;
public class Cube : MonoBehaviour
{
public GameObject plane; //平台,用于计算区域长度
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
float comVxy(float far,float near,float width){ //封装内部函数
if(far<width){ //换成near>-width也是一样的,因为目的区域必然不大于整个区域,所以只要比较一个
if(Random.value>0.5){
return Random.Range(far,width);
}
else{
return Random.Range(-1*width,near);
}
}
else{
return Random.Range(-1*width,near);
}
}
public void transmit(Vector3 near,Vector3 far){
//以0,0,0为中心的情况下,给出:整个区域长度、目的区域近点和远点 将正方体传送到整个区域之内,目的区域之外
//设定平面长宽相等,生成在与目的区域相对的区域内
Vector3 v2=new Vector3(0,-4.5f,0);
float myWidth=GetComponent<Transform>().localScale.x*0.5f; //由于算的是物体中心的位置,要减去到中心的距离
float width=plane.GetComponent<Transform>().localScale.x*5-myWidth;
v2.x=this.comVxy(far.x,near.x,width);
v2.z=this.comVxy(far.z,near.z,width); //笔者的实例里y轴朝上
transform.localPosition=v2; //这里是相对坐标
print(v2);
}
}
可以看到在Cube.cs中,我们传送物体使用的是transform.localPosition(GetComponent<Transform>().localPosition),而非position。这是因为笔者在之前建立了一个空游戏对象作为Cube的父对象,而这里需要对正方体的父对象定位(如果没有父对象localPosition就与position相同)。我们把position称为世界位置,而localPosition则是相对位置。position是游戏对象在绝对坐标系下(世界坐标系,也就是无父对象时Transform面板显示的坐标)的位置,而localPosition则是position对游戏对象的所有父对象的位置进行变换之后的位置。例如在场景下有一个根游戏对象A(1,1,1),其子游戏对象B在Transform面板里的坐标为(0,1,-1),则B在世界坐标系的坐标为A+B(1,2,0)。
由于需要判断两个物体的位置,为了节省代码量我们把判断位置部分的代码封装为一个函数comPosition(其实也就是节省了一行不到,但是也省得写了)。同时,建议读者尽量使用unity的Random来生成随机数,而不使用C#自带的Random。
映维网Nweon 2023年03月15日)Unity的XR Interaction Toolkit(XRI)是一个基于组件的high level交互系统,主要用于创建VR和AR体验。它为交互提供了一个通用框架,并简化了跨平台的创建。新发布的XRI 2.3版本增加了三个关键功能:
值得一提的是,XR开发者兼LearnXR.io创始人迪尔默·瓦莱西洛斯(Dilmer Valecillos)发布了一个XRI 2.3视频教程:而下面我们将介绍XRI 2.3的主要亮点:
https://v.qq.com/x/page/g3506gcwsds.html
全面支持关节式手部追踪
与XRI 2.3一起,Unity在预发行版中提供了XR Hands package。XR Hands是一个新的XR子系统,它添加了支持手部追踪的API。它包括OpenXR的内置支持,不久将支持Meta平台。另外,第三方硬件提供商可以通过API文档,从现有XR SDK中获取实时追踪数据。
XRI的这一版本包括Hands Interaction Demo。这个展示手部交互设置的示例包允许你在裸手交互和控制器之间切换,无需更改设备场景中的任何内容。使用所述功能,你的内容可以从标准控制器设置开始,但可以无缝过渡到特定任务或游戏中的自然交互。
XRI 2.3同时支持通过XR Poke Interactitor进行的自然点戳交互。这允许你在3D UI或启用XRI的UGUI画布元素使用裸手或控制器进行点戳。
使用眼睛注视进行交互
像微软HoloLens 2、Meta Quest Pro和PSVR 2这样的新头显包括可以追踪用户视线的传感器。基于注视的交互可以帮助你构建感觉更自然的XR应用程序,并提供一种与内容交互的额外方式。为了支持这种类型的交互,Unity引入了由眼睛注视或头部姿势驱动的XR Gaze Interactor。你可以使用这个交互器进行直接操作。
由于Unity通常不建议应用程序完全由眼睛控制,所以团队提供了一种额外的控制器和基于手部的交互辅助形式,从而帮助用户选择特定对象:XR Interactible Snap Volume。这个组件可以作为XR Gaze Interactor的补充,因为它允许在瞄准对象周围的定义区域时将交互锁定至附近的可交互对象。Snap Volume同时可以在没有Gaze Interactor的情况下使用,以便用户更容易地选择对象。
视听可供性
使用裸手进行交互与使用控制器不同,因为没有触觉反馈来确认交互何时发生。可供性系统可以根据对象的交互状态动画化对象动或触发声音效果,从而帮助缓解这种反馈差距。
用双手拉伸、摆动和旋转
新的XR General Grab Transformer降低了层次结构的复杂性,并允许一个通用Transformer在可交互平台支持单手和双手交互,而不是多个Grab Transformer。它同时支持双手缩放,允许你通过移动双手来上下缩放对象。
Unity同时添加了一个Interaction Group组件,从而允许开发人员将interactor分组,并按优先级对其进行排序,这使得每个组在给定时间只能有一个interactor进行交互。例如,当Poke、Direct和Ray interactor组合在一起时,点戳一个按钮将暂时阻止其他interactor与场景交互。
无头显XR迭代变得更容易
在头显测试XR应用程序很重要,但在编辑器中测试有助于减少迭代时间。在这个版本中,团队优化了XR Device Simulator,其中包括一个新的屏幕UI小组件,从而允许你更容易地查看哪些输入驱动模拟器,哪些输入当前处于活动状态。
Unity同时添加了新的模拟模式,以便你可以在常用的控制模式之间切换。在启动时,设 XR Device Simulator激活新的第一人称射击(FPS)模式,并操作头显和两个控制器。然后,你可以在其他模式中循环操作各个设备:头显、左控制器和右控制器。要使用 XR Device Simulator,请从Package Manager导入示例。
新的XRI示例项目
Unity提供了新的示例项目,并用于展示了你可以在XRI 2.3中使用的一系列XR体验构建模块。你可以访问GitHub的示例项目,并使用它开启下一个XR应用程序。
展望未来
尽管眼手操作依然处于早期阶段,但Unity正在努力优化体验。随着迈向XRI 2.4及以上版本,团队将继续根据用户反馈来改进工具。
介
网络套接字是在unity webgl 游戏中扩展网络的一个好的选择。网络套接字提供两种现存的连接到服务器的方式。不幸的是,Unity WebGL中的网络套接字这个时候会受限制。Unity团队中Unity商店中有一个示例源库,但是,网络套接字功能还没有全部被拓展。这篇文章介绍了如何使用Javascript Socket.io来扩展现存的通过网络服务器来连接p2p的方式。
Socket.io一种特殊的使用方法是服务器编码能够用Javascript来写,这个作用是允许代码在服务器和客户端都很相似,为了更清晰和易维护。
解释概念
为了用webgl 游戏来扩展socket.io:,这个方法有三个重要的部分:
用unity 创建一个网络连接脚本
创建javascript客户端代码
创建javascript服务端代码
通过转换数据到/从JSON对象和字符串,数据传回到unitygame。
扩展
项目创建
为了尝试这种方法,你的服务器必须要安装Node.js。一旦安装,打开命令行并转到unity 项目的WebGl 的目录。然后创建JSON文件命名为package.json在目录下用下面的内容。
{
"dependencies": {
"express": "4.13.4",
"socket.io": "1.4.5"
}
}
实际最新版本可以通过命令行“hpm info express version”来获得,文档被创建后:
1:运行“npm install”来下载节点模块,传递socket.io 到你创建的目录。
2:创建一个文件夹"public"到你unity创建的目录
3:创建一个空白的“client.js”脚本到“public”文件夹
Unity 特有的代码
下面是一个示例你可以用另外的客户端javascript来交互,反过来也可以与服务器端脚本交互。
JSONUtility类在例子中会有影响,因为只有字符串数据能能够通过Application.externalCall及在客户端javaacript 方面,它的对应的接收方法。
使用下列代码,数据可以被传输并在浏览器中执行:
Application.ExternalCall (string functionName, string dataParameter);
理论上,我们应该编码网络管理器前,首先设置一些数据类,用JSONUnit来添加转变 到/从Json 对象。
// Sample Data Classes that could be stringified by JSONUtility
public class User
{
public string uid;
public string displayname;
public User(string u,string d)
{
uid=u;
displayname=d;
}
}
public class MatchJoinResponse
{
public bool result;
}
下面创建一个C#脚本命名为“NetworkManager”并赋予场景中的游戏对象(GameObject)
using UnityEngine;
public class NetworkManager : MonoBehaviour
{
public void ConnectToServer(User user)
{
Application.ExternalCall ("Logon", JsonUtility.ToJson (user));
}
public void OnMatchJoined(string jsonresponse)
{
MatchJoinResponse mjr=JsonUtility.FromJson<MatchJoinResponse> (jsonresponse);
if(mjr.result)
{
Debug.Log("Logon successful");
}
else
{
Debug.Log("Logon failed");
}
}
public void BroadcastQuit()
{
Application.ExternalCall ("QuitMatch");
}
}
当你重新创建你的Unity项目时,确保你添加下面几行代码到“index.html”文件:
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="client.js"></script>
客户端Javascript
客户端javascript接受了从unity game的调用在以前部分并在下一部分与服务器连接。用下面的调用来调用Unity Game:
SendMessage(string GameObjectName, string MethodName, string data);
例子使用了暂停功能来阻止在事件中游戏的进程,服务器性能下降。在客户端的.js文件:
var connection;
var logonTimeout;
var logonCallback=function (res)
{
clearTimeout(logonTimeout);
// Send Message back to Unity GameObject with name 'XXX' that has NetworkManager script attached
SendMessage('XXX','OnMatchJoined',JSON.stringify(response));
};
function Logon(str)
{
var data=JSON.parse(str);
connection=io.connect();
// Setup receiver client side function callback
connection.on('JoinMatchResult', logonCallback);
// Attempt to contact server with user data
connection.emit('JoinMatchQueue', data);
// Disconnect after 30 seconds if no response from server
logonTimeout=setTimeout(function(){
connection.disconnect();
connection=null;
var response={result:false};
// Send Message back to Unity GameObject with name 'XXX' that has NetworkManager script attached
SendMessage('XXX','OnMatchJoined',JSON.stringify(response));
},30000);
}
function QuitMatch()
{
if(connection)
{
connection.disconnect();
connection=null;
}
}
服务器Javascript
例子中服务器传递路径和文件简单递送。在例子中,服务器和客户端功能是类似的。
// Variable Initialization
var express=require('express'),
app=express(),
server=require('http').createServer(app),
port=process.env.PORT || 3000,
io=require('socket.io')(server);
// Store Client list
var clients={};
// Allow express to serve static files in folder structure set by Unity Build
app.use("/TemplateData",express.static(__dirname + "/TemplateData"));
app.use("/Release",express.static(__dirname + "/Release"));
app.use(express.static('public'));
// Start server
server.listen(port);
// Redirect response to serve index.html
app.get('/',function(req, res)
{
res.sendfile(__dirname + '/index.html');
});
// Implement socket functionality
io.on('connection', function(socket){
socket.on('JoinMatchQueue', function(user){
socket.user=user;
clients[user.uid]=socket;
var response={result:true};
socket.emit('JoinMatchResult', response);
console.log(user.uid + " connected");
});
socket.on('disconnect', function()
{
delete clients[socket.user.uid];
console.log(socket.user.uid + " disconnected");
});
});
一旦保存,服务器可以在主机上启动,用Node.js用命令行"node server.js".
其他记录
游戏中最可能要考虑的是性能。数据从Json对象转化为Json对象以及浏览器中的字符串并返回游戏中;每个转变在方法中是必要的开销。如果阐述这种方法的人有任何关于性能与经验的信息,我很乐意倾听的。
同时,如果在Azure服务器解析Socket.io,确保下面的代码也添加到网站的web.config文件中:
<webSocket enabled="false"/>
这会使IIS WebSockets模块不起作用,包括自己的WebSockets解析和与Node.js 具体的websocket模块冲突,比如Socket.IO.
结论
这篇文章讲述了一种把Socket.io和WebGLUnity创建连接成网络的方式。
显然更好的方式执行Socke.io和Unity 互用性会是在unity环境中主要的唯一的插件。文章中表现出来的解决方式是一个可扩展的可替代的。直到某些时候,Unity解析websocket原生功能性地到游戏引擎中。最终,有添加的好处在javascript客户端和服务器代码会很相似。
关于作者
Damian Osiebo是一个职业海军军官。从MIT中取得计算机科学硕士学位。空闲时间是一个游戏爱好者和编程狂人。
许可证
GDOL(Gamedev.net Open License)
原文链接:https://www.gamedev.net/resources/_/technical/multiplayer-and-network-programming/integrating-socketio-with-unity-5-webgl-r436
原文作者:Damian Osiebo
*请认真填写需求信息,我们会在24小时内与您取得联系。