Sonntag, 29. April 2012

This blog entry is for internal use only. I will make a detailed post as far as the game "can do" something. Dieser Blogeintrag ist für mich selbst...ich werde einen detaillierteren Beitrag machen, sobald das game auch was "tun" kann.

Step-by-Step-Anleitung: Nicht-Autoratives Third-Person Netzwerkspiel in Unity3d

Grundaufbau

Zuerst wird eine kleine Sandbox erstellt...

1. Mache einen Boden:
1.1 GameObject->Create Other->Plane
Position: 0,0,0
Size: 5,5,5

2. Mache einen Player (Prefab):
2.1 Assets->Create->Prefab und nenne es "Player", tagge es mit "Player"
2.2 Ziehe deinen Player in das Prefab (Capsule erstellen: GameObject->Create Other->Capsule)
2.3 Klicke auf das Player-Prefab und dann auf Component->Miscellaneous->Network View
2.4 Der Player sollte nun eine Network-View-Komponente haben. Stelle dort die "State Synchronization" auf "Reliable Data Compress"

Es muss KEIN Player in der Scene sein. Dieser wird dann über einen Spawn-Point erstellt.

3. Mache einen Spawn Point
3.1 GameObject->Create Empty und nenne es "Spawn", tagge es mit "Respawn"

4. (Noch kein Camera Code) Ziehe die Kamera in der Scene an eine Stelle, an der Sie möglichst die ganze Plane im Blickfeld hat. ACHTUNG: Mit dem hier mitgelieferten Input-Script für den Player kann er sich NICHT bewegen, wenn die Kamera von oben nach unten sieht. Sie sollte also vorerst mal irgendwo an der Seite stehen.

Nun erstellen wir ein paar Scripts und hängen sie an die jeweiligen Dingens an.
ACHTUNG: Die Scripts wurden meist aus irgendwelchen Tutorials rauskopiert und werden hier (noch) nicht näher erläutert. Dies dient nur einem minimalen Grundaufbau für ein Netzwerk-Spiel! Wenn nicht anders angegeben, sind es JavaScript-Scripts.

Das Input-Script

Damit der Player sich bewegen kann, muss er ein Input-Script haben. Erstelle dieses Script(Name: ThirdPersonController) und hänge es an das "Player"-Prefab an:

// The speed when walking
var walkSpeed = 5.0;
// after trotAfterSeconds of walking we trot with trotSpeed
var trotSpeed = 10.0;
// when pressing "Fire3" button (cmd) we start running
var runSpeed = 10.0;

var inAirControlAcceleration = 3.0;

// How high do we jump when pressing jump and letting go immediately
var jumpHeight = 0.5;
// We add extraJumpHeight meters on top when holding the button down longer while jumping
var extraJumpHeight = 2.5;

// The gravity for the character
var gravity = 20.0;
// The gravity in controlled descent mode
var controlledDescentGravity = 2.0;
var speedSmoothing = 10.0;
var rotateSpeed = 500.0;
var trotAfterSeconds = 3.0;

var canJump = true;
var canControlDescent = true;
var canWallJump = false;

private var jumpRepeatTime = 0.05;
private var wallJumpTimeout = 0.15;
private var jumpTimeout = 0.15;
private var groundedTimeout = 0.25;

// The camera doesnt start following the target immediately but waits for a split second to avoid too much waving around.
private var lockCameraTimer = 0.0;

// The current move direction in x-z
private var moveDirection = Vector3.zero;
// The current vertical speed
private var verticalSpeed = 0.0;
// The current x-z move speed
private var moveSpeed = 0.0;

// The last collision flags returned from controller.Move
private var collisionFlags : CollisionFlags;

// Are we jumping? (Initiated with jump button and not grounded yet)
private var jumping = false;
private var jumpingReachedApex = false;

// Are we moving backwards (This locks the camera to not do a 180 degree spin)
private var movingBack = false;
// Is the user pressing any keys?
private var isMoving = false;
// When did the user start walking (Used for going into trot after a while)
private var walkTimeStart = 0.0;
// Last time the jump button was clicked down
private var lastJumpButtonTime = -10.0;
// Last time we performed a jump
private var lastJumpTime = -1.0;
// Average normal of the last touched geometry
private var wallJumpContactNormal : Vector3;
private var wallJumpContactNormalHeight : float;

