本节将指导您从头开始创建将在本教程中使用的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/。
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
- 让我们在层次结构中添加一个CharacterController组件到我的Kyle Robot实例。你可以直接在Prefab本身上这样做,但我们需要调整它,所以这是更快的这种方式。
- 双击My Robot Kyle让场景视图放大。注意Capsule Collider在脚中间; 我们需要Capsule Collider来正确匹配角色。
- 在Capsule Collider组件中把Center.y属性改成1。
- 点击Apply使改变对prefab生效。这不很重要,因为我们编辑了My Kyle Robot prefab,我们想要所有的实例都生效,不只是这一个,所以点击Apply。
动画设置
分配动画控制器
Kyle Robot Fbx资源需要Animator Graph控制。我们这篇教程不会介绍这个Graph的创建,所以我们提供了一个控制器,在项目Assets中Photon Unity Networking/Demos/PunBasics/Animator/ 下面,叫做Kyle Robot。
AnimatorController.png要把这个Kyle Robot控制器分配给我们的Prefab,只需要把Animator组件的Controller属性设置为Kyle Robot。
AssignAnimatorController.png不要忘了,如果你修改的是My Kyle Robot实例,你需要点击Prefab的Apply使改变生效。
尝试控制器参数
理解Animator控制器的关键特性是动画参数,这些参数是我们如何通过脚本控制动画。在我们的例子中,有参数例如速度,方向,跳跃,Hi。
Animator组件的一个重要功能是根据其动画实际移动角色的能力,这个功能称为Root Motion,并且在Animator组件上有一个属性Apply Root Motion,默认情况下是true。
所以,实际上,要让人物走路,我们只需要将速度动画参数设置为正值,它将开始行走和向前移动。 我们开工吧!
Animator Manager Script
让我们创建一个新的脚本,我们将基于用户的输入控制角色。
-
创建新的脚本PlayerAnimatorManager
-
添加到My Robot Kyle这个Prefab 上
-
代码如下
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 } }
-
保存脚本
Animator Manager : 速度控制
我们要做的第一件事实获得Animator组件,以便于控制它
-
确保你正在编辑PlayerAnimatorManager
-
创建一个Animator类型的私有变量animator
-
在Start()函数中保存Animator组件
private Animator animator; // Use this for initialization void Start() { animator = GetComponent<Animator>(); if (!animator) { Debug.LogError("PlayerAnimatorManager is Missing Animator Component", this); } }
注意,由于我们需要一个Animator组件,如果我们没有得到一个,我们记录一个错误,以便它不被忽视,并由开发人员直接解决。你应该总是写出来,如果它将被其他人使用的话:)它尽管单调乏味,但长远来看是值得的。
-
让我们监听用户输出,控制动画的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); }
-
保存脚本
让我们看看这个脚本做了什么:
因为我们的游戏不允许向后移动,所以我们确保v大于0.用户按下向下键或's'键,我们不允许这样做,并将值强制为0。
你会注意到,我们已经平方了两个输入,为什么? 所以它总是一个正的绝对值,以及增加一些缓和。 这里是个微妙的技巧。你也可以使用Mathf.Abs(),这将工作正常。
我们还增加了两个输入来控制Speed,所以当我们只是按下左边的输入,我们转弯时仍然获得一些速度。
当然,所有这些都是非常具体的我们的角色设计,根据你的游戏逻辑,你可能希望角色原地转,或有能力后退,所以动画参数的控制总是针对具体的游戏 。
测试,测试 1 2 3...
让我们验证我们迄今为止做了什么。确保您已打开Kyle Test场景。目前,在这个场景中,我们只有一个相机和Kyle Robot实例,场景缺少地面机器人站立的地面,如果你现在运行场景凯尔机器人会下降。 此外,我们不会在现场照明或任何奇怪,我们想测试和验证我们的角色和脚本是否正常工作。
- 将Cube添加到场景。因为一个Cube默认情况下是Box碰撞器,我们很好地使用它作为地板。
- 把它放到0,-0.5,0,因为立方体的高度是1.我们希望立方体的顶面是地板。
- 将Cube缩放到30,1,30,以便我们有空间进行实验
- 选择相机并将其移开,以获得良好的观察视野。一个很好的技巧是在Scene视图中调整到您喜欢的视图,选择相机并转到菜单 "GameObject/Align With View",摄像机将匹配场景视图。
- 最后一步,往y轴上方移动My Robot Kyle 0.1单位,否则碰撞在开始时被错过,角色通过地板,所以在碰撞者之间留下一些物理空间。
- 运行场景,按向上箭头键或'a',角色正在走!您可以使用所有键进行测试以验证。
很好,但仍然很多工作在前头,我们的相机需要跟随,我们不能转动...
如果你现在想在相机上工作,请转到相机部分,本页的其余部分将完成Animator控件并实现旋转。
Animator Manager 脚本:方向控制
控制旋转将稍微更复杂,我们不希望我们的角色因为按左右键突然旋转,我们希望温柔平滑的旋转。幸运的是,动画参数可以使用一些阻尼来设置。
-
确保您正在编辑脚本 PlayerAnimatorManager
-
在“PRIVATE PROPERTIES”区域上方的脚本的新区域“PUBLIC PROPERTIES”中创建公共float变量DirectionDampTime
//#region PUBLIC PROPERTIES public float DirectionDampTime = .25f; //#endregion
-
在Update函数结尾处,添加:
animator.SetFloat( "Direction", h, DirectionDampTime, Time.deltaTime );
所以我们马上注意到animator.SetFloat()有不同的声明。我们用来控制速度是一个简单的,但对于这一个需要两个参数,一个是阻尼时间,一个deltaTime。阻尼时间有意义:它达到所需的值需要多长时间,但deltaTime呢?它本质上让你编写的帧速率独立的代码,因为Update()取决于帧速率,我们需要通过使用deltaTime来考虑这种情况。尽可能多的阅读关于这个主题,当你和在网上搜索的时候你会发现。只有在你理解了这一概念之后,在动画和随时间变化对数值的一致性控制方面,你才能充分利用Unity的许多功能。
- 保存脚本PlayerAnimatorManager
- 运行你的场景,并使用所有箭头,看看你的角色是行走和转身怎么样
- 测试DirectionDampTime的效果:将其设为1,例如,然后5,看到它需要达到最大转向能力。你会看到转弯半径随着DirectionDampTime增加。
Animator Manager 脚本:跳跃
关于跳跃,我们需要多做一点工作,因为两个原因。一是我们不想让玩家在不跑动的情况下跳跃,二是我们不想跳跃循环。
-
确保正在编辑脚本PlayerAnimatorManager
-
在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"); }
-
保存脚本PlayerAnimatorManager
-
测试,开始跑动然后按下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<Animator>();
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,请去下一部分,完成后回到这里。
- 将组件CameraWork添加到My Kyle Robot Prefab
- 打开属性Follow on Start,可以有效地使照相机即时跟随角色。当我们开始网络实现时,我们将关闭它
- 设置属性Center Offset为0,4,0,这使得相机看起来更高,从而给出了一个更好的视角的环境比,如果相机直视玩家,我们会看到太多的地面什么都没有。
- 运行场景Kyle Test,并移动角色,以验证相机正确跟随角色。
光束设置
我们的机器人角色还没有武器,让我们创造一些可以从它的眼睛中发出来的激光束。
添加光束模型
为了简单起见,我们将使用简单的立方体并将它们缩放为非常瘦长。有一些技巧来快速做到这一点:不要直接添加一个Cube作为头部节点的子节点,而是创建它移动它,并放大,然后将其附加到头,这将防止猜测正确旋转值让你的光束与眼睛对齐。
另一个重要的技巧是,对两个光束只使用一个碰撞器。这是为了让物理引擎更好地工作,瘦的碰撞器从来不是一个好主意,它不可靠,所以我们将制作一个大盒子碰撞器,以确保可靠地击中目标。
- 打开Kyle test场景
- 添加一个Cube,命名为Beam Left
- 把它修改成一个长的光束,放到左眼的位置
- 在Hierarchy中选中My Kyle Robot
- 选中Head子节点
- 给Head对象添加一个空白对象,命名为Beams
- 把Beam Left拖拽到Beams下面
- 复制Beams Left,命名为Beams Right
- 把它放到右眼的位置上
- 去掉Beams Right的碰撞体
- 调整Beams Right的碰撞体,让它包括两个Beam对象
- 把Beams Left碰撞体的IsTrigger属性设置为True,我们只想知道光束接触到的玩家,而不是碰撞体
- 撞见一个新的材质,命名为Red Beam,保存
- 把Red Beam赋值给两个Beams
- 对prefab执行Apply
你应该像下面这样的:
KyleRobotBeams.png KyleRobotHeadHierarchyWithBeams.png通过用户输入控制Beams
好了,既然我们有了激光束,让我们使用Fire键来触发他们。
创建一个C#脚本,命名为PlayerManager。下面是该脚本第一个版本的完整内容:
using UnityEngine;
using UnityEngine.EventSystems;
using System.Collections;
namespace Com.MyCompany.MyGame
{
/// <summary>
/// Player manager.
/// Handles fire Input and Beams.
/// </summary>
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
/// <summary>
/// MonoBehaviour method called on GameObject by Unity during early initialization phase.
/// </summary>
void Awake()
{
if (Beams == null)
{
Debug.LogError("<Color=Red><a>Missing</a></Color> Beams Reference.", this);
}
else
{
Beams.SetActive(false);
}
}
/// <summary>
/// MonoBehaviour method called on GameObject by Unity on every frame.
/// </summary>
void Update()
{
ProcessInputs();
// trigger Beams active state
if (Beams != null && IsFiring != Beams.GetActive())
{
Beams.SetActive(IsFiring);
}
}
#endregion
#region Custom
/// <summary>
/// Processes the inputs. Maintain a flag representing when the user is pressing Fire.
/// </summary>
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本身。
- 打开Kyle Test场景
- 在场景Hierachy中选择我的Kyle Robot
- 将PlayerManager组件添加到My Kyle Robot
- 将My Kyle Robot/Root/Ribs/Neck/Head/Beams拖放到Inspector中的PlayerManager Beams属性中
- 将实例中的更改应用到Prefab
如果你点击play,并按Fire1输入(默认情况下是左鼠标或左ctrl键),Beams将显示,并立即隐藏时释放时。
健康设置
让我们实现一个非常简单的健康系统,当光束击中玩家时会减少生命。由于它不是子弹,而是一个恒定的能量流,我们需要以两种方式考虑健康损害,当我们受到光束撞击时,以及在整个时间射束撞击我们。
-
打开PlayerManager脚本
-
为了暴露PhotonView组件,把PlayerManager改变成Photon.PunBehaviour的子对象,
public class PlayerManager : Photon.PunBehaviour {
-
在Public Variables区块添加一个公共的Health属性
[Tooltip("The current Health of our player")] public float Health = 1f;
-
在MonoBehaviour CallBacks区块中添加下面两个方法
/// <summary> /// 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. /// </summary> 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; } /// <summary> /// 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 /// </summary> /// <param name="other">Other.</param> 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; }
-
保存PlayerManager脚本
首先,这两种方法几乎是相同的,唯一的区别是,我们在TriggerStay期间使用Deltatime减少健康,减量的速度不取决于帧速率。这是一个重要的概念,通常适用于动画,但在这里,我们也需要这样,我们希望Health在所有设备上以可预测的方式减少,在更快的计算机上这是不公平的,你的健康下降更快:) Deltatime在这里是为了保证一致性。如果您有问题,并通过搜索Unity社区了解DeltaTime,直到您完全吸收这个概念,然后回来,这是至关重要的。
第二个重要的方面,现在应该明白,我们只影响本地玩家的健康,这就是为什么我们前面退出方法的条件PhotonView不是Mine。
最后,如果击中我们的对象是一个Beam,我们只想影响健康,所以我们使用标签“Beam”检查这点,这是我们为何标记我们的Beam对象。
为了便于调试,我们使Health float作为一个公共浮动,以便在等待UI构建时轻松检查其值。
好吧,这看起来一切正确吗?健康系统是不完整的,当健康是0时,没有考虑到玩家的游戏结束状态,让我们现在做到这一点。
游戏结束健康检查
为了保持简单,当玩家的健康达到0时,我们就离开房间。如果你还记得,我们已经在GameManager Script中创建了一个离开房间的方法。如果我们可以重用这个方法而不是重写一遍,这是不错的主意。 相同结果的重复代码是你应该尽一切代价避免的。这也将是一个好时机,介绍一个非常方便的编程概念,“Singleton”。 虽然这个主题本身可以写满几个教程,我们将只实现极小的“单例”。了解Singleton,它们在Unity上下文中的变体以及它们如何帮助创建强大的功能是非常重要的,并将为您节省很多麻烦。所以,不要犹豫,把时间放在这个教程来了解更多。
-
打开GameManager脚本
-
在Public Properties区块添加这个变量
static public GameManager Instance;
-
在Start函数中添加这行代码
Instance = this;
-
保存GameManager脚本
注意,我们使用[static]关键字修饰了Instance变量,这意味着,不必持有一个指向GameManager实例的指针,就可以使用这个变量,所以你可以在代码中的任何地方做一个简单的GameManager.instance.xxx()。这是非常实用的!让我们看看如何用于我们的游戏结束逻辑管理。
-
打开PlayerManager脚本
-
在Update函数中,ProcessInput之后,加入这些代码
if ( Health <= 0f) { GameManager.Instance.LeaveRoom(); }
-
保存PlayerManager脚本
- 注意,我们考虑到健康可能是负面的,因为激光束造成的损害在强度上是不同的。
- 注意,我们调用了GameManager实例的LeaveRoom()公共方法,而实际上不需要获取组件或任何东西,我们仅仅依赖于我们假设GameManager组件在当前场景中某个GameObject上的事实。
好了,下面我们要处理网络部分。
原文
http://doc.photonengine.com/en-us/pun/current/tutorials/pun-basics-tutorial/player-prefab#cam_setup
网友评论