第二部分——主角的创建
-
学习前说明:
项目源码:链接: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会在客户端加载的时候自动生成
就这样吧,掰掰。