// the height we jumped from (Used to determine for how long to apply extra jump power after jumping.)
private var lastJumpStartHeight = 0.0;
// When did we touch the wall the first time during this jump (Used for wall jumping)
private var touchWallJumpTime = -1.0;

private var inAirVelocity = Vector3.zero;

private var lastGroundedTime = 0.0;

private var lean = 0.0;
private var slammed = false;

private var isControllable = true;

function Awake ()
{
    moveDirection = transform.TransformDirection(Vector3.forward);
}

// This next function responds to the "HidePlayer" message by hiding the player.
// The message is also 'replied to' by identically-named functions in the collision-handling scripts.
// - Used by the LevelStatus script when the level completed animation is triggered.

function HidePlayer()
{
    GameObject.Find("rootJoint").GetComponent(SkinnedMeshRenderer).enabled = false; // stop rendering the player.
    isControllable = false;    // disable player controls.
}

// This is a complementary function to the above. We don't use it in the tutorial, but it's included for
// the sake of completeness. (I like orthogonal APIs; so sue me!)

function ShowPlayer()
{
    GameObject.Find("rootJoint").GetComponent(SkinnedMeshRenderer).enabled = true; // start rendering the player again.
    isControllable = true;    // allow player to control the character again.
}


function UpdateSmoothedMovementDirection ()
{
    var cameraTransform = Camera.main.transform;
    var grounded = IsGrounded();
   
    // Forward vector relative to the camera along the x-z plane   
    var forward = cameraTransform.TransformDirection(Vector3.forward);
    forward.y = 0;
    forward = forward.normalized;

    // Right vector relative to the camera
    // Always orthogonal to the forward vector
    var right = Vector3(forward.z, 0, -forward.x);

    var v = Input.GetAxisRaw("Vertical");
    var h = Input.GetAxisRaw("Horizontal");

    // Are we moving backwards or looking backwards
    if (v < -0.2)
        movingBack = true;
    else
        movingBack = false;
   
    var wasMoving = isMoving;
    isMoving = Mathf.Abs (h) > 0.1 || Mathf.Abs (v) > 0.1;
       
    // Target direction relative to the camera
    var targetDirection = h * right + v * forward;
   
    // Grounded controls
    if (grounded)
    {
        // Lock camera for short period when transitioning moving & standing still
        lockCameraTimer += Time.deltaTime;
        if (isMoving != wasMoving)
            lockCameraTimer = 0.0;

        // We store speed and direction seperately,
        // so that when the character stands still we still have a valid forward direction
        // moveDirection is always normalized, and we only update it if there is user input.
        if (targetDirection != Vector3.zero)
        {
            // If we are really slow, just snap to the target direction
            if (moveSpeed < walkSpeed * 0.9 && grounded)
            {
                moveDirection = targetDirection.normalized;
            }
            // Otherwise smoothly turn towards it
            else
            {
                moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
               
                moveDirection = moveDirection.normalized;
            }
        }
       
        // Smooth the speed based on the current target direction
        var curSmooth = speedSmoothing * Time.deltaTime;
       
        // Choose target speed
        //* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways
        var targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0);
   
        // Pick speed modifier
        if (Input.GetButton ("Fire3"))
        {
            targetSpeed *= runSpeed;
        }
        else if (Time.time - trotAfterSeconds > walkTimeStart)
        {
            targetSpeed *= trotSpeed;
        }
        else
        {
            targetSpeed *= walkSpeed;
        }
       
        moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
       
        // Reset walk time start when we slow down
        if (moveSpeed < walkSpeed * 0.3)
            walkTimeStart = Time.time;
    }
    // In air controls
    else
    {
        // Lock camera while in air
        if (jumping)
            lockCameraTimer = 0.0;

        if (isMoving)
            inAirVelocity += targetDirection.normalized * Time.deltaTime * inAirControlAcceleration;
    }
   

       
}

