这一次,使用Unity3D的Network模块实现一个联网的小游戏,当然,资源有限,只是在本地模拟实现一个联网的小游戏,小游戏的名字为——坦克动荡。在开始实现这个游戏之前,我们需要了解一些关于Unet的基本知识,可参考博客——Unity5网络模块UNet介绍。
在了解了Unet的基础概念后,就可以开始做游戏了。
第一步,创建一个空对象,命名为NetworkManager,然后,为其添加NetworkManager组件和NetworkManager HUD组件,事实上,当你添加了NetworkManager HUD组件的时候,会自动添加NetworkManager组件。
NetworkManager是一个组件,它是完全使用HLAPI实现,用来管理网络多人游戏的状态。NetworkManager封装好了很多有用的功能到一个地方,使创建、运行和调试多人游戏尽可能简单。
NetworkManagerHUD属性面板显示网络在运行时的状态额外的信息。
NetworkManager的Inspector面板如下图:
第二步,构建游戏地图。在这里,我只构建了一幅地图,使用plane作为地面,用cube作为墙,然后为地面和墙增加点颜色,再根据自己想要的样子,将地图中的墙面堆砌好。构建好地图后,将地图拉入资源栏,制成预制。我完成的地图如下:
第三步,建立玩家预制和子弹预制。在这里,我只是简单地使用了一个cube和一个Capsule组成坦克,其中,Capsule用来标记坦克的前面,使用一个Sphere作为子弹,并为他们加上颜色,如下:
为了使玩家对象能够联网产生,需要给预制体加上NetworkIdentity组件【网络物体最基本的组件,客户端与服务器确认是否是一个物体(netID),也用来表示各个状态,如是否是服务器,是否是客户端,是否有权限,是否是本地玩家等】,并勾上LocalPlayerAuthority。然后,由于涉及对象的移动,需要给玩家对象加上NetworkTransform组件。则Player对象的组件如下:
同理,bullet预制也要加上NetworkIdentify组件和NetworkTransform组件,如下图:
在这里,为了子弹可以无损能量反弹,我创建了一个Physic Material,并将其Bounciness属性设置为1,然后将该Material添加到了bullet的Collider组件中,如下:
然后,回到NetworkManager,在Inspector面板中,找到NetworkManager组件里的Spawn Info,将Player预制体拖入Player Prefab槽中,并增添一个Registered Spawnable Prefab,将bullet预制体拖入槽中,如下:
我们可以看到,在Spawn Info中,有一个Player Spawn Method,在这里,我选了Random,表示玩家在开始点中随机选一个点出现,Round Robin则是按开始点的顺序出现。
现在,我们来开始设置开始点。创建一个空对象,命名为startPoint,然后,Add Component -> Network Start Position。将开始点拖到你想玩家对象开始出现的位置即可,在这里,我设置了六个开始点,如下:(为了方便观察,我将开始点的图标改成了蓝色。)
至此,场景设计部分就完成了。
首先是玩家的移动和开火,其中,开火要使用command命令,在服务端执行动作,并用Spawn同步到每一个玩家。脚本如下:
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
private float movementInputValue;
private float turnInputValue;
private float speed = 4f;//the moving speed
private Rigidbody m_rigidbody;
private float turnSpeed = 150f; //the rotating speed
private void Awake()
{
m_rigidbody = GetComponent();
}
//distinguish self from others
public override void OnStartLocalPlayer()
{
GetComponent().material.color = Color.red;
}
//fire
[Command]
void CmdFire()
{
// This [Command] code is run on the server!
// create the bullet object locally
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position + transform.forward * 0.7f,
Quaternion.identity);
bullet.GetComponent().velocity = transform.forward * 5;
// spawn the bullet on the clients
NetworkServer.Spawn(bullet);
// when the bullet is destroyed on the server it will automaticaly be destroyed on clients after 6s
Destroy(bullet, 6.0f);
}
void Update()
{
if (!isLocalPlayer)
return;
//get the movement value and rotation value
movementInputValue = Input.GetAxis("Vertical");
turnInputValue = Input.GetAxis("Horizontal");
if (Input.GetKeyDown(KeyCode.Space))
{
// Command function is called from the client, but invoked on the server
CmdFire();
}
}
//move and turn
private void FixedUpdate()
{
Move();
Turn();
}
private void Move()
{
Vector3 movement = transform.forward * movementInputValue * speed * Time.deltaTime;
m_rigidbody.MovePosition(m_rigidbody.position + movement);
}
private void Turn()
{
float turn = turnInputValue * turnSpeed * Time.deltaTime;
Quaternion turnRotation = Quaternion.Euler(0f, turn, 0f);
m_rigidbody.MoveRotation(m_rigidbody.rotation * turnRotation);
}
}
编写完后,将这个脚本挂载到Player预制上,然后将bullet预制拖拉到脚本的BulletPrefab上即可。
然后,再写一个Combat类,用来处理玩家的生命值和重生处理。
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100; //the max health
public AudioSource audio; //the audio when the player died
[SyncVar]
public int health = maxHealth; //the current health
//deal with the damage, this job belongs to the server
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
//if the player is dead
if (health <= 0)
{
health = maxHealth;
// called on the server, will be invoked on the clients
RpcRespawn();
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
//play the audio and reset the player
audio.Play();
transform.position = new Vector3(-8, 0.5f, -4);
}
}
}
将上述脚本挂载在Player预制中,同时,增加一个AudioSource组件,并将爆炸音效拖拉到AudioClip槽中。
上面既然说了要处理伤害,那么,就得有个血条,这里,弄了一个很简单的血条,将下面的脚本直接挂载到Player中即可。
using UnityEngine;
using System.Collections;
public class HealthBar : MonoBehaviour
{
GUIStyle healthStyle;
GUIStyle backStyle;
Combat combat;
void Awake()
{
combat = GetComponent();
}
void OnGUI()
{
InitStyles();
// Draw a Health Bar
Vector3 pos = Camera.main.WorldToScreenPoint(transform.position);
// draw health bar background
GUI.color = Color.grey;
GUI.backgroundColor = Color.grey;
GUI.Box(new Rect(pos.x - 26, Screen.height - pos.y + 20, Combat.maxHealth / 2, 7), ".", backStyle);
// draw health bar amount
GUI.color = Color.green;
GUI.backgroundColor = Color.green;
GUI.Box(new Rect(pos.x - 25, Screen.height - pos.y + 21, combat.health / 2, 5), ".", healthStyle);
}
void InitStyles()
{
if (healthStyle == null)
{
healthStyle = new GUIStyle(GUI.skin.box);
healthStyle.normal.background = MakeTex(2, 2, new Color(0f, 1f, 0f, 1.0f));
}
if (backStyle == null)
{
backStyle = new GUIStyle(GUI.skin.box);
backStyle.normal.background = MakeTex(2, 2, new Color(0f, 0f, 0f, 1.0f));
}
}
Texture2D MakeTex(int width, int height, Color col)
{
Color[] pix = new Color[width * height];
for (int i = 0; i < pix.Length; ++i)
{
pix[i] = col;
}
Texture2D result = new Texture2D(width, height);
result.SetPixels(pix);
result.Apply();
return result;
}
}
下面,就是处理子弹碰撞的问题,就子弹碰撞到玩家的时候,产生伤害,并销毁子弹即可。将下面的代码挂载到bullet预制体上即可。
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent();
if (hitPlayer != null)
{
// Subscribe and Publish model may be good here!
var combat = hit.GetComponent();
combat.TakeDamage(20);
Destroy(gameObject);
}
}
}
至此,代码部分就完成了。由于没有使用工厂模式之类的,整体的代码量还是比较少的,也比较容易理解。
然后,再给游戏增加一点儿背景音乐,在地图上增加一个AudioSource组件,将背景音乐拖入AudioClip中,并勾选PlayOnAwake和Loop即可,如下:
最后,就可以运行游戏了。先生成exe文件,操作为file->Build setting,或者file->Build&Run。然后,就可以点击exe文件运行游戏了。
在开始游戏的时候,第一个玩家必须建立服务器,即以Host模式启动,后面的玩家则以Client模式启动。
至此,这个非常简易的坦克动荡就完成了。当然,还有很多不足的地方,例如没有设置玩家的GUI,没有设置胜利和计分以及多幅地图等。但是,这一次主要是熟悉联网游戏的制作过程,其他的处理,等期末考完了,再加上去吧。。。
视频
Github地址