Photon Unity Networking基础教程 5 Building the Player

本节将指导您从头开始创建将在本教程中使用的Player Prefab,以便我们涵盖创建过程的每一步。

这里介绍一个好的方法,首先尝试和创建一个Player Prefab,可以在没有PUN连接的情况下工作,所以它很容易快速测试,调试,并确保在没有任何网络的时候正常工作。然后,您可以慢慢地建立和修改每个功能到网络兼容的角色:通常,用户输入只应该在玩家拥有的实例上激活,而不是在其他的玩家计算机上。 我们将在下面详细介绍。

主要内容

  • prefab基础
  • CharacterController
  • 动画设置
  • 用户输入
  • 相机设置
  • 光束设置
  • 健康设置

Prefab基础

了解PUN的第一个重要约定是,对于一个要通过网络实例化的Prefab,它需要保存在Resources文件夹中,否则不行。

在Resources中使用Prefabs的第二个重要的副作用,是你需要监视他们的名字。在Assets Resources中不应该有相同名字的Prefab,因为Unity会选择它找到的第一个,因此请务必确保在您的项目资源中,Resources路径中没有两个Prefab命名相同。

我们将使用Unity提供的Kyle Robot作为一个自由资产。它作为一个Fbx文件,它是由3d软件生成的,例如3ds Max,Maya,cinema4d。使用这些软件创建网格和动画超出了本教程的范围,但是对于创建自己的角色和动画来说至关重要。这个机器人Kyle.fbx位于/Assets/Photon Unity Networking/Demos/Shared Assets/。

Photon Unity Networking基础教程 5 Building the Player_第1张图片
KyleRobotFbx.png

这里有一种方法开始使用Kyle Robot.fbx为你的玩家:

  • 在项目浏览器中,创建一个名为“Resources”的文件夹
  • 创建一个新的空场景,并保存为Kyle Test,放在这个文件夹/PunBasics_tutorial/Scenes/
  • 将Robot Kyle拖放到场景Hierarchy上。
  • 将刚刚在Hierarchy中创建的GameObject重命名为My Robot Kyle
  • 将我的机器人Kyle拖放到/PunBasics_tutorial/Resources/

我们现在创建了一个基于Kyle Robot Fbx资产的Prefab,我们在您的场景Kyle Test的Hierarchy中有一个实例。现在我们可以开始使用它。

CharacterController

  1. 让我们在层次结构中添加一个CharacterController组件到我的Kyle Robot实例。你可以直接在Prefab本身上这样做,但我们需要调整它,所以这是更快的这种方式。
  2. 双击My Robot Kyle让场景视图放大。注意Capsule Collider在脚中间; 我们需要Capsule Collider来正确匹配角色。
  3. 在Capsule Collider组件中把Center.y属性改成1。
Photon Unity Networking基础教程 5 Building the Player_第2张图片
KyleRobotCCCenter.png
  1. 点击Apply使改变对prefab生效。这不很重要,因为我们编辑了My Kyle Robot prefab,我们想要所有的实例都生效,不只是这一个,所以点击Apply。
InstancePrefabApply.png

动画设置

分配动画控制器

Kyle Robot Fbx资源需要Animator Graph控制。我们这篇教程不会介绍这个Graph的创建,所以我们提供了一个控制器,在项目Assets中Photon Unity Networking/Demos/PunBasics/Animator/ 下面,叫做Kyle Robot。

Photon Unity Networking基础教程 5 Building the Player_第3张图片
AnimatorController.png

要把这个Kyle Robot控制器分配给我们的Prefab,只需要把Animator组件的Controller属性设置为Kyle Robot。

Photon Unity Networking基础教程 5 Building the Player_第4张图片
AssignAnimatorController.png

不要忘了,如果你修改的是My Kyle Robot实例,你需要点击Prefab的Apply使改变生效。

尝试控制器参数