function ApplyWallJump ()
{
    // We must actually jump against a wall for this to work
    if (!jumping)
        return;

    // Store when we first touched a wall during this jump
    if (collisionFlags == CollisionFlags.CollidedSides)
    {
        touchWallJumpTime = Time.time;
    }

    // The user can trigger a wall jump by hitting the button shortly before or shortly after hitting the wall the first time.
    var mayJump = lastJumpButtonTime > touchWallJumpTime - wallJumpTimeout && lastJumpButtonTime < touchWallJumpTime + wallJumpTimeout;
    if (!mayJump)
        return;
   
    // Prevent jumping too fast after each other
    if (lastJumpTime + jumpRepeatTime > Time.time)
        return;
   
       
    if (Mathf.Abs(wallJumpContactNormal.y) < 0.2)
    {
        wallJumpContactNormal.y = 0;
        moveDirection = wallJumpContactNormal.normalized;
        // Wall jump gives us at least trotspeed
        moveSpeed = Mathf.Clamp(moveSpeed * 1.5, trotSpeed, runSpeed);
    }
    else
    {
        moveSpeed = 0;
    }
   
    verticalSpeed = CalculateJumpVerticalSpeed (jumpHeight);
    DidJump();
    SendMessage("DidWallJump", null, SendMessageOptions.DontRequireReceiver);
}

function ApplyJumping ()
{
    // Prevent jumping too fast after each other
    if (lastJumpTime + jumpRepeatTime > Time.time)
        return;

    if (IsGrounded()) {
        // Jump
        // - Only when pressing the button down
        // - With a timeout so you can press the button slightly before landing       
        if (canJump && Time.time < lastJumpButtonTime + jumpTimeout) {
            verticalSpeed = CalculateJumpVerticalSpeed (jumpHeight);
            SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
        }
    }
}


function ApplyGravity ()
{
    if (isControllable)    // don't move player at all if not controllable.
    {
        // Apply gravity
        var jumpButton = Input.GetButton("Jump");
       
        // * When falling down we use controlledDescentGravity (only when holding down jump)
        var controlledDescent = canControlDescent && verticalSpeed <= 0.0 && jumpButton && jumping;
       
        // When we reach the apex of the jump we send out a message
        if (jumping && !jumpingReachedApex && verticalSpeed <= 0.0)
        {
            jumpingReachedApex = true;
            SendMessage("DidJumpReachApex", SendMessageOptions.DontRequireReceiver);
        }
   
        // * When jumping up we don't apply gravity for some time when the user is holding the jump button
        //   This gives more control over jump height by pressing the button longer
        var extraPowerJump =  IsJumping () && verticalSpeed > 0.0 && jumpButton && transform.position.y < lastJumpStartHeight + extraJumpHeight;
       
        if (controlledDescent)           
            verticalSpeed -= controlledDescentGravity * Time.deltaTime;
        else if (extraPowerJump)
            return;
        else if (IsGrounded ())
            verticalSpeed = 0.0;
        else
            verticalSpeed -= gravity * Time.deltaTime;
    }
}

function CalculateJumpVerticalSpeed (targetJumpHeight : float)
{
    // From the jump height and gravity we deduce the upwards speed
    // for the character to reach at the apex.
    return Mathf.Sqrt(2 * targetJumpHeight * gravity);
}

function DidJump ()
{
    jumping = true;
    jumpingReachedApex = false;
    lastJumpTime = Time.time;
    lastJumpStartHeight = transform.position.y;
    touchWallJumpTime = -1;
    lastJumpButtonTime = -10;
}

