第二部分——主角的创建
*
学习前说明:
项目源码:链接:https://pan.baidu.com/s/1g78L9QODXdRjoVcm-odRSg 密码:0pzo
源码引用自Siki老师的Unet基础系列教程,文章主要以解释为主,后期会添加一些Siki老师源码以外的新东西,敬请期待。
文中用红色显示的内容为我自己命名的关键名词,例如场景名、代码名、阐述代码或方法所实现的功能 等等
文中用蓝色显示的内容为UI元素、组件名或是变量名,例如Button、InputField、NetworkManager 等等
文中用紫色显示的内容为当前所解释的代码部分。例如:
这里使用一个Console命令完成输出语句
public void ShowMessage()
{
Console.WriteLine("Hello World.");
}
本文中仅讨论Unet用于局域网的情况
如有互联网联网需求,请自行查询Unity与Socket协议结合相关的文章,不推荐使用Unet完成
本文主要服务于渤海大学交互式虚拟现实开发基地,为求尽可能的细致和易懂,可能有些地方写的过于冗杂,不喜勿喷
*
需求分析
1.W/S键能够完成向前/向后的移动
2.左右键能够完成朝向的旋转
3.空格键能够完成开枪的操作,每发子弹造成10点的伤害
4.头上显示血量,每个人基础血量为100
*
常用内容(void)
1.继承自NetworkBehaviour而非MonoBehaviour
2.初始化放在OnStartLocalPlayer中而不是Start中
3.Update中放入 if(isLocalPlayer==false) return; 检查是不是本机
4.[SyncVar]修饰的变量,一般用于可以和服务端或是其他客户端交互发生变化的变量。强调具有影响性
。比如我的生命值(可被敌人伤害减少),我的攻击力(可以影响伤害敌人的效果),我的级别(可以影响我的伤害值、体型)等等
5.[SyncVar=hook"方法名"]修饰的变量,用于在变量变化后调用的方法。强调依赖性。比如升级时弹出技能升级面板、血量为0后屏幕变灰等等
6.[Command]修饰的方法,一般用于客户端所能操控的物体。强调主动性。比如我主动开枪,我主动移动,我主动生成随从等等
7.[ClientRpc]修饰的方法,一般用于客户端受到的效果。强调被动性
。比我我被敌人命中,我被杀死,我重生(被恢复满状态并被移动到指定位置)等等。一般配合 if(isServer==false)return; 使用
8.NetworkServer.Spawn()
方法,一般用于生成附属于自己的物体。比如小兵(可以帮自己打敌人),子弹(造成伤害给自己加经验)、棋子(能够判断自己胜利)等等
9.NetworkTransform
组件,一般用于当前控制角色的位移,比如主角的移动,汽车的移动。但是仅能实现当前Transform的变换,不会影响到子物体。比如汽车开车时,其他客户端仅能看到车整体的移动,看不到车轮的移动。
*
实例分析
创建角色
1.创建主角对象,这里使用了Capsule、Cube和Cylinder分别代表身体、眼镜和枪搭成一个简单的人物,注意Cube
的Z轴要为正值,这样才能保证游戏对象是面向“前方”的,眼镜是戴在眼镜上而不是后脑勺。
2.创建一个Canvas,并将它的Render Mode修改为World级别,将Scale调至(0.01,0.01,0.01),并通过修改Position
以调整至主角的头上方。
3.为主角添加一个Slider以表示血条,将其中的Fill Area和Handle Slide Area删去,仅保留Background,
========》
4.修改血条的背景色为红色,填充色为绿色,满血时就是绿色,每掉一点血,就会露出一点红色。
血条面向摄像机
新建脚本LookAtCamera,并挂在Player下的Canvas上。
Camera.main 代表带有MainCamera 标签的Camera组件,一般一个场景中只有一个。
transform.LookAt(目标transform),使物体的正方向中心始终面对这个点,也就是“看向”目标点。
public class LookAtCamera : MonoBehaviour { // Update is called once per frame
void Update () { transform.LookAt(Camera.main.transform); } }
角色控制
1.新建一个脚本Player Controller挂在Player下,并继承自NetworkBehaviour
。当继承自NetworkBehaviour时,自动添加一个组件Network Identity。
这是角色控制的完整代码。
using UnityEngine; using System.Collections; using UnityEngine.Networking; ///
<summary> /// NetworkBehaviour是Unet联网过程中,所有可操控对象必须继承的类 ///
他提供了额外的生命周期函数及联网相关的方法与属性、特性 /// NetworkBehaviour本身是继承自MonoBehaviour ///
</summary> public class PlayerController : NetworkBehaviour { public GameObject
bulletPrefab; public Transform bulletSpawn; // Update is called once per frame
void Update() { if (isLocalPlayer ==
false)//isLocalPlayer用于判断是否是当前客户端进行操作,以防止误操作别人的角色 { return;//如果不是当前对象,则不执行下述代码
} float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical");
//transform.Rotate用于控制物体的旋转, //transform.Rotate(旋转的轴向*单位时间内旋转的角度*旋转的速度)
//当前为匀速转动 transform.Rotate(Vector3.up * (h * 120) * Time.deltaTime);
//transform.Translate用于控制物体的位移 //transform.Translate(位移的方向*单位时间内位移的距离*位移的速度)
//当前为匀速位移 transform.Translate(Vector3.forward * v * 3 * Time.deltaTime);
//点击空格时,开火 if (Input.GetKeyDown(KeyCode.Space)) { CmdFire(); } } /// <summary>
/// 当客户端第一次进入游戏场景时调用 /// </summary> public override void OnStartLocalPlayer() {
//将自己变蓝以区别敌我 GetComponent<MeshRenderer>().material.color = Color.blue; } //
凡是希望从一个客户端发出,在各个客户端都能看到的效果,例如开火,都需要使用Command特性,同时方法名需要以Cmd开头 [Command] void
CmdFire()//实际上这个方法在server里面调用,我们只是发出请求,需要记住,目前不要求理解 { //生成个子弹 GameObject bullet
= Instantiate(bulletPrefab, bulletSpawn.position, bulletSpawn.rotation) as
GameObject; //子弹向前飞 bullet.GetComponent<Rigidbody>().velocity =
bullet.transform.forward * 10; //两秒后自动消失 Destroy(bullet, 2);
//关键部分:将当前的物体(bullet)分发到各个客户端,这样其他的客户端才能看到子弹 NetworkServer.Spawn(bullet); } }
几个地方需要注意(从上至下)
*
在Update开始时,必须先检查isLocalPlayer是不是为false,也就是保证自己只能控制自己的角色,否则一个人点击后,所有人都在动
if (isLocalPlayer == false)
{
return;
}
*
OnStartLocalPlayer会在此客户端加入场景时调用,一般我们不在联网的对象上使用Awake(),Start()方法,可以认为OnStartLocalPlayer就是用于替代Start()方法的,同样的,这个方法也是第一步调用,且只能够调用一次。
此方法需重载(override)
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.blue;
}
* [Command]特性--用于从客户端发出指令,在服务器端执行。
同时,它修饰的方法,方法名前面要加上Cmd前缀。凡是你希望对其他客户端的操作,无论是攻击其他客户端还是自己生成一个新物体(自己生成新物体,也可以理解为通知其他客户端自己有这个物体),都需要这样编写。
[Command]
void CmdFire()
{
//function
}
*
NetworkServer.Spawn() -- 将物体显示在各个客户端上。正常的小兵、子弹等等这种附属关系的物体都需要以下的步骤
在Lobby场景中,先完成物体的注册。只有注册了的物体才能够使用NetworkServer.Spawn()
使用Instantiate方法实例化对象
[Command]
void CmdFire()
{
GameObject bullet = Instantiate(bulletPrefab, bulletSpawn.position,
bulletSpawn.rotation) as GameObject;
}
执行对象所需执行的操作
[Command]
void CmdFire()
{
GameObject bullet = Instantiate(bulletPrefab, bulletSpawn.position,
bulletSpawn.rotation) as GameObject;
bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward
* 10;
Destroy(bullet, 2);
}
使用NetworkServer.Spawn()在各个客户端上显示出来这个物体。他同时会显示出这个物体的状态(位移、旋转、缩放等)
[Command]
void CmdFire()
{
GameObject bullet = Instantiate(bulletPrefab, bulletSpawn.position,
bulletSpawn.rotation) as GameObject;
bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward
* 10;
Destroy(bullet, 2);
NetworkServer.Spawn(bullet);
}
在其他客户端上同步位移
在Player身上加上一个组件——NetworkTransform
选择Transform Sync Mode(Transform同步方式)
添加血条
新建一个代码Health,并将它挂在Player身上
public const int maxHealth = 100; [SyncVar(hook="OnChangeHealth") ] public int
currentHealth = maxHealth; public Slider healthSlider; public bool
destroyOnDeath = false; private NetworkStartPosition[] spawnPoints; void
Start() { if (isLocalPlayer) { spawnPoints =
FindObjectsOfType<NetworkStartPosition>(); } } public void TakeDamage(int
damage) { if (isServer == false) return;// 血量的处理只在服务器端执行 currentHealth -=
damage; if (currentHealth <= 0) { if (destroyOnDeath) {
Destroy(this.gameObject); return; } currentHealth = maxHealth;
Debug.Log("Dead"); RpcRespawn(); } } void OnChangeHealth(int health) {
healthSlider.value = health / (float)maxHealth; } [ClientRpc] void RpcRespawn()
{ if (isLocalPlayer == false) return; Vector3 spawnPosition = Vector3.zero; if
(spawnPoints != null && spawnPoints.Length > 0) { spawnPosition =
spawnPoints[Random.Range(0, spawnPoints.Length)].transform.position; }
transform.position = spawnPosition; }
几个地方需要注意(从上至下)
* [SyncVar]--同步变量,这是一个特性,代表这个变量可以受到其他客户端的影响。
[SyncVar(hook="方法名")],是它的一个派生特性,代表当这个变量发生变化
时,将调用一次hook中提到的方法,如果有参数,默认将此变量作为参数代入方法中。
特性必须紧挨着修饰的变量或方法
[SyncVar(hook="OnChangeHealth") ]
public int currentHealth = maxHealth;
当变量发生变化时,调用方法
[SyncVar(hook="OnChangeHealth") ]
public int currentHealth = maxHealth;
//...
void OnChangeHealth(int health)
{
healthSlider.value = health / (float)maxHealth;
}
* isServer--服务端判断,与isLocalPlayer相对,可以判断当前是否为服务端。
我们当前的方式是让当前主机作为服务端的同时操控一个客户端,其他主机仅操控一个客户端
public void TakeDamage(int damage)
{
if (isServer == false) return;
//....
}
当血量已经为0的时候,让你复活,如果是系统生成的敌人,为他勾上destroyOnDeath,这样当他血量为0,他就会消失了
public void TakeDamage(int damage)
{
if (isServer == false) return
currentHealth -= damage;
if (currentHealth <= 0)
{
if (destroyOnDeath)//当系统敌人血量为0
{
Destroy(this.gameObject); return;
}
currentHealth = maxHealth;
Debug.Log("Dead");
RpcRespawn();//重生
}
}
* [ClientRpc]--从服务端发起,在客户端调用。
这个特性与[Command]相对,它适合用于被动的效果。比如重生(被杀死后移动到某一位置),传送(被移动)等等
同样的,它修饰的方法名前面需要加上Rpc
一般这个方法配合if (isLocalPlayer == false)使用
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer == false) return;
//...
}
为子弹添加效果
新建代码Bullet,并挂在子弹的身上,将它保存为Prefab
using UnityEngine; using System.Collections; public class Bullet :
MonoBehaviour { void OnCollisionEnter(Collision col) { GameObject hit =
col.gameObject; Health health = hit.GetComponent<Health>(); if (health != null)
{ health.TakeDamage(10); } Destroy(this.gameObject); } }
* 利用碰撞检测
void OnCollisionEnter(Collision col)
{
//function
}
* 试图获得物体身上的Health(血条)
void OnCollisionEnter(Collision col)
{
GameObject hit = col.gameObject;
Health health = hit.GetComponent<Health>();
}
* 当取得到物体身上的Health组件,就说这这个物体代表敌人,对他造成伤害
void OnCollisionEnter(Collision col)
{
GameObject hit = col.gameObject;
Health health = hit.GetComponent<Health>();
if (health != null)
{
health.TakeDamage(10);
}
}
* 子弹撞墙或者撞到地面都可以让他消失
void OnCollisionEnter(Collision col)
{
GameObject hit = col.gameObject;
Health health = hit.GetComponent<Health>();
if (health != null)
{
health.TakeDamage(10);
}
Destroy(this.gameObject);
}
最后处理
将此Player制作为Prefab,并在Main场景中删去
Player会在客户端加载的时候自动生成
就这样吧,掰掰。
热门工具 换一换