理解Animator控制器的关键特性是动画参数,这些参数是我们如何通过脚本控制动画。在我们的例子中,有参数例如速度,方向,跳跃,Hi。

Animator组件的一个重要功能是根据其动画实际移动角色的能力,这个功能称为Root Motion,并且在Animator组件上有一个属性Apply Root Motion,默认情况下是true。

所以,实际上,要让人物走路,我们只需要将速度动画参数设置为正值,它将开始行走和向前移动。 我们开工吧!

Animator Manager Script

让我们创建一个新的脚本,我们将基于用户的输入控制角色。

  1. 创建新的脚本PlayerAnimatorManager

  2. 添加到My Robot Kyle这个Prefab 上

  3. 代码如下

     using UnityEngine;
     using System.Collections;
     
     namespace Com.MyCompany.MyGame
     {
         public class PlayerAnimatorManager : MonoBehaviour
         {
             #region MONOBEHAVIOUR MESSAGES
     
             // Use this for initialization
             void Start()
             {
     
             }
     
             // Update is called once per frame
             void Update()
             {
     
             }
             #endregion
         }
     }
    
  4. 保存脚本

Animator Manager : 速度控制

我们要做的第一件事实获得Animator组件,以便于控制它

  1. 确保你正在编辑PlayerAnimatorManager

  2. 创建一个Animator类型的私有变量animator

  3. 在Start()函数中保存Animator组件

     private Animator animator;
     // Use this for initialization
     void Start()
     {
         animator = GetComponent();
         if (!animator)
         {
             Debug.LogError("PlayerAnimatorManager is Missing Animator Component", this);
         }
     }
    

注意,由于我们需要一个Animator组件,如果我们没有得到一个,我们记录一个错误,以便它不被忽视,并由开发人员直接解决。你应该总是写出来,如果它将被其他人使用的话:)它尽管单调乏味,但长远来看是值得的。

  1. 让我们监听用户输出,控制动画的Speed参数

     // Update is called once per frame
     void Update()
     {
         if (!animator)
         {
             return;
         }
         float h = Input.GetAxis("Horizontal");
         float v = Input.GetAxis("Vertical");
         if (v < 0)
         {
             v = 0;
         }
         animator.SetFloat("Speed", h * h + v * v);
     }
    
  2. 保存脚本

让我们看看这个脚本做了什么:

因为我们的游戏不允许向后移动,所以我们确保v大于0.用户按下向下键或's'键,我们不允许这样做,并将值强制为0。

你会注意到,我们已经平方了两个输入,为什么? 所以它总是一个正的绝对值,以及增加一些缓和。 这里是个微妙的技巧。你也可以使用Mathf.Abs(),这将工作正常。

我们还增加了两个输入来控制Speed,所以当我们只是按下左边的输入,我们转弯时仍然获得一些速度。

当然,所有这些都是非常具体的我们的角色设计,根据你的游戏逻辑,你可能希望角色原地转,或有能力后退,所以动画参数的控制总是针对具体的游戏 。

测试,测试 1 2 3...

让我们验证我们迄今为止做了什么。确保您已打开Kyle Test场景。目前,在这个场景中,我们只有一个相机和Kyle Robot实例,场景缺少地面机器人站立的地面,如果你现在运行场景凯尔机器人会下降。 此外,我们不会在现场照明或任何奇怪,我们想测试和验证我们的角色和脚本是否正常工作。

  1. 将Cube添加到场景。因为一个Cube默认情况下是Box碰撞器,我们很好地使用它作为地板。
  2. 把它放到0,-0.5,0,因为立方体的高度是1.我们希望立方体的顶面是地板。
  3. 将Cube缩放到30,1,30,以便我们有空间进行实验
  4. 选择相机并将其移开,以获得良好的观察视野。一个很好的技巧是在Scene视图中调整到您喜欢的视图,选择相机并转到菜单 "GameObject/Align With View",摄像机将匹配场景视图。
  5. 最后一步,往y轴上方移动My Robot Kyle 0.1单位,否则碰撞在开始时被错过,角色通过地板,所以在碰撞者之间留下一些物理空间。
  6. 运行场景,按向上箭头键或'a',角色正在走!您可以使用所有键进行测试以验证。