function Update() {
   
    if (!isControllable)
    {
        // kill all inputs if not controllable.
        Input.ResetInputAxes();
    }

    if (Input.GetButtonDown ("Jump"))
    {
        lastJumpButtonTime = Time.time;
    }

    UpdateSmoothedMovementDirection();
   
    // Apply gravity
    // - extra power jump modifies gravity
    // - controlledDescent mode modifies gravity
    ApplyGravity ();

    // Perform a wall jump logic
    // - Make sure we are jumping against wall etc.
    // - Then apply jump in the right direction)
    if (canWallJump)
        ApplyWallJump();

    // Apply jumping logic
    ApplyJumping ();
   
    // Calculate actual motion
    var movement = moveDirection * moveSpeed + Vector3 (0, verticalSpeed, 0) + inAirVelocity;
    movement *= Time.deltaTime;
   
    // Move the controller
    var controller : CharacterController = GetComponent(CharacterController);
    wallJumpContactNormal = Vector3.zero;
    collisionFlags = controller.Move(movement);
   
    // Set rotation to the move direction
    if (IsGrounded())
    {
        if(slammed) // we got knocked over by an enemy. We need to reset some stuff
        {
            slammed = false;
            controller.height = 2;
            transform.position.y += 0.75;
        }
       
        transform.rotation = Quaternion.LookRotation(moveDirection);
           
    }   
    else
    {
        if(!slammed)
        {
            var xzMove = movement;
            xzMove.y = 0;
            if (xzMove.sqrMagnitude > 0.001)
            {
                transform.rotation = Quaternion.LookRotation(xzMove);
            }
        }
    }   
   
    // We are in jump mode but just became grounded
    if (IsGrounded())
    {
        lastGroundedTime = Time.time;
        inAirVelocity = Vector3.zero;
        if (jumping)
        {
            jumping = false;
            SendMessage("DidLand", SendMessageOptions.DontRequireReceiver);
        }
    }
}

function OnControllerColliderHit (hit : ControllerColliderHit )
{
//    Debug.DrawRay(hit.point, hit.normal);
    if (hit.moveDirection.y > 0.01)
        return;
    wallJumpContactNormal = hit.normal;
}

function GetSpeed () {
    return moveSpeed;
}

function IsJumping () {
    return jumping && !slammed;
}

function IsGrounded () {
    return (collisionFlags & CollisionFlags.CollidedBelow) != 0;
}

function SuperJump (height : float)
{
    verticalSpeed = CalculateJumpVerticalSpeed (height);
    collisionFlags = CollisionFlags.None;
    SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
}

function SuperJump (height : float, jumpVelocity : Vector3)
{
    verticalSpeed = CalculateJumpVerticalSpeed (height);
    inAirVelocity = jumpVelocity;
   
    collisionFlags = CollisionFlags.None;
    SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
}

function Slam (direction : Vector3)
{
    verticalSpeed = CalculateJumpVerticalSpeed (1);
    inAirVelocity = direction * 6;
    direction.y = 0.6;
    Quaternion.LookRotation(-direction);
    var controller : CharacterController = GetComponent(CharacterController);
    controller.height = 0.5;
    slammed = true;
    collisionFlags = CollisionFlags.None;
    SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
}

function GetDirection () {
    return moveDirection;
}

function IsMovingBackwards () {
    return movingBack;
}

function GetLockCameraTimer ()
{
    return lockCameraTimer;
}

function IsMoving ()  : boolean
{
    return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5;
}

function HasJumpReachedApex ()
{
    return jumpingReachedApex;
}

function IsGroundedWithTimeout ()
{
    return lastGroundedTime + groundedTimeout > Time.time;
}

function IsControlledDescent ()
{
    // * When falling down we use controlledDescentGravity (only when holding down jump)
    var jumpButton = Input.GetButton("Jump");
    return canControlDescent && verticalSpeed <= 0.0 && jumpButton && jumping;
}

function Reset ()
{
    gameObject.tag = "Player";
}
// Require a character controller to be attached to the same game object
@script RequireComponent(CharacterController)
@script AddComponentMenu("Third Person Player/Third Person Controller")


Nun kommt der Netzwerk-Kram dran. Der Player braucht noch ein Netzwerk-Initialisierungs-Script (ThirdPersonNetworkInit)

Das Network-Init-Script

Hänge dieses Script an das Player-Prefab an:

function OnNetworkInstantiate (msg : NetworkMessageInfo) {
    // This is our own player
    if (networkView.isMine)
    {
        //Camera.main.SendMessage("SetTarget", transform);
        GetComponent("NetworkInterpolatedTransform").enabled = false;
    }
    // This is just some remote controlled player
    else
    {
        name += "Remote";
        GetComponent(ThirdPersonController).enabled = false;
        //GetComponent(ThirdPersonSimpleAnimation).enabled = false;
        GetComponent("NetworkInterpolatedTransform").enabled = true;
    }
}


