Unity3D教學 開發簡單版第一人稱射擊遊戲 可以多人聯機(附原始碼)
阿新 • • 發佈:2019-01-02
簡介:
這一篇文章主要是和大家分享如何製作一個屬於自己的“第一人稱射擊遊戲”,而且是要可以多人聯機的。這個遊戲屬於比簡單的,大神可以直接無視,如果有做錯的地方請大家多多指點,我也是剛學如何做遊戲。程式碼是用C#編寫,主要實現的功能有三個:第一人稱移動控制、角色控制(如射擊)、TCP服務端和客戶端。我將專案命名為FPS。
遊戲運作流程:
服務端
1. 建立服務端
2. 從各個客戶端接收資料,然後廣播給所有客戶
客戶端
1. 連線至服務端
2. 從服務端接收到其他客戶的資料(比如當前位置,是否有開槍等等),然後更新到遊戲上
3. 傳送自己當前在遊戲的狀態給服務端
聯機效果圖:
服務端
客戶端
完整原始碼與已編譯好的遊戲程式:
C#指令碼簡介:
1. FirstPersonControl.cs
這個指令碼是用來做第一人稱控制的
using System.Collections; using System.Collections.Generic; using UnityEngine; public class FirstPersonControl : MonoBehaviour { // Use this for initialization void Start () { // 獲取攝像頭物件 mCamera = transform.Find("Main Camera"); // 獲取右手物件 mRightHand = transform.Find("RightHand"); // 獲取腳步聲播放組建 mAudio = transform.GetComponent<AudioSource>(); } // Update is called once per frame void Update() { if (UpdateMovement()) { PlayStepSound(); } else { StopPlayStepSound(); } UpdateLookAt(); } // 第一人稱控制器的常量與變數 private Transform mCamera; private Transform mRightHand; private AudioSource mAudio; public float mMoveSpeed = 8; // 物體移動速度 public float mMouseSensitivity = 5; // 滑鼠旋轉的敏感度 public float mMinimumX = 325; // 向下望的最大角度 public float mMaximumX = 45; // 向上望的最大角度 public float mMinimumY = -360; // 向左望的最大角度 public float mMaximumY = 360; // 向右望的最大角度 private Vector3 _curRotation = new Vector3(0,0,0); // 當前旋轉角度 // 更新移動位置 private bool UpdateMovement() { float distance = mMoveSpeed * Time.deltaTime; // 移動距離 // 前 if (Input.GetKey(KeyCode.W)) { float x = Mathf.Sin(transform.eulerAngles.y * Mathf.Deg2Rad) * distance; float z = Mathf.Cos(transform.eulerAngles.y * Mathf.Deg2Rad) * distance; transform.Translate(new Vector3(x,0,z), Space.World); return true; } // 後 if (Input.GetKey(KeyCode.S)) { float x = Mathf.Sin(transform.eulerAngles.y * Mathf.Deg2Rad) * distance; float z = Mathf.Cos(transform.eulerAngles.y * Mathf.Deg2Rad) * distance; transform.Translate(new Vector3(-x, 0, -z), Space.World); return true; } // 左 if (Input.GetKey(KeyCode.A)) { transform.Translate(new Vector3(-distance, 0, 0)); return true; } // 右 if (Input.GetKey(KeyCode.D)) { transform.Translate(new Vector3(distance, 0, 0)); return true; } return false; } // 更新攝像頭指向位置 private void UpdateLookAt() { // 左右旋轉 _curRotation.y = _curRotation.y + Input.GetAxis("Mouse X") * mMouseSensitivity; _curRotation.y = Mathf.Clamp(_curRotation.y, mMinimumY, mMaximumY); // 設定身體 Vector3 rotation = transform.eulerAngles; rotation.y = _curRotation.y; transform.eulerAngles = rotation; // 上下旋轉 _curRotation.x = _curRotation.x - Input.GetAxis("Mouse Y") * mMouseSensitivity; _curRotation.x = Mathf.Clamp(_curRotation.x, mMinimumX, mMaximumX); // 設定攝像頭 mCamera.localEulerAngles = new Vector3(_curRotation.x, 0, 0); // 設定右手 rotation = mRightHand.eulerAngles; rotation.x = _curRotation.x; mRightHand.eulerAngles = rotation; } // 播放腳步聲 private void PlayStepSound() { if (!mAudio.isPlaying) { mAudio.Play(); } } // 停止播放聲音 private void StopPlayStepSound() { if (mAudio.isPlaying) { mAudio.Stop(); } } }
2. Character.cs
角色控制指令碼,主要是用來控制開槍等角色動作
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { // Use this for initialization void Start() { // 獲取攝像頭物件 mCamera = transform.Find("Main Camera"); // 獲取右手物件 mRightHand = transform.Find("RightHand"); // 獲取槍聲播放元件 mGunAudio = transform.Find("RightHand/Pistol").GetComponent<AudioSource>(); // 獲取火花效果 mFireEffect = transform.Find("RightHand/Pistol/FireEffect").GetComponent<ParticleSystem>(); // 獲取網路元件 mNetwork = transform.GetComponent<Network>(); } // Update is called once per frame void Update() { UpdateFire(); // 傳送當前狀態到服務端,然後服務端就會轉發給其他客戶 mNetwork.SendStatus(transform.position, transform.eulerAngles, mCamera.eulerAngles, mRightHand.eulerAngles, _isShooted, _hp); // 處理伺服器發過來的資料包,資料包裡裝著其他客戶的資訊 ProcessPackage(); } private Transform mCamera; private Transform mRightHand; private AudioSource mGunAudio; public GameObject mPiece; // 開槍後撞擊產生的碎片 private ParticleSystem mFireEffect; // 開槍後的火花 private bool _isShooted; // 判斷是否開了槍 private Network mNetwork; // 網路元件 public GameObject mEnemyCharacter; // 其他客戶的例項 private Hashtable _htEnemies = new Hashtable(); // 其他客戶的控制指令碼 // 開槍 private void UpdateFire() { if (Input.GetButtonUp("Fire1")) { // 射擊音效與畫面 PlayShotSound(); // 播放火花效果 PlayFireEffect(); // 判斷射擊位置 RaycastHit hit; if (Physics.Raycast(mCamera.position, mCamera.forward, out hit, 100)) { // 被槍擊中的地方會有碎片彈出 DrawPieces(hit); } // 設定開槍判斷 _isShooted = true; } else { // 設定開槍判斷 _isShooted = false; } } // 播放槍聲 private void PlayShotSound() { mGunAudio.PlayOneShot(mGunAudio.clip); } // 畫碎片 private void DrawPieces(RaycastHit hit) { for (int i = 0; i < 5; ++i) { GameObject p = Transform.Instantiate(mPiece); // 碎片撞擊到物體後的反彈位置 Vector3 fwd = mCamera.forward * -1; p.transform.position = hit.point; p.GetComponent<Rigidbody>().AddForce(fwd * 100); // 0.3秒後刪除 Destroy(p, 0.3f); } } // 播放火花效果 private void PlayFireEffect() { mFireEffect.Play(); } // 人物變數 private int _hp = 100; // 受到傷害 public void GetHurt() { _hp -= 10; if (_hp <= 0) { // 復活 Revive(); } } // 復活 private void Revive() { _hp = 100; transform.position = new Vector3(0,1,0); } // 處理資料包 private void ProcessPackage() { Network.Package p; // 獲取資料包直到完畢 while (mNetwork.NextPackage(out p)) { // 確定不是本機,避免重複 if (mNetwork._id == p.id) { return; } // 獲取該客戶相對應的人物模組 if (!_htEnemies.Contains(p.id)) { AddEnemyCharacter(p.id); } // 更新客戶的人物模型狀態 EnemyCharacter ec = (EnemyCharacter)_htEnemies[p.id]; // 血量 ec.SetHP(p.hp); // 移動動作 ec.Move(p.pos.V3, p.rot.V3, p.cameraRot.V3, p.rightHandRot.V3); // 開槍 if (p.isShooted) { ec.Fire(); } } } // 增加客戶的人物模組 private EnemyCharacter AddEnemyCharacter(int id) { GameObject p = GameObject.Instantiate(mEnemyCharacter); EnemyCharacter ec = p.GetComponent<EnemyCharacter>(); // 修改ID ec.SetID(id); // 加入到雜湊表 _htEnemies.Add(id, ec); return ec; } // 刪除客戶的人物模組 private void RemoveEnemyCharacter(int id) { EnemyCharacter ec = (EnemyCharacter)_htEnemies[id]; ec.Destroy(); _htEnemies.Remove(id); } // 刪除所有客戶的人物模組 public void RemoveAllEnemyCharacter() { foreach (int id in _htEnemies.Keys) { EnemyCharacter ec = (EnemyCharacter)_htEnemies[id]; ec.Destroy(); } _htEnemies.Clear(); } }
3.Network.cs
網路元件,用來建立服務端或客戶端。服務端負責廣播從各個客戶端接收的資料給所有客戶。客戶端則是接收從服務端接收其他客戶的資料。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Net;
using System.Net.Sockets;
public class Network : MonoBehaviour {
// Use this for initialization
void Start () {
// 獲取人物元件
mCharacter = transform.GetComponent<Character>();
// 資料包大小
_packageSize = PackageSize();
}
// Update is called once per frame
void Update () {
}
// 網路設定UI
void OnGUI() {
GUI.Box (new Rect(0,0,800,60),"網路設定");
if (_connected) {
GUI.Label (new Rect(10,25,100,25), _isServer ? "已建立服務端":"已連線服務端");
} else if (!_connected) {
GUI.Label (new Rect(10,25,100,25), "未連線");
}
GUI.Label (new Rect(130,25,20,25), "IP:");
GUI.Label (new Rect(270,25,40,25), "埠:");
GUI.Label (new Rect(380,25,40,25), "ID:");
if (!_connected && !_isServer) {
_ip = GUI.TextField (new Rect (150, 25, 100, 25), _ip, 100);
_port = System.Convert.ToInt32 (GUI.TextField (new Rect (310, 25, 50, 25), _port.ToString (), 100));
_id = System.Convert.ToInt32 (GUI.TextField (new Rect (420, 25, 100, 25), _id.ToString(), 100));
} else {
GUI.TextField (new Rect (150, 25, 100, 25), _ip, 100);
GUI.TextField (new Rect (310, 25, 50, 25), _port.ToString (), 100);
GUI.TextField (new Rect (420, 25, 100, 25), _id.ToString(), 100);
}
if (!_connected && !_isServer) {
if (GUI.Button(new Rect(540,25,100,25), "開啟服務端")) {
StartServer();
ConnectServer();
}
if (GUI.Button(new Rect(660,25,100,25), "連線至服務端")) {
ConnectServer();
}
} else {
if (_isServer) {
if (GUI.Button(new Rect(540,25,100,25), "關閉服務端")) {
StopServer();
}
} else if (_connected) {
if (GUI.Button(new Rect(540,25,100,25), "取消連線")) {
DisconnectServer();
}
}
}
}
// 服務端和客戶端的共有變數
private Character mCharacter; // 人物元件
private bool _connected=false; // 判斷是否已經建立連線或開啟服務端
private bool _isServer=false; // 判斷本程式建立的是服務端還是客戶端
private string _ip = "127.0.0.1"; // 主機IP
private int _port = 18000; // 埠
public int _id = 1 ; // 人物id
List<Package> _packages=new List<Package>(); // 資料包
private int _packageSize; // 資料包大小
// 資料包
[Serializable]
public struct Package {
public int id;
public Vector3Serializer pos; // 人物位置
public Vector3Serializer rot; // 人物旋轉角度
public Vector3Serializer cameraRot; // 攝像頭旋轉角度
public Vector3Serializer rightHandRot; // 右手旋轉角度
public bool isShooted; // 判斷是否有開槍
public int hp; // 血量
}
// 獲取包大小
private int PackageSize() {
Package p = new Package();
byte[] b;
Serialize(p, out b);
return b.Length;
}
// 可序列化的Vector3
[Serializable]
public struct Vector3Serializer {
public float x;
public float y;
public float z;
public void Fill(Vector3 v3) {
x = v3.x;
y = v3.y;
z = v3.z;
}
public Vector3 V3
{ get { return new Vector3(x, y, z); } }
}
// 序列化資料包
public bool Serialize(object obj, out byte[] result) {
bool ret = false;
result = null;
try {
MemoryStream ms = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ms, obj);
result = ms.ToArray();
ret = true;
} catch (Exception e) {
ret = false;
Debug.Log(e.Message);
}
return ret;
}
// 反序列化資料包
public bool Deserialize(byte[] data,out object result) {
bool ret = false;
result = new object();
try {
MemoryStream ms = new MemoryStream(data);
BinaryFormatter bf = new BinaryFormatter();
result = bf.Deserialize(ms);
ret = true;
} catch (Exception e) {
ret = false;
Debug.Log(e.Message);
}
return ret;
}
// 服務端變數
TcpListener _listener;
// 儲存已連線客戶的結構體
private struct Client {
public TcpClient client;
public byte[] buffer; // 接收緩衝區
public List<byte> pendingData; // 還未處理的資料
}
// 客戶列表
List<Client> _clients = new List<Client>();
// 開啟服務端
private void StartServer() {
try {
_listener = new TcpListener(IPAddress.Any, _port);
_listener.Start();
_listener.BeginAcceptSocket(HandleAccepted, _listener);
_isServer = true;
} catch (Exception e) {
Debug.Log(e.Message);
}
}
// 停止停止服務端
private void StopServer() {
try {
_listener.Stop();
// 清空客戶列表
lock (_clients) {
foreach (Client c in _clients) {
RemoveClient(c);
}
_clients.Clear();
}
// 清空資料包
lock (_packages) {
_packages.Clear();
}
_isServer = false;
} catch (Exception e) {
Debug.Log(e.Message);
}
}
// 處理客戶端連線的回撥函式
private void HandleAccepted(IAsyncResult iar) {
if (_isServer) {
TcpClient tcpClient = _listener.EndAcceptTcpClient(iar);
Client client = new Client();
client.client = tcpClient;
client.buffer = new byte[tcpClient.ReceiveBufferSize];
client.pendingData = new List<byte>();
// 把客戶加入到客戶列表
lock (_clients) {
AddClient(client);
}
// 開始非同步接收從客戶端收到的資料
tcpClient.GetStream().BeginRead(
client.buffer,
0,
client.buffer.Length,
HandleClientDataReceived,
client);
// 開始非同步接收連線
_listener.BeginAcceptSocket(HandleAccepted, _listener);
}
}
// 從客戶端接收資料的回撥函式
private void HandleClientDataReceived(IAsyncResult iar) {
try {
if (_isServer) {
Client client = (Client)iar.AsyncState;
NetworkStream ns = client.client.GetStream();
int bytesRead = ns.EndRead(iar);
// 連線中斷
if (bytesRead == 0) {
lock (_clients) {
_clients.Remove(client);
}
return;
}
// 儲存資料
for (int i=0; i<bytesRead; ++i) {
client.pendingData.Add(client.buffer[i]);
}
// 把資料解析成包
while (client.pendingData.Count >= _packageSize) {
byte[] bp = client.pendingData.GetRange(0, _packageSize).ToArray();
client.pendingData.RemoveRange(0, _packageSize);
// 把資料包分發給所有客戶
lock(_clients) {
foreach (Client c in _clients) {
c.client.GetStream().Write(bp, 0, _packageSize);
c.client.GetStream().Flush();
}
}
}
// 開始非同步接收從客戶端收到的資料
client.client.GetStream().BeginRead(
client.buffer,
0,
client.buffer.Length,
HandleClientDataReceived,
client);
}
} catch (Exception e) {
Debug.Log(e.Message);
}
}
// 加入客戶
private void AddClient(Client c) {
_clients.Add(c);
}
// 刪除客戶
private void RemoveClient(Client c) {
c.client.Client.Disconnect(false);
}
// 客戶端變數
Client _server;
// 連線至服務端
private void ConnectServer() {
try {
TcpClient tcpServer = new TcpClient();
tcpServer.Connect(_ip, _port);
_server = new Client();
_server.client = tcpServer;
_server.buffer = new byte[tcpServer.ReceiveBufferSize];
_server.pendingData = new List<byte>();
// 非同步接收服務端資料
tcpServer.GetStream().BeginRead(
_server.buffer,
0,
tcpServer.ReceiveBufferSize,
HandleServerDataReceived,
_server);
_connected = true;
} catch (Exception e) {
Debug.Log(e.Message);
}
}
// 從服務端斷開
private void DisconnectServer() {
try {
lock (_server.client) {
_server.client.Client.Close();
}
// 清空資料包
lock (_packages) {
_packages.Clear();
}
// 刪除所有客戶人物模型
mCharacter.RemoveAllEnemyCharacter();
_connected = false;
} catch (Exception e) {
Debug.Log(e.Message);
}
}
// 從服務端讀取資料的回撥函式
private void HandleServerDataReceived(IAsyncResult iar) {
if (_connected) {
Client server = (Client)iar.AsyncState;
NetworkStream ns = server.client.GetStream();
int bytesRead = ns.EndRead(iar);
// 連線中斷
if (bytesRead == 0) {
DisconnectServer();
return;
}
// 儲存資料
for (int i=0; i<bytesRead; ++i) {
server.pendingData.Add(server.buffer[i]);
}
// 把資料解析成包
while (server.pendingData.Count >= _packageSize) {
byte[] bp = server.pendingData.GetRange(0, _packageSize).ToArray();
server.pendingData.RemoveRange(0,_packageSize);
// 把資料轉換成包然後再儲存包列表
object obj;
Deserialize(bp, out obj);
lock (_packages) {
_packages.Add((Package)obj);
}
}
// 非同步接收服務端資料
server.client.GetStream().BeginRead(
server.buffer,
0,
server.client.ReceiveBufferSize,
HandleServerDataReceived,
server);
}
}
// 傳送自己的當前的狀態包給服務端
public void SendStatus(Vector3 pos, Vector3 rot,Vector3 cameraRot,
Vector3 rightHandRot, bool isShooted, int hp) {
try {
if (_connected) {
Package p = new Package();
p.id = _id;
p.pos.Fill(pos);
p.rot.Fill(rot);
p.cameraRot.Fill(cameraRot);
p.rightHandRot.Fill(rightHandRot);
p.isShooted = isShooted;
p.hp = hp;
// 傳送包到服務端
byte[] bp;
Serialize(p, out bp);
lock (_server.client) {
_server.client.GetStream().Write(bp, 0, _packageSize);
_server.client.GetStream().Flush();
}
}
} catch (Exception e) {
Debug.Log(e.Message);
// 斷開服務端
DisconnectServer();
}
}
// 獲取包
public bool NextPackage(out Package p) {
lock (_packages) {
if (_packages.Count == 0) {
p = new Package();
return false;
}
p = _packages[0];
_packages.RemoveAt(0);
}
return true;
}
}
4.EnemyCharacter.cs
這個指令碼負責控制其他客戶的行為,比如從服務端接收到其他客戶移動或開槍的資料,就要用這個指令碼來更新其他客戶的當前行為
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyCharacter : MonoBehaviour {
// Use this for initialization
void Start () {
// 獲取本機玩家的物件
mCharacter = GameObject.Find("Character").transform;
mCharacterComponent = mCharacter.GetComponent<Character>();
// 獲取攝像頭物件
mCamera = transform.Find("Camera");
// 獲取右手物件
mRightHand = transform.Find("RightHand");
// 獲取槍聲播放元件
mGunAudio = transform.Find("RightHand/Pistol").GetComponent<AudioSource>();
// 獲取火花效果
mFireEffect = transform.Find("RightHand/Pistol/FireEffect").GetComponent<ParticleSystem>();
// 獲取腳步聲播放元件
mAudio = transform.GetComponent<AudioSource>();
// 顯示血量和ID的元件
txID = transform.Find ("ID");
txIDText = transform.Find ("ID").GetComponent<TextMesh> ();
txHP = transform.Find ("HP");
txHPText = transform.Find ("HP").GetComponent<TextMesh> ();
}
// Update is called once per frame
void Update () {
// 摧毀物件
if (isDestroy) {
Destroy(gameObject);
}
// 更新物件屬性
UpdataProperties();
}
private Transform mCharacter;
private Character mCharacterComponent;
private Transform mCamera;
private Transform mRightHand;
private AudioSource mGunAudio;
private AudioSource mAudio;
private ParticleSystem mFireEffect; // 開槍後的火花
public GameObject mPiece; // 開槍後撞擊產生的碎片
private bool isDestroy = false;
// 銷燬角色
public void Destroy() {
isDestroy = true;
}
// 角色移動動作
public void Move(Vector3 pos, Vector3 rot, Vector3 cameraRot, Vector3 rightHandRot) {
if (pos != transform.position) {
transform.position = pos;
PlayStepSound();
} else {
StopPlayStepSound();
}
mCamera.eulerAngles = cameraRot;
transform.eulerAngles = rot;
mRightHand.eulerAngles = rightHandRot;
}
// 播放腳步聲
private void PlayStepSound() {
if (!mAudio.isPlaying) {
mAudio.Play();
}
}
// 停止播放聲音
private void StopPlayStepSound() {
if (mAudio.isPlaying) {
mAudio.Stop();
}
}
// 開槍
public void Fire() {
// 射擊音效與畫面
PlayShotSound();
// 播放火花效果
PlayFireEffect();
// 判斷射擊位置
RaycastHit hit;
if (Physics.Raycast(mCamera.position, mCamera.forward, out hit, 100)) {
// 被槍擊中的地方會有碎片彈出
DrawPieces(hit);
// 判斷本機玩家是否中槍如果是就減
print(hit.collider.name);
if (hit.collider.name == mCharacter.name) {
mCharacterComponent.GetHurt();
}
}
}
// 播放槍聲
private void PlayShotSound() {
mGunAudio.PlayOneShot(mGunAudio.clip);
}
// 畫碎片
private void DrawPieces(RaycastHit hit) {
for (int i = 0; i < 5; ++i) {
GameObject p = Transform.Instantiate(mPiece);
// 碎片撞擊到物體後的反彈位置
Vector3 fwd = mCamera.forward * -1;
p.transform.position = hit.point;
p.GetComponent<Rigidbody>().AddForce(fwd * 100);
// 0.3秒後刪除
Destroy(p, 0.3f);
}
}
// 播放火花效果
private void PlayFireEffect() {
mFireEffect.Play();
}
// 人物變數
private int _id = 1;
private int _hp = 100;
private Transform txID;
private TextMesh txIDText;
private Transform txHP;
private TextMesh txHPText;
// 角色id
public void SetID(int id) {
_id = id;
}
// 角色血量
public void SetHP(int hp) {
_hp = hp;
}
// 更新角色變數/屬性
private void UpdataProperties() {
// 顯示血量和ID
txIDText.text = "ID:"+_id.ToString();
txHPText.text = "HP:"+_hp.ToString();
// 血量和ID的方向,面向著本機玩家
txID.rotation = mCharacter.rotation;
txHP.rotation = mCharacter.rotation;
}
}