很好,但仍然很多工作在前头,我们的相机需要跟随,我们不能转动...

如果你现在想在相机上工作,请转到相机部分,本页的其余部分将完成Animator控件并实现旋转。

Animator Manager 脚本:方向控制

控制旋转将稍微更复杂,我们不希望我们的角色因为按左右键突然旋转,我们希望温柔平滑的旋转。幸运的是,动画参数可以使用一些阻尼来设置。

  1. 确保您正在编辑脚本 PlayerAnimatorManager

  2. 在“PRIVATE PROPERTIES”区域上方的脚本的新区域“PUBLIC PROPERTIES”中创建公共float变量DirectionDampTime

     //#region PUBLIC PROPERTIES
     public float DirectionDampTime = .25f;
     //#endregion
    
  3. 在Update函数结尾处,添加:

     animator.SetFloat( "Direction", h, DirectionDampTime, Time.deltaTime );
    

所以我们马上注意到animator.SetFloat()有不同的声明。我们用来控制速度是一个简单的,但对于这一个需要两个参数,一个是阻尼时间,一个deltaTime。阻尼时间有意义:它达到所需的值需要多长时间,但deltaTime呢?它本质上让你编写的帧速率独立的代码,因为Update()取决于帧速率,我们需要通过使用deltaTime来考虑这种情况。尽可能多的阅读关于这个主题,当你和在网上搜索的时候你会发现。只有在你理解了这一概念之后,在动画和随时间变化对数值的一致性控制方面,你才能充分利用Unity的许多功能。

  1. 保存脚本PlayerAnimatorManager
  2. 运行你的场景,并使用所有箭头,看看你的角色是行走和转身怎么样
  3. 测试DirectionDampTime的效果:将其设为1,例如,然后5,看到它需要达到最大转向能力。你会看到转弯半径随着DirectionDampTime增加。

Animator Manager 脚本:跳跃

关于跳跃,我们需要多做一点工作,因为两个原因。一是我们不想让玩家在不跑动的情况下跳跃,二是我们不想跳跃循环。

  1. 确保正在编辑脚本PlayerAnimatorManager

  2. 在update方法中,捕捉用户输入之前,插入下面代码

     // deal with Jumping
     AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);          
     // only allow jumping if we are running.
     if (stateInfo.IsName("Base Layer.Run"))
     {
         // When using trigger parameter
         if (Input.GetButtonDown("Fire2")) animator.SetTrigger("Jump"); 
     }
    
  3. 保存脚本PlayerAnimatorManager

  4. 测试,开始跑动然后按下alt键,Kyle会跳起来

要理解的第一件事情就是我们怎样知道animator正在跑动,通过使用stateInfo.IsName("Base Layer.Run"),我们询问目前Animator的激活状态是否是Run。我们必须添加Base Layer因为Run状态是在Base Layer中的。

如果我们处于Run状态,然后我们监听Fire2输入,然后调用Jump触发器。