Dieses Script greift auf eine Komponente "NetworkInterpolatedTransform" zu, welche ein C#-Script ist.
Erstelle also dieses, und hänge es an das Player-Prefab ran:

NetworkInterpolatedTransform.cs

using UnityEngine; using System.Collections;

public class NetworkInterpolatedTransform : MonoBehaviour {
   
    public double interpolationBackTime = 0.1;
   
    internal struct  State
    {
        internal double timestamp;
        internal Vector3 pos;
        internal Quaternion rot;
    }

    // We store twenty states with "playback" information
    State[] m_BufferedState = new State[20];
    // Keep track of what slots are used
    int m_TimestampCount;
   
    void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)
    {
        // Always send transform (depending on reliability of the network view)
        if (stream.isWriting)
        {
            Vector3 pos = transform.localPosition;
            Quaternion rot = transform.localRotation;
            stream.Serialize(ref pos);
            stream.Serialize(ref rot);
        }
        // When receiving, buffer the information
        else
        {
            // Receive latest state information
            Vector3 pos = Vector3.zero;
            Quaternion rot = Quaternion.identity;
            stream.Serialize(ref pos);
            stream.Serialize(ref rot);
          
            // Shift buffer contents, oldest data erased, 18 becomes 19, ... , 0 becomes 1
            for (int i=m_BufferedState.Length-1;i>=1;i--)
            {
                m_BufferedState[i] = m_BufferedState[i-1];
            }
          
            // Save currect received state as 0 in the buffer, safe to overwrite after shifting
            State state;
            state.timestamp = info.timestamp;
            state.pos = pos;
            state.rot = rot;
            m_BufferedState[0] = state;
          
            // Increment state count but never exceed buffer size
            m_TimestampCount = Mathf.Min(m_TimestampCount + 1, m_BufferedState.Length);

            // Check integrity, lowest numbered state in the buffer is newest and so on
            for (int i=0;i<m_TimestampCount-1;i++)
            {
                if (m_BufferedState[i].timestamp < m_BufferedState[i+1].timestamp)
                    Debug.Log("State inconsistent");
            }
          
            //Debug.Log("stamp: " + info.timestamp + "my time: " + Network.time + "delta: " + (Network.time - info.timestamp));
        }
    }
   
    // This only runs where the component is enabled, which is only on remote peers (server/clients)
    void Update () {
        double currentTime = Network.time;
        double interpolationTime = currentTime - interpolationBackTime;
        // We have a window of interpolationBackTime where we basically play
        // By having interpolationBackTime the average ping, you will usually use interpolation.
        // And only if no more data arrives we will use extrapolation
      
        // Use interpolation
        // Check if latest state exceeds interpolation time, if this is the case then
        // it is too old and extrapolation should be used
        if (m_BufferedState[0].timestamp > interpolationTime)
        {
            for (int i=0;i<m_TimestampCount;i++)
            {
                // Find the state which matches the interpolation time (time+0.1) or use last state
                if (m_BufferedState[i].timestamp <= interpolationTime || i == m_TimestampCount-1)
                {
                    // The state one slot newer (<100ms) than the best playback state
                    State rhs = m_BufferedState[Mathf.Max(i-1, 0)];
                    // The best playback state (closest to 100 ms old (default time))
                    State lhs = m_BufferedState[i];
                  
                    // Use the time between the two slots to determine if interpolation is necessary
                    double length = rhs.timestamp - lhs.timestamp;
                    float t = 0.0F;
                    // As the time difference gets closer to 100 ms t gets closer to 1 in
                    // which case rhs is only used
                    if (length > 0.0001)
                        t = (float)((interpolationTime - lhs.timestamp) / length);
                  
                    // if t=0 => lhs is used directly
                    transform.localPosition = Vector3.Lerp(lhs.pos, rhs.pos, t);
                    transform.localRotation = Quaternion.Slerp(lhs.rot, rhs.rot, t);
                    return;
                }
            }
        }
        // Use extrapolation. Here we do something really simple and just repeat the last
        // received state. You can do clever stuff with predicting what should happen.
        else
        {
            State latest = m_BufferedState[0];
          
            transform.localPosition = latest.pos;
            transform.localRotation = latest.rot;
        }
    }
}

