Unity学习笔记6:多人游戏进阶篇
来源:互联网 发布:网络电视盒子刷机软件 编辑:程序博客网 时间:2024/06/05 07:44
这篇笔记学习三个问题:多人游戏的level loading,连接测试以及带宽优化。
一.关卡装载。
必须了解的函数:
1.RequireComponent()
//JavaScript实例
// Mark the PlayerScript as requiring a rigidbody in the game object.
//指定此script绑定的对象必须包含rigidbody组件,没有则自动添加
@script RequireComponent(Rigidbody)
function
FixedUpdate() {
rigidbody.AddForce(Vector3.up);
}
//C#实例
[RequireComponent (
typeof
(Rigidbody))]
public
class
PlayerScript : MonoBehaviour {
void
FixedUpdate() {
rigidbody.AddForce(Vector3.up);
}
}
2.DontDestroyOnLoad()
当装载一个新的level,旧场景的对象会被销毁,调用此函数可以在装载新level时保留某个对象,如果这个对象是GameObject或者Component那么它的整个transform层都将被保留。
//C#代码
public
class
example : MonoBehaviour {
void
Awake() {
DontDestroyOnLoad(transform.gameObject);
}
}
3.Network.peerType
Network类的一个数据成员,返回值是NetworkPeerType的枚举类型,其值有NetworkPeerType.Disconnected//无客户端连接且没有初始化服务器
NetworkPeerType.Connecting//正在连接服务器
NetworkPeerType.Server//正在做为服务器运行
NetworkPeerType.Client//正在作为客户端运行
4.Network.RemoveRPCsInGroup()
清除某个组的RPC buffer的所有RPC调用。
5.Network.SetSendingEnabled (group : int, enabled : boolean)
开启或关闭某个组的Network View的消息或RPC发送。比如要开始装载某个新level,与旧level相关的消息或RPC就可以停止发送。
6.Network.IsMessageQueueRunning()
开启或关闭消息队列的运行,如果关闭则RPC调用不再执行,状态同步机制也不工作。
7.Network.SetLevelPrefix(prefix: int)
为所有Network ViewID设置一个前缀。这将有效防止旧level的数据和新levle的数据互相干扰,这一效果不会造成网络带宽负担,但是由于Network ViewID添加了前缀,导致ID池数量有所减少。
8.Object.FindObjectsOfType(type:Type)
返回一个活动的对象列表(数组)。不返回assets和非活动的对象。这个函数会导致系统变慢,建议不要在每帧都使用,大多数情况下你都可以使用Singleton模式(参见C#设计模式)作为替代。
//C#实例
public
class
example : MonoBehaviour {
void
OnMouseDown() {
HingeJoint[] hinges = FindObjectsOfType(
typeof
(HingeJoint))
as
HingeJoint[];
foreach
(HingeJoint hinge
in
hinges) {
hinge.useSpring =
false
;
}
}
}
9.SendMessage()
GameObject.SendMessage(methodName : string, value : object = null, options : SendMessageOptions = SendMessageOptions.RequireReceiver)
Component.SendMessage(methodName : string, value : object = null, options : SendMessageOptions = SendMessageOptions.RequireReceiver)
调用methodName所指向的方法,若没有传递任何参数被调用的方法可以选择忽略。当SendMessageOptions设置为RequireReceiver时,若没有组件接收该调用消息,则打印一条错误信息。
//C#实例
public
class
example : MonoBehaviour {
void
ApplyDamage(
float
damage) {
print(damage);
}
void
Example() {
gameObject.SendMessage(
"ApplyDamage"
, 5.0F);
}
}
这些函数在下面的例子中会被用到。
以下是一个简单的多人游戏关卡装载的例子,在关卡装载过程中必须确保不处理其他的network消息,在装载完毕一切就绪之前任何消息不得发送。关卡一旦装载好以后会自动发送一条消息到所有的脚本,通知它们关卡已经装载完毕。SetLevelPrefix()函数保证自动过滤与新装载关卡无关的network数据更新,这些无关数据更新可能是比如说上一个关卡的数据。本例还利用组功能分隔开游戏数据和关卡装载通信数据。组0用于游戏数据交通,而组1用于关卡装载。当关卡正在装载的时候,关闭组0开启组1.
//Javascript实例
var
supportedNetworkLevels : String[] = [
"mylevel"
];
var
disconnectedLevel : String =
"loader"
;
private
var
lastLevelPrefix = 0;
function
Awake ()
{
// Network level loading is done in a separate channel.
DontDestroyOnLoad(
this
);
networkView.group = 1;
Application.LoadLevel(disconnectedLevel);
}
function
OnGUI ()
{
if
(Network.peerType != NetworkPeerType.Disconnected)
{
GUILayout.BeginArea(Rect(0, Screen.height - 30, Screen.width, 30));
GUILayout.BeginHorizontal();
for
(
var
level
in
supportedNetworkLevels)
{
if
(GUILayout.Button(level))
{
Network.RemoveRPCsInGroup(0);
Network.RemoveRPCsInGroup(1);
networkView.RPC(
"LoadLevel"
, RPCMode.AllBuffered, level, lastLevelPrefix + 1);
}
}
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
}
@RPC
function
LoadLevel (level : String, levelPrefix : int)
{
lastLevelPrefix = levelPrefix;
// There is no reason to send any more data over the network on the default channel,
// because we are about to load the level, thus all those objects will get deleted anyway
Network.SetSendingEnabled(0,
false
);
// We need to stop receiving because first the level must be loaded first.
// Once the level is loaded, rpc's and other state update attached to objects in the level are allowed to fire
Network.isMessageQueueRunning =
false
;
// All network views loaded from a level will get a prefix into their NetworkViewID.
// This will prevent old updates from clients leaking into a newly created scene.
Network.SetLevelPrefix(levelPrefix);
Application.LoadLevel(level);
yield
;
yield
;
// Allow receiving data again
Network.isMessageQueueRunning =
true
;
// Now the level has been loaded and we can start sending out data to clients
Network.SetSendingEnabled(0,
true
);
for
(
var
go
in
FindObjectsOfType(GameObject))
go.SendMessage(
"OnNetworkLoadedLevel"
, SendMessageOptions.DontRequireReceiver);
}
function
OnDisconnectedFromServer ()
{
Application.LoadLevel(disconnectedLevel);
}
@script RequireComponent(NetworkView)
二.连接测试。
Network.TestConnection (forceTest : boolean = false) : ConnectionTesterStatus
此函数用于测试网络连接状况。
返回值类型ConnectionTesterStatus是枚举类型,其值如下表所示。
其中PublicIPIsConnectable,PublicIPPortBlocked以及PublicIPNoServerStarted是测试是否具有公共IP的结果,这个测试主要用于作为服务器运行的情形,因为即使没有Public IP,客户端通过NAT punchthough或者Facilitator等还是可以连接到服务器,但是要作为服务器运行就需要有Public IP.此项测试需要有一个已运行的服务器实例,然后测试服务器尝试连接该服务器的指定IP和端口。
最下面4项是测试是否具备NAT punchthrough的能力,这个测试既用于服务器也用于客户端,不需要提前进行设置。其中Full cone type和Address-restricted cone type支持完整的NAT punchthrough功能,Port retricted不能连接到Symmetric,但可以连接到另外三种,Symmetric则只能与前两种连接。
此函数提供的网络连接测试功能具有异步性,上一次调用的测试结果下一次调用才能获得结果。而且为了保证已完成的测试不被重复进行,下一次测试(比如网络状况发生变化时需要进行重新测试)需要给参数forceTest赋值true.
下面是一个javascript写的测试实例:
var
testStatus =
"Testing network connection capabilities."
;
var
testMessage =
"Test in progress"
;
var
shouldEnableNatMessage : String =
""
;
var
doneTesting =
false
;
var
probingPublicIP =
false
;
var
serverPort = 9999;
var
connectionTestResult = ConnectionTesterStatus.Undetermined;
// Indicates if the useNat parameter be enabled when starting a server
var
useNat =
false
;
function
OnGUI() {
GUILayout.Label(
"Current Status: "
+ testStatus);
GUILayout.Label(
"Test result : "
+ testMessage);
GUILayout.Label(shouldEnableNatMessage);
if
(!doneTesting)
TestConnection();
}
function
TestConnection() {
// Start/Poll the connection test, report the results in a label and
// react to the results accordingly
connectionTestResult = Network.TestConnection();
switch
(connectionTestResult) {
case
ConnectionTesterStatus.Error:
testMessage =
"Problem determining NAT capabilities"
;
doneTesting =
true
;
break
;
case
ConnectionTesterStatus.Undetermined:
testMessage =
"Undetermined NAT capabilities"
;
doneTesting =
false
;
break
;
case
ConnectionTesterStatus.PublicIPIsConnectable:
testMessage =
"Directly connectable public IP address."
;
useNat =
false
;
doneTesting =
true
;
break
;
// This case is a bit special as we now need to check if we can
// circumvent the blocking by using NAT punchthrough
case
ConnectionTesterStatus.PublicIPPortBlocked:
testMessage =
"Non-connectable public IP address (port "
+
serverPort +
" blocked), running a server is impossible."
;
useNat =
false
;
// If no NAT punchthrough test has been performed on this public
// IP, force a test
if
(!probingPublicIP) {
connectionTestResult = Network.TestConnectionNAT();
probingPublicIP =
true
;
testStatus =
"Testing if blocked public IP can be circumvented"
;
timer = Time.time + 10;
}
// NAT punchthrough test was performed but we still get blocked
else
if
(Time.time > timer) {
probingPublicIP =
false
;
// reset
useNat =
true
;
doneTesting =
true
;
}
break
;
case
ConnectionTesterStatus.PublicIPNoServerStarted:
testMessage =
"Public IP address but server not initialized, "
+
"it must be started to check server accessibility. Restart "
+
"connection test when ready."
;
break
;
case
ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted:
testMessage =
"Limited NAT punchthrough capabilities. Cannot "
+
"connect to all types of NAT servers. Running a server "
+
"is ill advised as not everyone can connect."
;
useNat =
true
;
doneTesting =
true
;
break
;
case
ConnectionTesterStatus.LimitedNATPunchthroughSymmetric:
testMessage =
"Limited NAT punchthrough capabilities. Cannot "
+
"connect to all types of NAT servers. Running a server "
+
"is ill advised as not everyone can connect."
;
useNat =
true
;
doneTesting =
true
;
break
;
case
ConnectionTesterStatus.NATpunchthroughAddressRestrictedCone:
case
ConnectionTesterStatus.NATpunchthroughFullCone:
testMessage =
"NAT punchthrough capable. Can connect to all "
+
"servers and receive connections from all clients. Enabling "
+
"NAT punchthrough functionality."
;
useNat =
true
;
doneTesting =
true
;
break
;
default
:
testMessage =
"Error in test routine, got "
+ connectionTestResult;
}
if
(doneTesting) {
if
(useNat)
shouldEnableNatMessage =
"When starting a server the NAT "
+
"punchthrough feature should be enabled (useNat parameter)"
;
else
shouldEnableNatMessage =
"NAT punchthrough not needed"
;
testStatus =
"Done testing"
;
}
}
如果Server和Client的NAT类型可以被提前获知,那么可以不用TestConnection()函数,自己就能写出一个简单的比较函数,只要其中之一不是Symmetric类型,两台机器就可以进行连接。
javascript的例子如下:
function
CanConnectTo(type1: ConnectionTesterStatus, type2: ConnectionTesterStatus) : boolean
{
if
(type1 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted &&
type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
return
false
;
else
if
(type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
type2 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted)
return
false
;
else
if
(type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
return
false
;
return
true
;
}
其中用到的一些函数和变量:
1.Network.TestConnectionNAT(forceTest : boolean = false)
专用于测试NAT punchthrough,即使机器没有NAT IP(内部IP)地址,而拥有一个外部公共地址。
2.Time.time
Time.time的值是游戏开始运行到当前的时间。下面是一个利用Time.time实现子弹按一定时间间隔连续发射的例子:
public
class
example : MonoBehaviour {
public
GameObject projectile;
public
float
fireRate = 0.5F;
private
float
nextFire = 0.0F;
void
Update() {
if
(Input.GetButton(
"Fire1"
) && Time.time > nextFire) {
nextFire = Time.time + fireRate;
GameObject clone = Instantiate(projectile, transform.position, transform.rotation)
as
GameObject;
}
}
}
三.优化带宽。
带宽的使用量取决于你是否使用Reliable Delta Compressed或者Unreliable类型的状态同步机制。Unreliable模式下,整个被同步的对象在每次同步更新以后都按序传送,同步更新频率取决于Network.sendRate的值,默认情形下这个值为15.Unreliable模式下数据更新非常频繁但是不保证数据包的送达,如果有丢包处理方法是简单的忽略。此模式用于游戏舞台变化非常频繁迅速,并且少量的丢包可忽略的情形。Unreliable的带宽使用量还是比较大的,减少带宽的方法可以是降低数据传送率,15的默认值对一般游戏来说是个合适的取值。
Reliable Delta Compressed模式下数据按序传输,且保证数据包不丢失,如果有丢包现象,会等待数据包重传,如果数据包没有按序到达,会存入缓冲等待尚未到达的数据。数据等待重传会一定程度的降低数据传输效率,但是因为数据是Delta Compressed,只有更改的数据才会被更新同步,如果数据无变化则没有数据包传送。所以Reliable Delta Compressed模式的效果取决于游戏的场景变化频繁程度。
另一个问题是,什么样的数据需要被同步?这是一个足够游戏设计者发挥创意的问题,处理的好坏可以决定带宽的优化情况。一个例子是动画数据的同步,如果动画组件被Network View组件监视,动画属性的变化会被严格同步,动画的每一帧在每个客户端都表现相同。虽然这在某些情况下是可取的,但是一般情况下角色只需要知道角色处于走路,跳跃,跑步等等什么样的状态就足够,仅仅发送需要播放哪些动画帧的信息即可完成同步。这比同步整个动画组件要节省带宽得多。
再者,什么时候需要同步?当两个玩家在分隔在地图两个不同的区域,他们在一段时间内不能见到彼此,那么这时候的同步完全没有多大必要。而在地图一个区域内物理上有机会互相接触的玩家则需要同步。这样可以节省部分带宽。这个概念被称为相关集(Relevant Sets),即在某个特定时间,某个特定的客户端只与整个游戏的一个子集(subset)相关。根据客户端的相关集来进行数据同步可以使用RPC调用,后者可以对同步进行更多的控制。
在关卡装载的过程中不需要担心数据同步占用带宽的优化问题,因为每个玩家只需等待其他玩家都装载完毕即可,所以在关卡装载时常常涉及到传输大量的数据,比如图像和音频数据等。
- Unity学习系列笔记6:多人游戏进阶篇
- Unity学习笔记6:多人游戏进阶篇
- Unity学习笔记6:多人游戏进阶篇
- Unity学习笔记6:多人游戏进阶篇
- Unity学习系类笔记5:多人游戏基础
- Unity学习笔记5:多人游戏基础
- Unity学习笔记5:多人游戏基础
- unity学习笔记6
- unity学习笔记6
- Unity学习多人游戏的网络同步入门
- Unity学习笔记——Unity基础一:unity界面、场景、游戏物体、组件等基本概念
- JavaScript学习笔记-进阶篇
- 自己用的unity学习笔记(二)——unity进阶
- [unity&独立游戏]多人游戏插件
- 【Unity 3D】学习笔记十七:自定义游戏中字体
- 【Unity 3D】学习笔记十九:实例:游戏人物移动
- Unity学习笔记--鹰击长空类固定视角游戏摄像机跟随
- Unity 3D游戏开发学习笔记(1) 井字棋
- Xcode升级了6.3 出现的警告:Auto property synthesis will not synthesize property
- Android之自动显示软键盘
- Spring security入门
- SVN项目检出报错
- SQL提高查询效益之in、not in、between、like等条件讲述
- Unity学习笔记6:多人游戏进阶篇
- 如何使用C语言编写精确定时程序?
- 【我们都爱Paul Hegarty】斯坦福IOS8公开课个人笔记18 Gestures手势
- android权限大全(整理)
- 软件测试总结
- 杭电2005(第几天?)java字符串水过
- Simple Calculation
- ASP.NET NPOI Excel导入、导出,高级、全面、总结版
- 从已有java web项目中学习shiro安全管理