到目前为止PlayerAnimatorManager脚本的完整版本:

    using UnityEngine;
    using System.Collections;
    
    namespace Com.MyCompany.MyGame
    {
        public class PlayerAnimatorManager : MonoBehaviour
        {   
            #region PUBLIC PROPERTIES
            public float DirectionDampTime = .25f;
            #endregion
    
            #region MONOBEHAVIOUR MESSAGES
            private Animator animator;
            // Use this for initialization
            void Start()
            {
                animator = GetComponent();
                if (!animator)
                {
                    Debug.LogError("PlayerAnimatorManager is Missing Animator Component", this);
                }
            }
    
            // Update is called once per frame
            void Update()
            {
                if (!animator)
                {
                    return;
                }
    
                // deal with Jumping
                AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
                // only allow jumping if we are running.
                if (stateInfo.IsName("Base Layer.Run"))
                {
                    // When using trigger parameter
                    if (Input.GetButtonDown("Fire1"))
                    {
                        Debug.Log("jump");
                        animator.SetTrigger("Jump");
                    }                
                }
    
                float h = Input.GetAxis("Horizontal");
                float v = Input.GetAxis("Vertical");
                if (v < 0)
                {
                    v = 0;
                }
                animator.SetFloat("Speed", h * h + v * v);
                animator.SetFloat("Direction", h, DirectionDampTime, Time.deltaTime);
            }
            #endregion
        }
    }

当你考虑它在场景中实现的功能,对于这几行代码来说还不错。现在让我们处理相机的工作,因为我们能够在我们的世界活动,我们需要一个合适的相机行为跟随。

相机设置

在本节中,我们将使用CameraWork脚本,以保持专注于Player Prefab整体创建过程。如果你想从头开始写CameraWork,请去下一部分,完成后回到这里。

  1. 将组件CameraWork添加到My Kyle Robot Prefab
  2. 打开属性Follow on Start,可以有效地使照相机即时跟随角色。当我们开始网络实现时,我们将关闭它
  3. 设置属性Center Offset为0,4,0,这使得相机看起来更高,从而给出了一个更好的视角的环境比,如果相机直视玩家,我们会看到太多的地面什么都没有。
  4. 运行场景Kyle Test,并移动角色,以验证相机正确跟随角色。

光束设置

我们的机器人角色还没有武器,让我们创造一些可以从它的眼睛中发出来的激光束。

添加光束模型

为了简单起见,我们将使用简单的立方体并将它们缩放为非常瘦长。有一些技巧来快速做到这一点:不要直接添加一个Cube作为头部节点的子节点,而是创建它移动它,并放大,然后将其附加到头,这将防止猜测正确旋转值让你的光束与眼睛对齐。

另一个重要的技巧是,对两个光束只使用一个碰撞器。这是为了让物理引擎更好地工作,瘦的碰撞器从来不是一个好主意,它不可靠,所以我们将制作一个大盒子碰撞器,以确保可靠地击中目标。

  1. 打开Kyle test场景
  2. 添加一个Cube,命名为Beam Left
  3. 把它修改成一个长的光束,放到左眼的位置
  4. 在Hierarchy中选中My Kyle Robot
  5. 选中Head子节点
Photon Unity Networking基础教程 5 Building the Player_第5张图片
KyleRobotHeadHierarchy.png
  1. 给Head对象添加一个空白对象,命名为Beams
  2. 把Beam Left拖拽到Beams下面
  3. 复制Beams Left,命名为Beams Right
  4. 把它放到右眼的位置上
  5. 去掉Beams Right的碰撞体
  6. 调整Beams Right的碰撞体,让它包括两个Beam对象
  7. 把Beams Left碰撞体的IsTrigger属性设置为True,我们只想知道光束接触到的玩家,而不是碰撞体
  8. 撞见一个新的材质,命名为Red Beam,保存
  9. 把Red Beam赋值给两个Beams
  10. 对prefab执行Apply

你应该像下面这样的:

Photon Unity Networking基础教程 5 Building the Player_第6张图片
KyleRobotBeams.png
KyleRobotHeadHierarchyWithBeams.png

通过用户输入控制Beams

好了,既然我们有了激光束,让我们使用Fire键来触发他们。