Der Connect-Bildschirm

Erstelle ein Script namens "ConnectGui" und hänge es an die Kamera an. In diesem Script werden verschiedene Buttons angezeigt und zwei Textfelder, in welchen man IP und Port für den Client angeben kann.

//DontDestroyOnLoad(this);
var remoteIP = "127.0.0.1";
var remotePort = 25001;
var listenPort = 25000;
var remoteGUID = "";
var useNat = false;
private var connectionInfo = "";

function Awake()
{
    if (FindObjectOfType(ConnectGuiMasterServer))
        this.enabled = false;
}

function OnGUI ()
{
    GUILayout.Space(10);
    GUILayout.BeginHorizontal();
    GUILayout.Space(10);
    if (Network.peerType == NetworkPeerType.Disconnected)
    {
        useNat = GUILayout.Toggle(useNat, "Use NAT punchthrough");
        GUILayout.EndHorizontal();
        GUILayout.BeginHorizontal();
        GUILayout.Space(10);

        GUILayout.BeginVertical();
        if (GUILayout.Button ("Connect"))
        {
            if (useNat)
            {
                if (!remoteGUID)
                    Debug.LogWarning("Invalid GUID given, must be a valid one as reported by Network.player.guid or returned in a HostData struture from the master server");
                else
                    Network.Connect(remoteGUID);
            }
            else
            {
                Network.Connect(remoteIP, remotePort);
            }
        }
        if (GUILayout.Button ("Start Server"))
        {
            Network.InitializeServer(32, listenPort, useNat);
            // Notify our objects that the level and the network is ready
            for (var go in FindObjectsOfType(GameObject))
                go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);       
        }
        GUILayout.EndVertical();
        if (useNat)
        {
            remoteGUID = GUILayout.TextField(remoteGUID, GUILayout.MinWidth(145));
        }
        else
        {
            remoteIP = GUILayout.TextField(remoteIP, GUILayout.MinWidth(100));
            remotePort = parseInt(GUILayout.TextField(remotePort.ToString()));
        }
    }
    else
    {
        if (useNat)
            GUILayout.Label("GUID: " + Network.player.guid + " - ");
        GUILayout.Label("Local IP/port: " + Network.player.ipAddress + "/" + Network.player.port);
        GUILayout.Label(" - External IP/port: " + Network.player.externalIP + "/" + Network.player.externalPort);
        GUILayout.EndHorizontal();
        GUILayout.BeginHorizontal();
        if (GUILayout.Button ("Disconnect"))
            Network.Disconnect(200);
    }
    GUILayout.FlexibleSpace();
    GUILayout.EndHorizontal();
}

function OnServerInitialized()
{
    if (useNat)
        Debug.Log("==> GUID is " + Network.player.guid + ". Use this on clients to connect with NAT punchthrough.");
    Debug.Log("==> Local IP/port is " + Network.player.ipAddress + "/" + Network.player.port + ". Use this on clients to connect directly.");
}

function OnConnectedToServer() {
    // Notify our objects that the level and the network is ready
    for (var go in FindObjectsOfType(GameObject))
        go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);       
}

function OnDisconnectedFromServer () {
    if (this.enabled != false)
        Application.LoadLevel(Application.loadedLevel);
    else
        FindObjectOfType(NetworkLevelLoad).OnDisconnectedFromServer();
}


Das obige Script verlangt nach einer GameComponent namens "ConnectGuiMasterServer". Das ist auch ein Script. (nur was ist ein master server??) Also erstellen wir dieses auch:

ConnectGuiMasterServer

var serverPort = 33433;

private var timeoutHostList = 0.0;
private var lastHostListRequest = -1000.0;
private var hostListRefreshTimeout = 10.0;

