UNet系统初次使用——联网Boxing游戏(1)
UNet系统初次使用——联网Boxing游戏(2)
UNet系统初次使用——联网Boxing游戏(3)
操作规则
- W —— 前进
- S —— 后退
- A —— 左移
- D —— 右移
- 鼠标左键 —— 左拳
- 鼠标邮件 —— 右拳
- 空格 —— 防御
- 鼠标移动 —— 视角转动
七、player动作
动作的状态转移图
在这里我们要按照转移图实现player9个状态的转移。
新建一个PlayerAction挂载到Player上。 PlayerAction继承NetworkManager,因为需要同步player的状态到各个客户端。
两个int变量来同步和保存当前player的状态
[SyncVar(hook = "OnSyncStateChange")]
public int syncState;
public int actionState;
[Command]
public void CmdSetState(int nState)
{
syncState = nState;
}
public void SetState(int nState)
{
actionState = nState;
if (isLocalPlayer)
{
CmdSetState(nState);
}
}
private void OnSyncStateChange(int state)
{
syncState = state;
if (!isLocalPlayer)
actionState = state;
}
使用两个变量可以时延造成的输入和动画不连贯,动画根据变量actionState来更改。而syncState只是用于同步本地客户端和服务器以及各个客户端的actionState。
客户端a输入指令后,其localplayer的actionState马上更改,于是动画系统做出相应反应,实现连贯的动作。而其他客户端上的这一人物,则根据客户端a同步到服务器的actionState,来进行动画的反应。即保证了动作的同步,也解决了时延造成的不连贯。
同时,两个变量也分别用来处理不同的动作,actionState解决输入指令与动作的关联,syncState解决游戏机制与动作的关联。后者多半是在服务器上进行处理,然后通过syncState将结果返回个客户端。
函数GetIns()完成状态根据输入的变更
private void GetIns()
{
if (actionState == PlayerActionState.DEFEND)
{
if (Input.GetKeyUp(KeyCode.Space) || !Input.GetKey(KeyCode.Space))
{
SetState(PlayerActionState.MOVE);
}
}
if (actionState != PlayerActionState.MOVE)
{
return;
}
if (syncState == PlayerActionState.MOVE)
{
isAttacking = false;
}
if (Input.GetKeyDown(KeyCode.Space))
{
SetState(PlayerActionState.DEFEND);
return;
}
if (Input.GetMouseButtonDown(0))
{
SetState(PlayerActionState.LEFT_ATK);
return;
}
if (Input.GetMouseButtonDown(1))
{
SetState(PlayerActionState.RIGHT_ATK);
return;
}
}
到这里我们基本解决了输入的指令和动作之间的关联。下一步,我们解决游戏机制导致的动作状态的变更,也就是被打要受伤。
首先我们要实现攻击的机制
private bool isAttacking;
void OnAttack()
{
if ((syncState != PlayerActionState.LEFT_ATK && syncState != PlayerActionState.RIGHT_ATK) || isAttacking)
{
return;
}
RaycastHit hit;
LayerMask mask = 1 << LayerMask.NameToLayer("player");
isAttacking = true;
if (Physics.Raycast(transform.position + new Vector3(0, 1f, 0), transform.forward, out hit,
atkDistance, mask))
{
CmdAttack(hit.collider.gameObject);
}
}
[Command]
void CmdAttack(GameObject target)
{
PlayerAction playerAction = target.GetComponent<PlayerAction>();
playerAction.Hurt(syncState);
}
当时我将OnAttack()放在update中判断,所以使用了一个isAttking保证一次输入只会攻击一次,其实可以将OnAttack()放到CmdSetState(int nState)调用,这样结构更加清晰,性能消耗也少。
比较关键的一点是,在OnAttack()函数开始部分,我使用的是syncState来进行当前攻击状态的判定,因为后续的攻击处理是在服务器端进行的,所以我们要保证客户端和服务器之间攻击状态的同步。
攻击的判断使用了射线来判断,当输入攻击指令后,用射线判断player攻击是否成功。如果成功了,就让服务器执行CmdAttack(GameObject target)函数,并且将被攻击的gameobject作为参数传给服务器。服务器会被攻击的gameobject的playerAction.Hurt(syncState)函数,并且将当前player的攻击方式传给被攻击的gameobject。
Hurt函数只有服务器才可以调用,因为playerhealth中只有在服务器才可以修改血量,所以在服务器上调用Hurt(),并且根据攻击方式,和当前的状态计算扣除的血量,并且转移到对应的受伤状态。
private PlayerHealth playerHealth;
public void Hurt(int attackState)
{
if (!isServer)
{
return;
}
float damageAmount = hurtDamage;
if (actionState == PlayerActionState.DEFEND)
{
switch (attackState)
{
case PlayerActionState.LEFT_ATK:
syncState = PlayerActionState.LEFT_HURT_DEF;
break;
case PlayerActionState.RIGHT_ATK:
syncState = PlayerActionState.RIGHT_HURT_DEF;
break;
default:
return;
}
damageAmount -= defAmount;
}
else
{
switch (attackState)
{
case PlayerActionState.LEFT_ATK:
syncState = PlayerActionState.LEFT_HURT;
break;
case PlayerActionState.RIGHT_ATK:
syncState = PlayerActionState.RIGHT_HURT;
break;
default:
return;
}
}
if (playerHealth.healthy - damageAmount <= 0)
{
Die();
}
playerHealth.TakeDamage(damageAmount);
}
void Die()
{
syncState = PlayerActionState.DIE;
}
以客户端a作为攻击者,客户端b作为被攻击者,整个攻击的步骤就是:
- 客户端a输入指令攻击(客户端a)
- 客户端a进入攻击状态[actionState](客户端a)
- 客户端a进行攻击检测[syncState](客户端a)
- 攻击检测成功,服务器执行攻击处理(服务器)
- 服务器b执行受伤指令,根据服务器a的状态[syncState]进行受伤(服务器)
- 服务器b扣血,同步血量和受伤状态[syncState]给各个客户端(服务器)
- 客户端b根据血量扣血,并且根据受伤状态[syncState]更新动作状态。(客户端b)
之前提到的syncState和actionState的不同作用这里也有体现。
这里基本就完成了playerAction的函数。
剩下的就是在update里面的一些小操作
void Update ()
{
if (!isLocalPlayer)
{
if (isServer)
{
actionState = syncState;
}
return;
}
GetIns();
OnAttack();
}
但是我们状态转移图中还有一些没有条件的状态转移我们仍然没有实现,这些是在player动画中实现的,因为我们要确保播放完完整的动画,才能进行下一动作的转移。
八、player动画
之前也说过了,我使用的是旧动画系统,所以动画方面不会说太多。主要说明如何实现无条件的状态转移。
核心代码就是这个。
private void Move()
{
playerAction.SetState(PlayerActionState.MOVE);
if (anim.IsPlaying(PlayerAnimationName.Defend_End))
anim.Stop(PlayerAnimationName.Defend_End);
state = State.MOVE;
}
实际上就是在每个动画播放完毕后调用这一函数,就可以将状态转回Move,进而可以接受新的指令进行动作。
九、联网FP摄像机
之前我们也讲过了FP摄像机如何实现,但是在联网游戏中,每个player的prefab的layer层都是一样的,我们不希望只能看到对手的两个拳套,所以我们需要动态设定非本地player的layer。
public class SetFPSCamera : {
public GameObject fpsCameraPrefab;
public Transform rootIgnore;
public Transform leftHand;
public Transform rightHand;
public override void OnStartLocalPlayer()
{
if (Camera.main != null)
{
GameObject.Destroy(Camera.main);
}
GameObject fpsCamera = Instantiate(fpsCameraPrefab) as GameObject;
fpsCamera.transform.parent = this.transform;
fpsCamera.transform.localPosition = new Vector3(0, 2.5f, 0);
fpsCamera.transform.localRotation = Quaternion.identity;
fpsCamera.tag = "MainCamera";
foreach(Transform t in rootIgnore)
{
t.gameObject.layer = LayerMask.NameToLayer("Ignore Camera");
}
foreach(Transform t in leftHand)
{
t.gameObject.layer = LayerMask.NameToLayer("Guante");
}
foreach (Transform t in rightHand)
{
t.gameObject.layer = LayerMask.NameToLayer("Guante");
}
}
}
最后我们将代码都挂载到player身上,就可以完成游戏了。
完整的代码和资源链接
https://pan.baidu.com/s/1pL4oTwZ