创建一个C#脚本,命名为PlayerManager。下面是该脚本第一个版本的完整内容:

    using UnityEngine;
    using UnityEngine.EventSystems;
    using System.Collections;
    
    namespace Com.MyCompany.MyGame
    {
        /// 
        /// Player manager. 
        /// Handles fire Input and Beams.
        /// 
        public class PlayerManager : MonoBehaviour
        {
            #region Public Variables
    
            [Tooltip("The Beams GameObject to control")]
            public GameObject Beams;
    
            #endregion
    
            #region Private Variables
    
            //True, when the user is firing
            bool IsFiring;
    
            #endregion
    
            #region MonoBehaviour CallBacks
    
            /// 
            /// MonoBehaviour method called on GameObject by Unity during early initialization phase.
            /// 
            void Awake()
            {
                if (Beams == null)
                {
                    Debug.LogError("Missing Beams Reference.", this);
                }
                else
                {
                    Beams.SetActive(false);
                }
            }
    
            /// 
            /// MonoBehaviour method called on GameObject by Unity on every frame.
            /// 
            void Update()
            {
                ProcessInputs();
    
                // trigger Beams active state 
                if (Beams != null && IsFiring != Beams.GetActive())
                {
                    Beams.SetActive(IsFiring);
                }
            }
    
            #endregion
    
            #region Custom
    
            /// 
            /// Processes the inputs. Maintain a flag representing when the user is pressing Fire.
            /// 
            void ProcessInputs()
            {
                if (Input.GetButtonDown("Fire1"))
                {
                    if (!IsFiring)
                    {
                        IsFiring = true;
                    }
                }
    
                if (Input.GetButtonUp("Fire1"))
                {
                    if (IsFiring)
                    {
                        IsFiring = false;
                    }
                }
            }
            #endregion
        }
    }

这个脚本在这个阶段的要点是激活或停用激光束。当激活时,激光束将有效地触发与其他模型发生碰撞,因此我们将在后面利用这些触发器来影响每个角色的健康值。

我们还暴露了一个公共属性Beams,它将让我们在My Kyle Robot Prefab的层次结构中引用确切的对象。让我们看看我们如何工作来连接Beams,因为在Assets浏览器中,Prefabs只暴露第一个子节点,而不是所有子节点,而且我们的Beams确实埋在Prefab层次结构中,因此,我们需要从场景中的一个实例执行此操作,然后将其应用回Prefab本身。

  1. 打开Kyle Test场景
  2. 在场景Hierachy中选择我的Kyle Robot
  3. 将PlayerManager组件添加到My Kyle Robot
  4. 将My Kyle Robot/Root/Ribs/Neck/Head/Beams拖放到Inspector中的PlayerManager Beams属性中
  5. 将实例中的更改应用到Prefab

如果你点击play,并按Fire1输入(默认情况下是左鼠标或左ctrl键),Beams将显示,并立即隐藏时释放时。

健康设置