private var connectionTestResult : ConnectionTesterStatus = ConnectionTesterStatus.Undetermined;
private var filterNATHosts = false;
private var probingPublicIP = false;
private var doneTesting = false;
private var timer : float = 0.0;
private var useNat = false;        // Should the server enabled NAT punchthrough feature

private var windowRect;
private var serverListRect;
private var hideTest = false;
private var testMessage = "Undetermined NAT capabilities";

// Enable this if not running a client on the server machine
//MasterServer.dedicatedServer = true;

function OnFailedToConnectToMasterServer(info: NetworkConnectionError)
{
    Debug.Log(info);
}

function OnFailedToConnect(info: NetworkConnectionError)
{
    Debug.Log(info);
}

function OnGUI ()
{
    windowRect = GUILayout.Window (0, windowRect, MakeWindow, "Server Controls");
    if (Network.peerType == NetworkPeerType.Disconnected && MasterServer.PollHostList().Length != 0)
        serverListRect = GUILayout.Window(1, serverListRect, MakeClientWindow, "Server List");
}

function Awake ()
{
    windowRect = Rect(Screen.width-300,0,300,100);
    serverListRect = Rect(0, 0, Screen.width - windowRect.width, 100);
    // Start connection test
    connectionTestResult = Network.TestConnection();
   
    // What kind of IP does this machine have? TestConnection also indicates this in the
    // test results
    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 test is undetermined, keep running
    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-connectble 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)
            {
                Debug.Log("Testing if firewall can be circumvented");
                connectionTestResult = Network.TestConnectionNAT();
                probingPublicIP = true;
                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:
            Debug.Log("LimitedNATPunchthroughPortRestricted");
            testMessage = "Limited NAT punchthrough capabilities. Cannot connect to all types of NAT servers.";
            useNat = true;
            doneTesting = true;
            break;
                   
        case ConnectionTesterStatus.LimitedNATPunchthroughSymmetric:
            Debug.Log("LimitedNATPunchthroughSymmetric");
            testMessage = "Limited NAT punchthrough capabilities. Cannot connect to all types of NAT servers. Running a server is ill adviced as not everyone can connect.";
            useNat = true;
            doneTesting = true;
            break;
       
        case ConnectionTesterStatus.NATpunchthroughAddressRestrictedCone:
        case ConnectionTesterStatus.NATpunchthroughFullCone:
            Debug.Log("NATpunchthroughAddressRestrictedCone || 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;
    }
    //Debug.Log(connectionTestResult + " " + probingPublicIP + " " + doneTesting);
}

function MakeWindow (id : int)
{   
    hideTest = GUILayout.Toggle(hideTest, "Hide test info");
   
    if (!hideTest)
    {
        GUILayout.Label(testMessage);
        if (GUILayout.Button ("Retest connection"))
        {
            Debug.Log("Redoing connection test");
            probingPublicIP = false;
            doneTesting = false;
            connectionTestResult = Network.TestConnection(true);
        }
    }
   
    if (Network.peerType == NetworkPeerType.Disconnected)
    {
        GUILayout.BeginHorizontal();
        GUILayout.Space(10);
        // Start a new server
        if (GUILayout.Button ("Start Server"))
        {
            Network.InitializeServer(32, serverPort, useNat);
            MasterServer.RegisterHost(gameName, "stuff", "l33t game for all");
        }

        // Refresh hosts
        if (GUILayout.Button ("Refresh available Servers") || Time.realtimeSinceStartup > lastHostListRequest + hostListRefreshTimeout)
        {
            MasterServer.RequestHostList (gameName);
            lastHostListRequest = Time.realtimeSinceStartup;
        }
       
        GUILayout.FlexibleSpace();
       
        GUILayout.EndHorizontal();
    }
    else
    {
        if (GUILayout.Button ("Disconnect"))
        {
            Network.Disconnect();
            MasterServer.UnregisterHost();
        }
        GUILayout.FlexibleSpace();
    }
    GUI.DragWindow (Rect (0,0,1000,1000));
}

