标签:
转载 |
Unity 多个玩家开发教程
Unity多人在线教程
欢迎Unity用户,在这儿你将学习网络的使用。
希望你们过得愉快并且希望这个教程对你们有所帮助。
1。简介
这个教程的目的是展示Unity网络功能,我们将告诉你如何创建一个基本的以及用Master服务器/客户端,UDP服务器/客户端增强的网络应用程序来进行网络连接。在这个教程里我们将使用Unity iPhone 1.6, iPhone 3GS和来自于官网的Startrooper的案例。
2。开始
在开始前我先要了解一些知识点:
用于基本的和网络强化的组件。
创建一个服务器和客户端
使用Master Server
使用直接连接的方法
使用UDP广播服务器
在网络上创建一个简单的场景
把StartTrooper游戏变成多人在线游戏
使用其它的Unity组件以及更多其它功能
你应该有:
Unity iPhone 1.6.
iPhone或者iPod
有关网络和他们如何工作方面的知识。
高级的C#和JavaScript应用能力。
基本的Unity的使用技巧,你可以从这儿找到更多的信息
注意事项:
Untiy支持.NET 1.1和2.1
你可以通过:Edit->Project Settings->Player->Enable Unity Networking 来开启或关闭网络功能。
Unity网络支持wifi,3G和GSM链接。
你能连接两个不同的Unity平台,例如,你可以从Unity桌面链接到Unity iPhone,从Unity Web播放器链接到Unity iPhone,等等。
3。创建你的第一个用户/服务器程序
在这个章节里,我们将创建包括一个简单的多人在线的基础应用程序,在网络上创建第一个移动物体和基本的客户端和服务器端的交互的例子。这个例子我们使用了似有的多人在线组件。例如:Network和NetworkView.我们将用直接连接的方法连接客户端和服务器。
3。1。准备场景
现在让我们从一个简单的场景开始,你的第一步:
创建一个新工程。
创建一个新的预制物体然后给它命名:Player
创建一个新的方块物体
把你的Hierarchy面板的方块物体拖到预制物体上,然后从场景中删除这个方块。
创建一个新的Plane然后给它命名:Ground。并设置:Position(0,0,0),Rotation(0,0,0),Scale(5,5,5).
创建一个平行灯光,并设置:Position(0,15,0),Rotation(25,0,0),Scale(1,1,1),Shadows->Type->Soft Shadows
最后,保存你的场景并命名:MainGame.
现在看起来应该像这样子:
3。2。创建脚本并添加脚本
接下来你将学习如何创建服务器和客户端,在网络中实例化场景和物体,移动物体以及把所有的对象连接起来。
3。2。1。服务器和客户端
我们将开始最重要的部分——服务器和客户端的创建。
创建一个新的JavaScript文件然后命名它为ConnectionGUI
把这个脚本文件拖给层级面板里的Main Camera(主摄像机),然后打开这个文件创建一些变量:
var remoteIP="127.0.0.1";
var remotePort=25000;
var listenPort=25000;
var useNAT=false;
var yourIP="";
var yourPort="";
现在我们将用Unity GUI为服务器创建一个接口然后连接它。
function OnGUI()
{
//检查你是否连接到服务器。
if(Network.peerType==NetworkPeerType.Disconnected)
{
if(GUI.Button(new Rect(10,50,100,30),"Connect"))
{
Network.useNat=useNAT;
//连接到服务器
Network.Connect(remoteIP,remotePort);
}
if(GUI.Button(new Rect(10,50,100,30),"Start Server"))
{
Network.usenat=useNAT;
//创建服务器
Network.InitializeServer(32,listenPort);
//通知我们场景中的物体network已经准备好了。
for(var go:GameObject in FindObjectsOfType(GameObject))
{
go.SendMessage("OnNetworkLoadedLevel",SendMessageOptions.DontRequireReceiver);
}
}
//添加IP地址和端口号
remoteIP=GUI.TextField(new Rect(120,10,100,20),remoteIP);
remotePort=parseInt(GUI.TextField(new Rect(230,10,40,20),remotePort.ToString()));
}
else
{
//得到你的IP地址和端口
ipaddress=Network.player.ipAddress;
port=Network.player.port.ToString();
GUI.Label(new Rect(140,20,250,40),"IP Address:"+ipaddress+":"+port);
if(GUI.Button(new Rect(10,10,100,50),"Disconnect"))
{
//从服务器上断开连接
Network.Disconnect(200);
}
}
}
注意下面的函数,这个函数当有人连接成功后时被调用。同时通知场景中所有对象网络已经准备好了。
function OnConnectedToServer()
{
//通知场景中的物体网络已经准备好了
for(var go:GameObject in FindObjectsOfType(GameObject))
{
go.SendMessage("OnNetworkLoadedLevel",SendMessageOptions.DontRequireReceiver);
}
}
在Play模式你的屏幕应该是这样的:
现在可以测试你的服务器和用户端了。设置:Player Settings(Edit->Project Settings->Player)去设置你的iPhone Bundle Identifier并且切换默认的Screen Orientation为Landscape Right。编译你的工程到iPhone并且在编辑器中创建一个服务器。尝试用在服务器屏幕中找到的IP地址连接到服务器。如果一切正常的话。你将能看到“Disconnect”按钮然后你的IP地址同时出现在两屏幕上。注意两个应用程序必须在相同的网络上。
3。2。2。在网络上实例化场景和物体。
现在我们需要在Player(Cube)然后[写代码去实例化它:
选择Player预制物体然后添加一个NetworkView:
改变组件的同步状态Synchronization参数为Reliable Delta Compressed.这是需要你给所有的用户显示同步。
添加一个Rigidbody给你Player预制。
现在在网络上实例化你的Player和物体:
创建一个新的空的GameObjecj然后命名为:Spawn.Object参数:Position(0,5,0),Rotation(0,0,0),Scale(1,1,1)。
创建一个新的JavaScript文件然后命名Instantiate
打开文件写下以下代码:
var SpaceCraft:Transform;
function OnNetworkLoadedLevel()
{
//当网络加载后实例化SpaceCraft。
Network.Instantiate(SpaceCraft,transform.position,transform.rotation,0);
}
function OnPlayerDisconnected(player:NetworkPlayer)
{
Network.RemoveRPCs(player,0);
Network.DestroyPlayerObjects(player);
}
把这个脚本拖放到Spawn物体上。
选择Spawn物体然后拖放到Player参数到"Player(Transform)"变量上。
现在测试游戏,你将会看到服务器和每一个连接到的用户将会有他们的Player(Cube)。我们来创建一个简单的测试:
创建一个新的JavaScript文件并命名Control.
添加这段代码
function OnGUI()
{
if(GUI.Button(new Rect(20,100,50,50),"up"))
{
GameObject.Find("Player(Clone)").transform.position=new Vector3(0,5,0);
}
}
把这个文件拖放到Spawn物体上
编译这个工程,创建一个服务器并且连接它。现在你可以看到每一个客户端有发球自己的Player(Cube)。
你的编辑屏幕应该看起来像这样:
小结
恭喜你!你已经学会了创建了一个多人程序所必要的基础知识。
为多人游戏准备一个基本的场景
创建一个服务器。
创建一个客户端。
使用直接连接。
使用基本的network组件。
在newwork上实例化一个场景和物体。
把这些都连接在一起。
Download the whole NetworkExampla project:
http://asprofas.xz.lt/mp_documentation/images/NetworkExsample.zip
下一个章节,我们将在包括及创建一个高级的网络设置的思路上进行拓展。
4。利用Startrooper这个游戏执行多人在线的创建。
在这个章节里,你将学会如何把StarTrooper游戏由单人变成多人游戏。我们将用复杂的组件和三种不同的连接方式:直接连接(在第四章),MasterServer连接和UDP广播连接。在最后这个章节里你将在多人模式下有能力去环绕飞行并且击败其它的用。
从Unity官方网站下载StarTrooper并且弄明白它:
http://asprofas.xz.lt/mp_documentation/images/mpStarTrooper.zip
4。1。改变场景
我们将要把这个单人游戏修改一下,后面会进一步深入修改。
打开下载好的StarTrooper工程文件。
选择StarTrooper场景
在Hierarchy面板里选择SpaceCraftFBX文件。
添加一个NetworkView组件。
从Player上移除控制脚本。
从对象移除Missile Launcher脚本。
给对象上添加一个尾迹渲染器。
设置尾迹渲染器Materials->Element 0->missleTracer
创建一个新的空物体并命名Spawn.设置Transform参数:Position(0,30,11),Rotation(0,0,0),Scale(1,1,1).
4。2。整合
现在我们将要改变并且整合网络到这个工程的。让我们为网络准备这个场景。完成后我们将创建一个服务器和客户端。
4。2。1。整合物体和场景
现在我们需要创建一个脚本为的是在网络中转换我们的刚体:
首先,这我们的新脚本创建两个文件夹分别是:“NetworkFiles”和“Plugins”这个是存入C#文件的。
创建一个C#文件并命名它为NetworkRigidbody.
把NetworkRigidbody.cs文件拖放到Plugins文件夹。
别管这个文件是怎么回事,只要明白要需要它的地方就能用得上就可以了。
你可以把这个脚本用在任何你想要的工程中,因为它适合所有的刚体对象。
打开你的NetworkRigidbody.cs文件并输入以下代码:
using UnityEngine;
using System.Collections;
public class NetworkRigidbody : MonoBehaviour
{
public double m_InterpolationBackTime = 0.1;
public double m_ExtrapolationLimit = 0.5;
internal struct State
{
internal double timestamp;
internal Vector3 pos;
internal Vector3 velocity;
internal Quaternion rot;
internal Vector3 angularVelocity;
}
//我们储存20个状态用“playback”信息
State[] m_BufferedState = new State[20]; //与用到的slots(插槽)保持联系。
int m_TimestampCount;
void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)
{
// 发送数据给服务器
if (stream.isWriting)
{
Vector3 pos = rigidbody.position;
Quaternion rot = rigidbody.rotation;
Vector3 velocity = rigidbody.velocity;
Vector3 angularVelocity = rigidbody.angularVelocity;
stream.Serialize(ref pos);
stream.Serialize(ref velocity);
stream.Serialize(ref rot);
stream.Serialize(ref angularVelocity);
}
// 从远程客户端读取数据。
else
{
Vector3 pos = Vector3.zero;
Vector3 velocity = Vector3.zero;
Quaternion rot = Quaternion.identity;
Vector3 angularVelocity = Vector3.zero;
stream.Serialize(ref pos);
stream.Serialize(ref velocity);
stream.Serialize(ref rot);
stream.Serialize(ref angularVelocity);
//切换buffer另一边,删除状态20
for (int i=m_Buffered State.Length-1;i>=1;i--)
{
m_BufferedState[i] = m_BufferedState[i-1];
}
// 在slot 0记录当前状态
State state;
state.timestamp = info.timestamp;
state.pos = pos;
state.velocity = velocity;
state.rot = rot;
state.angularVelocity = angularVelocity;
m_BufferedState[0] = state;
//更新用slot计数,然后永远不超出buffer的大小。
// Slot不是真正的释放,这仅仅是确认buffer是填满了并且未初始化的slot是否
//没有用
m_TimestampCount = Mathf.Min(m_TimestampCount + 1, m_BufferedState.Length);
// 检查状态是否正常,如果这个是不协调的你可以重新调整或
//者丢掉非正常的状态。这里没有做什么。
for (int i=0;i<m_TimestampCount-1;i++)
{
if (m_BufferedState[i].timestamp < m_BufferedState[i+1].timestamp)
Debug.Log("State inconsistent");
}
}
}
// 我们有一个interpolationBackTime的窗口在我们本质上播放
// 通过有interpolationBackTime普通的ping,你将通常用来填写。
// 并且仅仅没有更过的数据到达我们将用的额外polation(位置?)。
void Update ()
{
//这个是刚体的目标playback time。
double interpolationTime = Network.time - m_InterpolationBackTime;
// 使用interpolation如果目标playback time是呈现在buffer里面
if (m_BufferedState[0].timestamp > interpolationTime)
{
//检查buffer并且查找正确的状态去play back。
for (int i=0;i<m_TimestampCount;i++)
{
if (m_BufferedState[i].timestamp <= interpolationTime || i == m_TimestampCount-1)
{
//一个slot的状态比最好的playback状态更新(<100ms)
State rhs = m_BufferedState[Mathf.Max(i-1, 0)];
// The best playback state (closest to 100 ms old (default time))
// 最好的playback状态(最接近100ms时长(默认时间))
State lhs = m_BufferedState[i];
// 用两个slots的时间去测定插值是否是必要的。
double length = rhs.timestamp - lhs.timestamp;
float t = 0.0F;
// 时间差越接近于100毫秒t越接近于1,在这种情况下只用rhs。
//例如:
// Time是10.000,所以sampleTime是9.900
// lhs.time是9.910 rhs.time是9.980length是0.070
// t是9.900 - 9.910 / 0.070 = 0.14。所以它用了rhs的14%,lhs的86%
if (length > 0.0001)
t = (float)((interpolationTime - lhs.timestamp) / length);
// 如果 t=0 =>直接使用lhs
transform.localPosition = Vector3.Lerp(lhs.pos, rhs.pos, t);
transform.localRotation = Quaternion.Slerp(lhs.rot, rhs.rot, t);
return;
}
}
}
//使用插值
else
{
State latest = m_BufferedState[0];
float extrapolationLength = (float)(interpolationTime - latest.timestamp);
// 不为多于500毫秒的插值,你需要仔细的做。
if (extrapolationLength < m_ExtrapolationLimit)
{
float axisLength = extrapolationLength * latest.angularVelocity.magnitude * Mathf.Rad2Deg; Quaternion angularRotation = Quaternion.AngleAxis(axisLength, latest.angularVelocity);
rigidbody.position = latest.pos + latest.velocity * extrapolationLength;
rigidbody.rotation = angularRotation * latest.rot; rigidbody.velocity = latest.velocity;
rigidbody.angularVelocity = latest.angularVelocity;
}
}
}
}
在Hierarchy选择你的SpaceCraftFBX物体(记住不在Project面板里的那个游戏对象)
添加NetworkRigidboddy.CS脚本到你的选择的物体上。
关闭NetworkRigidbody组件(仅仅是在组件上打上个标记)。我们将在以后用一些条件下激活它。
在Network View组件中:改变Observed把SpaceCraftFBX(Transform)换成SpaceCraftFBX(NetworkRigidbody)参数.
创建一个新的JavaScript文件并命名为RigidAssign
把RigidAssign.js脚本添加到你的物体上然后编辑这个文件:
function OnNetworkInstantiate (msg : NetworkMessageInfo)
{
if (networkView.isMine)
{
var _NetworkRigidbody : NetworkRigidbody = GetComponent("NetworkRigidbody");
_NetworkRigidbody.enabled = false;
}
else
{
name += "Remote";
var _NetworkRigidbody2 :NetworkRigidbody = GetComponent("NetworkRigidbody");
_NetworkRigidbody2.enabled = true;
}
}
创建一个新的预制物体,给它命名:SpaceCraft。然后把它放到预制文件夹里。
把你的SpaceCraftFBX从Hierarchy面板拖到Project面板里新的预制物体(SpaceCraft)。
从场景中删除SpaceCraftFBX文件。
为你的SpaceCraft预制物体创建“SpaceCraft”标签(tag)
再选中你的SpaceCraft预置物体把标签指定给它:点击"Untagged"并且选择"SpaceCraft"
现在看像下面这个样子
现在为Network准备了SpaceCraft!
创建一个新的JS文件并且命名Instantialte.js
把这个脚本文件拖给Hierarchy面板里的Spawn物体,然后编写以下代码:
var SpaceCraft : Transform;
function OnNetworkLoadedLevel ()
{
// Instantiating SpaceCraft when Network is loaded
// 实例化SpaceCraft当网络加载完成的时候
Network.Instantiate(SpaceCraft, transform.position, transform.rotation, 0);
}
function OnPlayerDisconnected (player : NetworkPlayer)
{
// Removing player if Network is disconnected
// 移除player,当网络断开连接。
Debug.Log("Server destroying player");
Network.RemoveRPCs(player, 0);
Network.DestroyPlayerObjects(player);
}
选择Spawn物体在Hierarchy并且设置Space Craft参数给SpaceCraft(Transform)(从列表选择你的SpaceCraft预设)
从Hierachy面板里选择Main Camera物体。
打开Smooth Follow脚本并且稍作修改:
var target : Transform;
var distance : float = 10.0;
var height : float = 5.0;
var heightDamping : float = 2.0;
var rotationDamping : float = 3.0;
function LateUpdate ()
{
if(GameObject.FindWithTag("SpaceCraft"))
{
if (!target)
target = GameObject.FindWithTag("SpaceCraft").transform;
// 计算当前的选择角度
var wantedRotationAngle : float = target.eulerAngles.y;
var wantedHeight : float = target.position.y + height;
var currentRotationAngle : float = transform.eulerAngles.y;
var currentHeight : float = transform.position.y;
// 减幅y轴方向的旋转。
var dt : float = Time.deltaTime;
currentRotationAngle = Mathf.LerpAngle (currentRotationAngle, wantedRotationAngle, rotationDamping * dt);
//减幅高度
currentHeight = Mathf.Lerp (currentHeight, wantedHeight, heightDamping * dt);
//转换角度变成旋转
var currentRotation : Quaternion = Quaternion.Euler (0, currentRotationAngle, 0);
// 设置cmera的位置在x-z平面成:
// 与后面的target的距离多少米。
transform.position = target.position;
var pos : Vector3 = target.position - currentRotation * Vector3.forward * distance;
pos.y = currentHeight;
// 设置摄像机的高度
transform.position = pos;
// 总是朝向目标
transform.LookAt (target);
}
}
把Player Controls脚本拖到Main Camera上,然后编辑。
把代码修改成这样的:
var turnSpeed : float = 3.0;
var maxTurnLean : float = 70.0;
var maxTilt : float = 50.0;
var sensitivity : float = 0.5;
var forwardForce : float = 5.0;
var guiSpeedElement : Transform;
var craft : GameObject;
private var normalizedSpeed : float = 0.2;
private var euler : Vector3 = Vector3.zero;
var horizontalOrientation : boolean = true;
function Awake ()
{
if (horizontalOrientation)
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.LandscapeLeft;
}
else
{
iPhoneSettings.screenOrientation = iPhoneScreenOrientation.Portrait;
}
guiSpeedElement = GameObject.Find("speed").transform;
guiSpeedElement.position = new Vector3 (0, normalizedSpeed, 0);
}
function FixedUpdate ()
{
if(GameObject.FindWithTag("SpaceCraft"))
{
GameObject.FindWithTag("SpaceCraft").rigidbody.AddRelativeForce(0, 0, normalizedSpeed * (forwardForce*3));
var accelerator : Vector3 = iPhoneInput.acceleration;
if (horizontalOrientation)
{
var t : float = accelerator.x;
accelerator.x = -accelerator.y;
accelerator.y = t;
}
// 基于重力感应旋转
euler.y += accelerator.x * turnSpeed;
// Since we set absolute lean position, do some extra smoothing on it
euler.z = Mathf.Lerp(euler.z, -accelerator.x * maxTurnLean, 0.2);
//由于我们的设置绝对依赖position,所以做了一些额外的平滑。
euler.x = Mathf.Lerp(euler.x, accelerator.y * maxTilt, 0.2);
// 应用旋转并且应用一些平滑
var rot : Quaternion = Quaternion.Euler(euler);
GameObject.FindWithTag("SpaceCraft").transform.rotation = Quaternion.Lerp (transform.rotation, rot, sensitivity);
}
}
function Update ()
{
for (var evt : iPhoneTouch in iPhoneInput.touches)
{
if (evt.phase == iPhoneTouchPhase.Moved)
{
normalizedSpeed = evt.position.y / Screen.height;
guiSpeedElement.position = new Vector3 (0, normalizedSpeed, 0);
}
}
}
添加脚本Missile launcher脚本给Main Camera。设置Missile(导弹)参数为MissilePrefab。
打开Missile Launcher然后编辑。
我们添加一个计时器给shooting(射击)同时也作一些小小的调整。
var missile : GameObject;
var timer : int = 0;
function FixedUpdate()
{
timer++;
}
function Update ()
{
if ((Input.GetMouseButtonDown (0))&&(timer>10))
{
// 如果SpaceCraft 存在的话
if(GameObject.FindWithTag("SpaceCraft"))
{
var position : Vector3 = new Vector3(0, -0.2, 1) * 10.0;
position = GameObject.FindWithTag("SpaceCraft").transform.TransformPoint(position);
//实例化
var thisMissile : GameObject = Network.Instantiate (missile, position, GameObject.FindWithTag("SpaceCraft").transform.rotation,0) as GameObject;
Physics.IgnoreCollision(thisMissile.collider, GameObject.FindWithTag("SpaceCraft").collider);
timer = 0;
}
}
}
创建新的标签“Missile”然后分配给missilePrefab预制物体。
选择missilePrefab物体并且打开MissileTrajector脚本来编辑。
我们将使它有能力击败其它的SpaceCraft
var explosion : GameObject;
function OnCollisionEnter(collision : Collision)
{
if(GameObject.FindWithTag("SpaceCraft"))
{
if(((collision.gameObject.tag == "Untagged")||(collision.gameObject.tag == "SpaceCraft"))&&(collision.gameObject.tag != "Missile"))
{
var contact : ContactPoint = collision.contacts[0];
Instantiate (explosion, contact.point + (contact.normal * 5.0) , Quaternion.identity);
if (collision.gameObject.tag == "SpaceCraft")
{
Instantiate (explosion, contact.point + (contact.normal * 5.0) , camera.main.transform.rotation);
collision.gameObject.transform.position = GameObject.Find("Spawn").transform.position;
}
Destroy (gameObject);
}
}
}
function FixedUpdate ()
{
if(GameObject.FindWithTag("Missile"))
{
rigidbody.AddForce (transform.TransformDirection (Vector3.forward + Vector3(0,0.1,0)) * 720.0);
}
}
你已经为网络准备好场景和物体了。
4。2。2。服务器/客户端。
现在是时候给这个游戏创建服务器了。我们将要创建三个不同的类型的servers(服务器)。
创建一个新场景并保存为服务器。这个场景被用作服务器。
创建一个新场景并保存为UDPServer。这个场景为用作UDP广播连接。
创建一个新场景保存为MasterServer。这个场景被用作MasterServer。
再创建一个新场景,保存为一个空场景。这个场景用作游戏断开时或启动一个新游戏之前清除掉所有的东西。
把所有新建的场景到File->Building Settings->Add Open Scene. 你的ServerChoose应该在列表里是第一个例子。如:
提示:你可以创建一个文件夹把这些都放到一起。
打开ServerChoose场景
创建一个新的JS文件并命名它为Menu。我们将用这个场景及这个脚本来选择不同类型的服务器。
把这个脚本添加给主摄像机(Main Camera)。
function OnGUI()
{
GUI.Label(new Rect((Screen.width/2)-80,(Screen.height/2)-130,200,50),"SELECT CONNECTION TYPE");
GUI.Label(new Rect((Screen.width-220),(Screen.height-30),220,30),"STAR-TROOPER MULTIPLAYER DEMO");
if(GUI.Button(new Rect((Screen.width/2)-100,(Screen.height/ 2)-100,200,50),"Master Server Connection"))
{
Application.LoadLevel("MasterServer");
}
if(GUI.Button(new Rect((Screen.width/2)-100,(Screen.height/2)-40,200,50),"Direct Connection"))
{
Application.LoadLevel("StarTrooper");
}
if(GUI.Button(new Rect((Screen.width/2)-100,(Screen.height/2)+20,200,50),"UDP Connection"))
{
Application.LoadLevel("UDPServer");
}
}
打开MasterServer场景
创建一个新的JS文件并命名它NetworkLevelLoad。我们将用这个脚本把StarTrooper场景和物体加载网络中来。
创建一个新的Empty GameObject并命名它ConnectionGUI。
把NetworkLevelLoad脚本添加到ConnectionGUI游戏物体上
private var lastLevelPrefix = 0;
function Awake ()
{
// 网络场景加载完成在一个分离的通道
DontDestroyOnLoad(this);
networkView.group = 1;
Application.LoadLevel("EmptyScene");
}
function OnGUI ()
{
//当network运行(服务器和客户端)然后显示场景“StarTrooper”
if (Network.peerType != NetworkPeerType.Disconnected)
{
if (GUI.Button(new Rect(350,10,100,30),"StarTrooper"))
{
//确信没有旧的RPC调用是在缓冲的然后发送加载的场景命令
Network.RemoveRPCsInGroup(0);
Network.RemoveRPCsInGroup(1);
//加载场景用增加后的level prefix(为查看ID)
networkView.RPC( "LoadLevel", RPCMode.AllBuffered, "StarTrooper", lastLevelPrefix + 1);
}
}
}
@RPC
function LoadLevel (level : String, levelPrefix : int)
{
Debug.Log("Loading level " + level + " with prefix " + levelPrefix); lastLevelPrefix = levelPrefix;
// 我们在网络上没有理由在默认的通道去发送更多的数据,因为我们将要加载场景,所有的这个物体将被删除。
Network.SetSendingEnabled(0, false);
//我们需要停止接受因为首先场景必须被加载。
//一旦场景加载完毕,RPC和其他状态的更新依附到场景中物体被允许去发射。
Network.isMessageQueueRunning = false;
// 所有的newtork views从一个场景加载将得到一个prefix在他们NetworkViewID中。
//这个将预防旧的来自于客户端的更新泄露进一个新创建的场景
Network.SetLevelPrefix(levelPrefix);
Application.LoadLevel(level); yield; yield;
// Allow receiving data again允许再一次加载数据
Network.isMessageQueueRunning = true;
//现在场景已经加载完毕然后我们可以开始发送我们的数据。
Network.SetSendingEnabled(0, true);
//通知我们的物体场景和网络已经准备好了。
var go : Transform[] = FindObjectsOfType(Transform);
var go_len = go.length;
for (var i=0;i<go_len;i++)
{
go[i].SendMessage("OnNetworkLoadedLevel",SendMessageOptions.DontRequireReceiver);
}
}
function OnDisconnectedFromServer ()
{
Application.LoadLevel("EmptyScene");
}
@script RequireComponent(NetworkView)
// Ensure that this script has added a Netw
确信这个脚本已经自动添加了NetworkView,另外从Component菜单添加这个脚本workLevelLoad.
创建一个新的JavaScript文件并且命名MasterServerGUI。用这个脚本我们将创建一个Master Server/Client,一些GUI将用到它。
把这个文件添加给ConnectionGUI物体并且打开这个脚本去编辑:
DontDestroyOnLoad(this);
var gameName = "YourGameName";
var serverPort = 25002;
private var timeoutHostList = 0.0;
private var lastHostListRequest = -1000.0;
private var hostListRefreshTimeout = 10.0;
private var natCapable : ConnectionTesterStatus = ConnectionTesterStatus.Undetermined;
private var filterNATHosts = false;
private var probingPublicIP = false;
private var doneTesting = false;
private var timer : float = 0.0;
private var windowRect = Rect (Screen.width-300,0,300,100);
private var hideTest = false;
private var testMessage = "Undetermined NAT capabilities";
// 如果没有运行客户端启用这个在服务器的机器上。
//MasterServer.dedicatedServer = true;
function OnFailedToConnectToMasterServer(info: NetworkConnectionError)
{
Debug.Log(info);
}
function OnFailedToConnect(info: NetworkConnectionError)
{
Debug.Log(info);
}
function OnGUI ()
{
ShowGUI();
}
function Awake ()
{
// 开始连接测试
natCapable = Network.TestConnection();
// 这个机器有什么种类的IP?TestConnection也在测试结果中显示。
if (Network.HavePublicAddress())
Debug.Log("This machine has a public IP address");
else
Debug.Log("This machine has a private IP address");
}
function Update()
{
// 如果测试不确定,保持运行
if (!doneTesting)
{
TestConnection();
}
}
function TestConnection()
{
// 开始/轮询连接测试,在一个标签里面报告结果并且对结果做出相应的反应。
natCapable = Network.TestConnection();
switch (natCapable)
{
case
ConnectionTesterStatus.Error:
testMessage = "Problem determining NAT capabilities";
doneTesting = true;
break;
case
ConnectionTesterStatus.Undetermined:
testMessage = "Undetermined NAT capabilities";
doneTesting = false;
break;
case
ConnectionTesterStatus.PrivateIPNoNATPunchthrough:
testMessage = "Cannot do NAT punchthrough, filtering NAT enabled hosts for client connections," +" local LAN games only.";
filterNATHosts = true; Network.useNat = true;
doneTesting = true;
break;
case
ConnectionTesterStatus.PrivateIPHasNATPunchThrough:
if (probingPublicIP)
testMessage = "Non-connectable public IP address (port "+ serverPort +" blocked),"+" NAT punchthrough can circumvent the firewall.";
else
testMessage = "NAT punchthrough capable. Enabling NAT punchthrough functionality.";
// NAT功能在服务器开始的时候是可用的,如果主机需要它在此基础上客户应该启用它, Network.useNat = true; doneTesting = true;
break;
case
ConnectionTesterStatus.PublicIPIsConnectable:
testMessage = "Directly connectable public IP address.";
Network.useNat = false; doneTesting = true; break;
//这种情况是比较特殊,因为我们现在需要检查是否可以通过使用NAT穿通 case
ConnectionTesterStatus.PublicIPPortBlocked:
testMessage = "Non-connectble public IP address (port " + serverPort +" blocked),"+" running a server is impossible.";
Network.useNat = false;
// 如果没有NAT穿透,测试将被搭建在这个公共的IP,强制一个测试
if (!probingPublicIP)
{
Debug.Log("Testing if firewall can be circumnvented");
natCapable = Network.TestConnectionNAT();
probingPublicIP = true; timer = Time.time + 10;
}
//NAT穿透测试被执行但是我们仍然是阻塞的。
else if (Time.time > timer)
{
probingPublicIP = false;
// reset
Network.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;
default: testMessage = "Error in test routine, got " + natCapable;
}
}
function ShowGUI()
{
if (GUI.Button (new Rect(100,10,120,30),"Retest connection"))
{
Debug.Log("Redoing connection test");
probingPublicIP = false; doneTesting = false;
natCapable = Network.TestConnection(true);
}
if (Network.peerType == NetworkPeerType.Disconnected)
{
// 开始一个新的服务器
if (GUI.Button(new Rect(10,10,90,30),"Start Server"))
{
Network.InitializeServer(32, serverPort); MasterServer.updateRate = 3;
MasterServer.RegisterHost(gameName, "stuff", "profas chat test");
}
// 刷新主机
if (GUI.Button(new Rect(10,40,210,30),"Refresh available Servers") || Time.realtimeSinceStartup > lastHostListRequest + hostListRefreshTimeout)
{
MasterServer.ClearHostList(); MasterServer.RequestHostList (gameName);
lastHostListRequest = Time.realtimeSinceStartup; Debug.Log("Refresh Click");
}
var data : HostData[] = MasterServer.PollHostList();
var _cnt : int = 0;
for (var element in data)
{
// Do not display NAT enabled games if we cannot do NAT punchthrough
//不显示NAT功能的游戏,如果我们不能这样做NAT穿透
if ( !(filterNATHosts && element.useNat) )
{
var name = element.gameName + " " + element.connectedPlayers + " / " + element.playerLimit;
var hostInfo; hostInfo = "[";
// Here we display all IP addresses, there can be multiple in cases where
// internal LAN connections are being attempted. In the GUI we could just display connect to the
// the first one in order not confuse the end user, but internally Unity will
// do a connection check on all IP addresses in the element.ip list, and
// first valid one.
//在这个这里我们显示所有IP地址,可以有不同情况内部局域网连接被尝试。在GUI里,我们可以仅仅显示第一个为了不混淆终端用户,但是内部Unity将做一个连接检查在所有的IP地址上在element.ip列表里,并且连接到第一个有效的ip。
for (var host in element.ip)
{
hostInfo = hostInfo + host + ":" + element.port + " ";
} hostInfo = hostInfo + "]";
if (GUI.Button(new Rect(20,(_cnt*50)+90,400,40),hostInfo.ToString()))
{
// Enable NAT functionality based on what the hosts if configured to do
//主机是否配置决定是否启用NAT功能。
Network.useNat = element.useNat;
if (Network.useNat)
print("Using Nat punchthrough to connect");
else
print("Connecting directly to host");
Network.Connect(element.ip, element.port);
}
}
}
}
else
{
if (GUI.Button (new Rect(10,10,90,30),"Disconnect"))
{
Network.Disconnect();
MasterServer.UnregisterHost();
}
}
}
现在你可以用Master Server来测试你的工程了。
去Edit -> Project Settings -> Player然后设置你的iPhone Bundle Identifier并且改变Default Screen Orientation 成 Landscape Left.
编译你的工程到iPhone。
运行Server场景在编辑器。
点击在编辑器里面Master Server Connection和iPhone上。
点击Start Server在编辑器。
服务器应该应用在iPhone上(如果没有,点击刷新可用服务器)。
连接到服务器从iPhone。
点击StarTrooper 按钮在编辑器或者在iPhone上。
你的图片应该看起来像这张图片:
我们已经完成了我们的Master Server
现在我们需要去创建一个第二种连接方式 – UDP广播连接:
打开UDPServer
创建一个新的C#文件并且命名他UDPConnectionGUI.
把UDPConnection文件移到Plugins文件夹。
创建一个新的空物体并且命名它UDPServer.
指定UDPConnectionGUI文件到UDPServer 物体。(或者在你写了接下来的代码以后分配他)。
分配一个NetworkView组件到UDPServer物体并且改变Observed参数给UDPServer(Transform)。
分配NetworkLevelLoad脚本文件给UDPServer物体。
打开UDPCOnnectionGUI然后写以下代码:
using UnityEngine;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Threading;
public class UDPConnectionGUI : MonoBehaviour
{
private UdpClient server;
private UdpClient client;
private IPEndPoint receivePoint;
private string port = "6767";
private int listenPort = 25001;
private string ip = "0.0.0.0";
private string ip_broadcast = "255.255.255.255";
private bool youServer = false;
private bool connected = false;
private string server_name = "";
private int clear_list = 0;
public void Update()
{
if(clear_list++>200)
{
server_name = "";
clear_list = 0;
}
}
public void Start()
{
Debug.Log("Start"); LoadClient();
}
public void LoadClient()
{
client = new UdpClient(System.Convert.ToInt32(port));
receivePoint = new IPEndPoint(IPAddress.Parse(ip),System.Convert.ToInt32(port));
Thread startClient = new Thread(new ThreadStart(start_client));
startClient.Start();
}
public void start_client()
{
bool continueLoop =true;
try
{
while(continueLoop)
{
byte[] recData = client.Receive(ref receivePoint);
System.Text.ASCIIEncoding encode = new System.Text.ASCIIEncoding();
server_name = encode.GetString(recData); if(connected) { server_name = "";
client.Close();
break;
}
}
}
catch {}
}
public void start_server()
{
try
{
while(true)
{
System.Text.ASCIIEncoding encode = new System.Text.ASCIIEncoding();
byte[] sendData = encode.GetBytes(Network.player.ipAddress.ToString());
server.Send(sendData,sendData.Length,ip_broadcast,System.Convert.ToInt32(port));
Thread.Sleep(100);
}
}
catch {}
}
void OnGUI()
{
if(!youServer)
{
if(GUI.Button(new Rect(10,10,100,30),"Start Server"))
{
youServer = true;
Network.InitializeServer(32, listenPort);
string ipaddress = Network.player.ipAddress.ToString();
ip = ipaddress;
client.Close();
server = new UdpClient(System.Convert.ToInt32(port));
receivePoint = new IPEndPoint(IPAddress.Parse(ipaddress),System.Convert.ToInt32(port));
Thread startServer = new Thread(new ThreadStart(start_server));
startServer.Start();
}
if(server_name!="")
{
if(GUI.Button(new Rect(20,100,200,50),server_name))
{
connected = true; Network.Connect(server_name, listenPort);
}
}
}
else
{
if(GUI.Button(new Rect(10,10,100,30),"Disconnect"))
{
Network.Disconnect();
youServer = false; server.Close();
LoadClient();
}
}
}
}
现在你可以测试这个连接类型:
按上面的要求设置你的Player Settings(如果你刚刚没有改变它们的话)。
把工程编译到iPhone。
在编辑器播放UDPServer场景。
从编辑器里面点击Start Server开启服务器。
当IP address 按钮显示的时候从iPhone连接到服务器。
你的场景应该看起来像这样:
我们已经完成我们的UDP广播服务器
现在我们需要去创建一个第三种连接方式 – 直接连接:
打开StarTrooper场景
创建一个新的JavaScript文件并且命名它ConnectionGUI.
指定ConnectionGUI.js文件给Main Camera并且添写以下代码:
var remoteIP = "127.0.0.1";
var remotePort = 25000;
var listenPort = 25000;
var useNAT = false; 、
var yourIP = "";
var yourPort = "";
function Awake()
{
if (FindObjectOfType(MasterServerGUI))
this.enabled = false;
if(FindObjectOfType(UDPConnectionGUI))
this.enabled = false;
}
function OnGUI ()
{
if (Network.peerType == NetworkPeerType.Disconnected)
{
// If not connected如果没有连接
if (GUI.Button (new Rect(10,10,100,30),"Connect"))
{
Network.useNat = useNAT;
// Connecting to the server连接服务器
Network.Connect(remoteIP, remotePort);
}
if (GUI.Button (new Rect(10,50,100,30),"Start Server"))
{
Network.useNat = useNAT;
// Creating server创建服务器
Network.InitializeServer(32, listenPort);
// Notify our objects that the level and the network is ready
//通知我们场景里物体,network(网络)准备好了
for (var go : GameObject in FindObjectsOfType(GameObject))
{
go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
}
}
remoteIP = GUI.TextField(new Rect(120,10,100,20),remoteIP);
remotePort = parseInt(GUI.TextField(new Rect(230,10,40,20),remotePort.ToString()));
}
else
{
// If connected如果连接了
// Getting your ip address and port获得你的ip地址和端口
ipaddress = Network.player.ipAddress;
port = Network.player.port.ToString();
GUI.Label(new Rect(140,20,250,40),"IP Adress: "+ipaddress+":"+port);
if (GUI.Button (new Rect(10,10,100,50),"Disconnect"))
{
// Disconnect from the server从服务器断开连接
Network.Disconnect(200);
}
}
function OnConnectedToServer()
{
// Notify our objects that the level and the network is ready
// 通知我们的物体场景和network准备好了
for (var go : GameObject in FindObjectsOfType(GameObject))
go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
}
function OnDisconnectedFromServer ()
{
if (this.enabled != false)
Application.LoadLevel(Application.loadedLevel);
else
{
var _NetworkLevelLoad : NetworkLevelLoad = FindObjectOfType(NetworkLevelLoad);
_NetworkLevelLoad.OnDisconnectedFromServer();
}
}
现在可以测试这种连接方式了。
按上面的要求设置你的Player Settings(如果你刚刚没有改变他们)
把工程编译到iPhone
播放StarTrooper场景在编辑器。
在编辑器和在iPhone上点击Direct Connection。
从编辑器点击Start Server开启服务器。
在iPhone上IP框输入IP地址的。你可以从Editor Player场景得到IP地址。
你的场景应该看起来想这张图片。
我们已经完成我们的客户段和服务器通三种方式。。。。
4。3。3。最后润色。
我们的工程现在已经基本完成。为了使我们的游戏更具可玩性我们需要去添加一些最终的润色:
打开StarTrooper场景
选择SpaceCraft预设并且设置Transform Scale参数成:1.8,1.8,1.8.
选择Main Camera在Hierarchy然后改变公共 变量在Play Controls脚本:Turn Speed: 3, Max Turn Lean: 70, Max Tilt: 50, Sensitivity: 0.5, Forward Force: 20.
在Main Camera 里打开MissileLauncher.js脚本。在第16行,改变向量从Vector3(0,-0.2,1) 到Vector3(0,-0.3,0.5).
选择missilePrefab预设并且设置Transform Scale参数成:8,8,8。
你最后的屏幕应该看起来像这张图片。
提示:这些最新的修改在我的尝试中是基本的,所以可以随意的做出你的自己的修改和新的功能。
祝贺你!你已经完成了MultiPlayer MultiPlayer StarTrooper工程。。。。
你已经学习了如何去:
准备一个场景和物体为network.
变换一个场景和物体在network.
一个单人游戏转换成多人
创建一个Master Server/Client.
创建一个Server/Client用直接连接。
创建一个Server/Client用UDP广播连接。
用主要的Network组件。
用增加的Unity组件