让我们实现一个非常简单的健康系统,当光束击中玩家时会减少生命。由于它不是子弹,而是一个恒定的能量流,我们需要以两种方式考虑健康损害,当我们受到光束撞击时,以及在整个时间射束撞击我们。

  1. 打开PlayerManager脚本

  2. 为了暴露PhotonView组件,把PlayerManager改变成Photon.PunBehaviour的子对象,

     public class PlayerManager : Photon.PunBehaviour {
    
  3. 在Public Variables区块添加一个公共的Health属性

     [Tooltip("The current Health of our player")]
     public float Health = 1f;
    
  4. 在MonoBehaviour CallBacks区块中添加下面两个方法

     /// 
     /// MonoBehaviour method called when the Collider 'other' enters the trigger.
     /// Affect Health of the Player if the collider is a beam
     /// Note: when jumping and firing at the same, you'll find that the player's own beam intersects with itself
     /// One could move the collider further away to prevent this or check if the beam belongs to the player.
     /// 
     void OnTriggerEnter(Collider other)
     {
         if (!photonView.isMine)
         {
             return;
         }
         // We are only interested in Beamers
         // we should be using tags but for the sake of distribution, let's simply check by name.
         if (!other.name.Contains("Beam"))
         {
             return;
         }
         Health -= 0.1f;
     }
    
     /// 
     /// MonoBehaviour method called once per frame for every Collider 'other' that is touching the trigger.
     /// We're going to affect health while the beams are touching the player
     /// 
     /// Other.
     void OnTriggerStay(Collider other)
     {
         // we dont' do anything if we are not the local player.
         if (!photonView.isMine)
         {
             return;
         }
         // We are only interested in Beamers
         // we should be using tags but for the sake of distribution, let's simply check by name.
         if (!other.name.Contains("Beam"))
         {
             return;
         }
         // we slowly affect health when beam is constantly hitting us, so player has to move to prevent death.
         Health -= 0.1f * Time.deltaTime;
     }
    
  5. 保存PlayerManager脚本

首先,这两种方法几乎是相同的,唯一的区别是,我们在TriggerStay期间使用Deltatime减少健康,减量的速度不取决于帧速率。这是一个重要的概念,通常适用于动画,但在这里,我们也需要这样,我们希望Health在所有设备上以可预测的方式减少,在更快的计算机上这是不公平的,你的健康下降更快:) Deltatime在这里是为了保证一致性。如果您有问题,并通过搜索Unity社区了解DeltaTime,直到您完全吸收这个概念,然后回来,这是至关重要的。

第二个重要的方面,现在应该明白,我们只影响本地玩家的健康,这就是为什么我们前面退出方法的条件PhotonView不是Mine。

最后,如果击中我们的对象是一个Beam,我们只想影响健康,所以我们使用标签“Beam”检查这点,这是我们为何标记我们的Beam对象。

为了便于调试,我们使Health float作为一个公共浮动,以便在等待UI构建时轻松检查其值。

好吧,这看起来一切正确吗?健康系统是不完整的,当健康是0时,没有考虑到玩家的游戏结束状态,让我们现在做到这一点。

游戏结束健康检查

为了保持简单,当玩家的健康达到0时,我们就离开房间。如果你还记得,我们已经在GameManager Script中创建了一个离开房间的方法。如果我们可以重用这个方法而不是重写一遍,这是不错的主意。 相同结果的重复代码是你应该尽一切代价避免的。这也将是一个好时机,介绍一个非常方便的编程概念,“Singleton”。 虽然这个主题本身可以写满几个教程,我们将只实现极小的“单例”。了解Singleton,它们在Unity上下文中的变体以及它们如何帮助创建强大的功能是非常重要的,并将为您节省很多麻烦。所以,不要犹豫,把时间放在这个教程来了解更多。

  1. 打开GameManager脚本

  2. 在Public Properties区块添加这个变量

    static public GameManager Instance;

  3. 在Start函数中添加这行代码

    Instance = this;

  4. 保存GameManager脚本

注意,我们使用[static]关键字修饰了Instance变量,这意味着,不必持有一个指向GameManager实例的指针,就可以使用这个变量,所以你可以在代码中的任何地方做一个简单的GameManager.instance.xxx()。这是非常实用的!让我们看看如何用于我们的游戏结束逻辑管理。

  1. 打开PlayerManager脚本

  2. 在Update函数中,ProcessInput之后,加入这些代码

     if ( Health <= 0f)
     {
         GameManager.Instance.LeaveRoom();
     }
    
  3. 保存PlayerManager脚本

  • 注意,我们考虑到健康可能是负面的,因为激光束造成的损害在强度上是不同的。
  • 注意,我们调用了GameManager实例的LeaveRoom()公共方法,而实际上不需要获取组件或任何东西,我们仅仅依赖于我们假设GameManager组件在当前场景中某个GameObject上的事实。

好了,下面我们要处理网络部分。

原文

http://doc.photonengine.com/en-us/pun/current/tutorials/pun-basics-tutorial/player-prefab#cam_setup

你可能感兴趣的:(Photon Unity Networking基础教程 5 Building the Player)