function MakeClientWindow(id : int)
{
    GUILayout.Space(5);

    var data : HostData[] = MasterServer.PollHostList();
    var count = 0;
    for (var element in data)
    {
        GUILayout.BeginHorizontal();

        // Do not display NAT enabled games if we cannot do NAT punchthrough
        if ( !(filterNATHosts && element.useNat) )
        {
            var connections = element.connectedPlayers + "/" + element.playerLimit;
            GUILayout.Label(element.gameName);
            GUILayout.Space(5);
            GUILayout.Label(connections);
            GUILayout.Space(5);
            var hostInfo = "";
           
            // Indicate if NAT punchthrough will be performed, omit showing GUID
            if (element.useNat)
            {
                GUILayout.Label("NAT");
                GUILayout.Space(5);
            }
            // 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
            // 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 connect to the
            // first valid one.
            for (var host in element.ip)
                hostInfo = hostInfo + host + ":" + element.port + " ";
           
            //GUILayout.Label("[" + element.ip + ":" + element.port + "]");   
            GUILayout.Label(hostInfo);   
            GUILayout.Space(5);
            GUILayout.Label(element.comment);
            GUILayout.Space(5);
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("Connect"))
                Network.Connect(element);
        }
        GUILayout.EndHorizontal();   
    }
}


Desweiteren greift das "ConnectGui"-Script auf ein Script namens "NetworkLevelLoad" zu.

NetworkLevelLoad

var supportedNetworkLevels : String[] = [ "mylevel" ];
var disconnectedLevel : String = "loader";

// Keep track of the last level prefix (increment each time a new level loads)
private var lastLevelPrefix = 0;

function Awake ()
{
    // Network level loading is done in a seperate channel.
    DontDestroyOnLoad(this);
    networkView.group = 1;
    Application.LoadLevel(disconnectedLevel);
}

function OnGUI ()
{
    // When network is running (server or client) then display the levels
    // configured in the supportedNetworkLevels array and allow them to be loaded
    // at the push of a button
    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))
            {
                // Make sure no old RPC calls are buffered and then send load level command
                Network.RemoveRPCsInGroup(0);
                Network.RemoveRPCsInGroup(1);
                // Load level with incremented level prefix (for view IDs)
                networkView.RPC( "LoadLevel", RPCMode.AllBuffered, level, lastLevelPrefix + 1);
            }
        }
        GUILayout.FlexibleSpace();
        GUILayout.EndHorizontal();
        GUILayout.EndArea();
    }
}

@RPC
function LoadLevel (level : String, levelPrefix : int)
{
    Debug.Log("Loading level " + level + " with prefix " + levelPrefix);
    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.
    // 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
    Network.SetSendingEnabled(0, true);

    // Notify our objects that the level and the network is ready
    for (var go in FindObjectsOfType(GameObject))
        go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);   
}

function OnDisconnectedFromServer ()
{
    Application.LoadLevel(disconnectedLevel);
}

@script RequireComponent(NetworkView)


Jetzt fehlt nur noch ein kleines Script, mit welchem der Spawn-Point auch wirklich einen Player spawnt:

Das Instantiate-Script

Dieses Script wird an das Spawn-Prefab angehängt:
#pragma strict

var InstantiateObject:Transform;

function OnNetworkLoadedLevel()
{
    // Instantiating the object when network is loaded.
    Network.Instantiate(InstantiateObject,transform.position,transform.rotation,0);
}

function OnPlayerDisconnected(player : NetworkPlayer)
{
    Network.RemoveRPCs(player,0);
    Network.DestroyPlayerObjects(player);
}
Zum Schluss muss man noch das "Player"-Prefab auf das "InstantiateObject" im Instantiante-Script im Spawn-Point ziehen. (Spawn-Point anklicken, dann das Script ausklappen und dort das Player Prefab auf InstantiateObject ziehen...nicht beim Spawn-PREFAB sondern beim Spawn-Punkt, der in der Scene ist.) Nun sollte eine einfache Netzwerkkommunikation möglich sein: Man kann einen Server starten, und muss dann beim Client die angezeigte IP angeben und auf "Connect" klicken. Beim Spawn-Point sollten neue Spieler dann erstellt werden.

Keine Kommentare:

Kommentar veröffentlichen