1第献、介紹
《Unity網(wǎng)絡(luò)游戲?qū)崙?zhàn)》的第三章節(jié)是做一個(gè)亂斗小游戲。實(shí)現(xiàn)的功能是玩家進(jìn)入到一個(gè)場(chǎng)景兔港,右鍵點(diǎn)擊地面移動(dòng)庸毫,左鍵點(diǎn)擊為攻擊,擊中其他玩家就扣血衫樊,血量為0就死亡飒赃。
2蜗帜、客戶端
本地玩家的控制腳本CtrlHuman和同步其他玩家的SyncHuman都繼承于BaseHuman涣雕,玩家的控制邏輯都寫在這三個(gè)腳本里面。網(wǎng)絡(luò)消息的發(fā)送和接收處理难礼,則是用了一個(gè)靜態(tài)類NetManager和NetWorkManager臀栈。NetWorkManager可以掛在在游戲的任何一個(gè)物體中蔫慧。
貼上代碼,和書上的源碼有些許不同权薯。
BaseHuman.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 作為CtrlHuman和SyncHuman的基類姑躲,實(shí)現(xiàn)共同的功能
public class BaseHuman : MonoBehaviour
{
// 是否正在移動(dòng)
internal bool isMoving = false;
// 移動(dòng)目標(biāo)點(diǎn)
private Vector3 targetPosition;
// 移動(dòng)速度
public float speed = 1.2f;
// 動(dòng)畫組件
private Animator animator;
// 是否正在攻擊
internal bool isAttacking = false;
internal float attackTime = float.MinValue;
// 描述
public string desc = "";
// 移動(dòng) -- 動(dòng)作
public void MoveTo(Vector3 pos)
{
targetPosition = pos;
isMoving = true;
animator.SetBool("isMoving", true);
}
// 移動(dòng)Update,每一幀的移動(dòng)
public void MoveUpdate()
{
if (!isMoving)
return;
if (isAttacking)
{
isAttacking = false;
animator.SetBool("isAttacking", false);
}
// 角色當(dāng)前的位置
Vector3 pos = transform.position;
// 移動(dòng)到targetPosition
transform.position = Vector3.MoveTowards(pos, targetPosition, speed * Time.deltaTime);
// 用transform.Translate也可以實(shí)現(xiàn)運(yùn)動(dòng)的效果 -- 但是要在space.world中
// transform.Translate(transform.forward * Time.deltaTime, Space.World);
// 看向目標(biāo)位置
transform.LookAt(targetPosition);
// 當(dāng)距離目標(biāo)小于0.1時(shí)盟蚣,停下
if (Vector3.Distance(transform.position, targetPosition) < 0.1f)
{
isMoving = false;
animator.SetBool("isMoving", false);
}
}
// 攻擊Attack -- 動(dòng)作
public void Attack()
{
isAttacking = true;
attackTime = Time.time;
animator.SetBool("isAttacking", true);
}
// 攻擊Attack Update --- 每一幀更新(判斷一次attack是否結(jié)束)
public void AttackUpdate()
{
if (!isAttacking)
return;
if (Time.time - attackTime < 1.2f)
return;
isAttacking = false;
animator.SetBool("isAttacking", false);
}
// Start is called before the first frame update
internal void Start()
{
animator = GetComponent<Animator>();
}
// Update is called once per frame
internal void Update()
{
MoveUpdate();
AttackUpdate();
}
}
CtrlHuman.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CtrlHuman : BaseHuman
{
// Start is called before the first frame update
new void Start()
{
base.Start();
}
// Update is called once per frame
new void Update()
{
base.Update();
if(Input.GetMouseButtonDown(1))
{
// 點(diǎn)擊鼠標(biāo)右鍵
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// Debug.DrawRay(ray.origin, ray.direction * 10, Color.yellow);
RaycastHit hit;
Physics.Raycast(ray, out hit);
// 點(diǎn)擊地板 移動(dòng)
if (hit.collider.tag == "Enviroment")
{
MoveTo(hit.point);
transform.LookAt(hit.point);
}
// 組裝Move協(xié)議
string sendStr = "Move|";
sendStr += desc + ",";
sendStr += hit.point.x + ",";
sendStr += hit.point.y + ",";
sendStr += hit.point.z + ",";
sendStr += transform.eulerAngles.y;
NetManager.Send(sendStr);
}
if(Input.GetMouseButtonDown(0))
{
if (isMoving || isAttacking)
return;
// 點(diǎn)擊鼠標(biāo)左鍵黍析,攻擊
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
transform.LookAt(hit.point);
Attack();
// 組裝協(xié)議
string sendStr = "Attack|";
sendStr += desc + ",";
sendStr += transform.eulerAngles.y;
NetManager.Send(sendStr);
// 攻擊判定 -- Hit協(xié)議(客戶端不需要處理)
// 線段起點(diǎn)
Vector3 startPoint = transform.position + 0.5f * Vector3.up;
// 線段終點(diǎn)
Vector3 endPoint = startPoint + transform.forward * 20.0f;
// 檢測(cè)是否擊中敵人
if(Physics.Linecast(startPoint, endPoint, out hit))
{
GameObject gobj = hit.collider.gameObject;
if (gobj == gameObject)
return;
SyncHuman human = gobj.GetComponent<SyncHuman>();
if (human == null)
return;
// 組裝協(xié)議 -- 服務(wù)器判斷誰打中了誰
sendStr = "Hit|";
sendStr += desc + ",";
sendStr += human.desc;
NetManager.Send(sendStr);
}
}
}
}
SyncHuman.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SyncHuman : BaseHuman
{
// Start is called before the first frame update
new void Start()
{
base.Start();
}
// Update is called once per frame
new void Update()
{
base.Update();
}
public void SyncAttack(float euly)
{
transform.eulerAngles = new Vector3(0, euly, 0);
Attack();
}
}
NetManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using System;
public static class NetManager
{
static Socket socket;
static int buffsize = 1024;
static byte[] recvBuff = new byte[buffsize];
// 監(jiān)聽協(xié)議的委托類型
public delegate void MsgListener(string str);
private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();
static List<string> msgList = new List<string>();
// 添加監(jiān)聽
public static void AddListener(string msgName, MsgListener listener)
{
listeners.Add(msgName, listener);
}
// 獲取描述 --- 本地玩家的ip - 端口
public static string GetDesc()
{
if (socket == null)
return "";
if (!socket.Connected)
return "";
return socket.LocalEndPoint.ToString();
}
// 連接服務(wù)器
public static void Connect(string ip, int port)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ip, port);
socket.BeginReceive(recvBuff, 0, buffsize, SocketFlags.None, ReceiveCallBack, socket);
}
// Receive Call Back
private static void ReceiveCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
int count = socket.EndReceive(_ar);
string recvStr = System.Text.Encoding.UTF8.GetString(recvBuff, 0, count);
msgList.Add(recvStr);
socket.BeginReceive(recvBuff, 0, buffsize, SocketFlags.None, ReceiveCallBack, socket);
}
catch(SocketException ex)
{
Debug.Log("Socket Receive fail: " + ex.ToString());
}
}
public static void Send(string sendStr)
{
if (socket == null)
return;
if (!socket.Connected)
return;
byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, SendCallBack, socket);
}
// Send Call Back
private static void SendCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
}
catch(SocketException ex)
{
Debug.Log("Send failed, " + ex.ToString());
}
}
public static void ProcessMsg()
{
if (msgList.Count <= 0)
return;
string msgStr = msgList[0];
msgList.RemoveAt(0);
string[] splitmsg = msgStr.Split('|');
string msgName = splitmsg[0];
string msgbody = splitmsg[1];
if (listeners.ContainsKey(msgName))
listeners[msgName](msgbody);
}
}
NetWorkManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NetWorkManager : MonoBehaviour
{
public GameObject humanPrefab;
// 人物列表 -- 本地玩家的控制組件:myHuman;其他玩家列表:otherHumans
public BaseHuman myHuman;
public Dictionary<string, BaseHuman> otherHuman = new Dictionary<string, BaseHuman>();
void Start()
{
NetManager.AddListener("Enter", OnEnter);
NetManager.AddListener("List", OnList);
NetManager.AddListener("Move", OnMove);
NetManager.AddListener("Leave", OnLeave);
NetManager.AddListener("Attack", OnAttack);
NetManager.AddListener("Die", OnDie);
NetManager.Connect("127.0.0.1", 8888);
// 生成角色
GameObject gobj = Instantiate(humanPrefab);
float x = Random.Range(-10, 10);
float z = Random.Range(-10, 10);
gobj.transform.position = new Vector3(x, 0, z);
gobj.name = NetManager.GetDesc();
// 添加組件
myHuman = gobj.AddComponent<CtrlHuman>();
myHuman.desc = NetManager.GetDesc();
Debug.Log(myHuman.desc);
// 發(fā)送協(xié)議
Vector3 pos = myHuman.transform.position;
float euly = myHuman.transform.eulerAngles.y;
// 組裝消息
string sendStr = "Enter|";
// PS: 如果寫成 pos.x + ','; 則逗號(hào)不會(huì)加進(jìn)去
sendStr += myHuman.desc + ",";
sendStr += pos.x + ",";
sendStr += pos.y + ",";
sendStr += pos.z + ",";
sendStr += euly;
NetManager.Send(sendStr);
NetManager.Send("List|GetAllPlayerStates");
}
void OnEnter(string msgbody)
{
Debug.Log("OnEnter: " + msgbody);
// 解析參數(shù)
string[] splitmsg = msgbody.Split(',');
string desc = splitmsg[0];
float x = float.Parse(splitmsg[1]);
float y = float.Parse(splitmsg[2]);
float z = float.Parse(splitmsg[3]);
float euly = float.Parse(splitmsg[4]);
// 如果是自己進(jìn)入則不處理
if (desc == myHuman.desc)
return;
// 生成角色
GameObject gobj = Instantiate(humanPrefab);
gobj.name = desc;
gobj.transform.position = new Vector3(x, y, z);
gobj.transform.eulerAngles = new Vector3(0, euly, 0);
// 添加同步角色組件
BaseHuman human = gobj.AddComponent<SyncHuman>();
human.desc = desc;
// 用endpoint作為主鍵屎开,但是正常應(yīng)該是用username
otherHuman.Add(desc, human);
}
void OnList(string msgbody)
{
Debug.Log(msgbody);
// 解析參數(shù)
string[] splitmsg = msgbody.Split(',');
// count: 玩家個(gè)數(shù)
int count = splitmsg.Length / 6;
// 生成每一個(gè)玩家
for (int i = 0; i < count; i++)
{
// 解析每一個(gè)參數(shù)
string desc = splitmsg[i * 6 + 0];
float x = float.Parse(splitmsg[i * 6 + 1]);
float y = float.Parse(splitmsg[i * 6 + 2]);
float z = float.Parse(splitmsg[i * 6 + 3]);
float euly = float.Parse(splitmsg[i * 6 + 4]);
int hp = int.Parse(splitmsg[i * 6 + 5]);
// check is other player
if (desc == myHuman.desc)
continue;
// 創(chuàng)建其他玩家
GameObject gobj = Instantiate(humanPrefab);
gobj.name = desc;
gobj.transform.position = new Vector3(x, y, z);
gobj.transform.eulerAngles = new Vector3(0, euly, 0);
BaseHuman human = gobj.AddComponent<SyncHuman>();
human.desc = desc;
otherHuman.Add(human.desc, human);
}
}
void OnMove(string msgbody)
{
// 解析參數(shù)
string[] splitmsg = msgbody.Split(',');
string desc = splitmsg[0];
float x = float.Parse(splitmsg[1]);
float y = float.Parse(splitmsg[2]);
float z = float.Parse(splitmsg[3]);
// 同步移動(dòng)其他玩家
if (!otherHuman.ContainsKey(desc))
return;
otherHuman[desc].MoveTo(new Vector3(x, y, z));
}
void OnLeave(string msgbody)
{
// 解析參數(shù)
string desc = msgbody;
// 刪除離線玩家
if (!otherHuman.ContainsKey(desc))
return;
Destroy(otherHuman[desc].gameObject);
otherHuman.Remove(desc);
}
void OnAttack(string msgbody)
{
// 解析參數(shù)
string[] splitmsg = msgbody.Split(',');
string desc = splitmsg[0];
float euly = float.Parse(splitmsg[1]);
// 同步攻擊
if (!otherHuman.ContainsKey(desc))
return;
SyncHuman human = (SyncHuman)otherHuman[desc];
human.SyncAttack(euly);
}
void OnDie(string msgbody)
{
// 解析參數(shù)
string[] splitmsg = msgbody.Split(',');
string playerDead_desc = splitmsg[0];
string killer_desc = splitmsg[1];
if (playerDead_desc == myHuman.desc)
{
Debug.Log("You Dead! Game Over! Killer: " + killer_desc);
Destroy(myHuman.gameObject);
return;
}
// check
if(!otherHuman.ContainsKey(playerDead_desc))
return;
Debug.Log(killer_desc + " kill " + playerDead_desc);
Destroy(otherHuman[playerDead_desc].gameObject);
otherHuman.Remove(playerDead_desc);
}
void Update()
{
NetManager.ProcessMsg();
}
}
客戶端的實(shí)現(xiàn)就這樣橄仍,代碼也比較簡(jiǎn)單。
3、服務(wù)器
服務(wù)器用python編寫侮繁,MessageHandler為處理網(wǎng)絡(luò)消息的類;GameServer為服務(wù)器類如孝,用于接收消息宪哩;ClientStaes為客戶端狀態(tài)類。
ClientStates.py
class ClientStates(object):
def __init__(self, sock, address):
self.socket = sock
self.addr = address
self.recv_buff = []
self.hp = -100
self.pos_x = 0
self.pos_y = 0
self.pos_z = 0
self.euly = 0
MessageHandler.py
class MessageHandler(object):
def __init__(self, server):
self.protocols = {'Enter': self.enter_response,
'List': self.list_response,
'Move': self.move_response,
'Leave': self.leave_response,
'Attack': self.attack_response,
'Hit': self.hit_response}
self.game_server = server
def handle(self, msg, client_socket):
split_msg = msg.split('|')
msg_name = split_msg[0]
msg_body = split_msg[1]
self.protocols[msg_name](msg_body, client_socket)
def broadcast(self, send_msg):
for client in self.game_server.client_states.values():
client.socket.send(send_msg)
def enter_response(self, msg_body, client_socket):
# parse param
spilt_msg = msg_body.split(',')
x = spilt_msg[1]
y = spilt_msg[2]
z = spilt_msg[3]
euly = spilt_msg[4]
# update client states
self.game_server.client_states[client_socket].hp = 100
self.game_server.client_states[client_socket].pos_x = x
self.game_server.client_states[client_socket].pos_y = y
self.game_server.client_states[client_socket].pos_z = z
self.game_server.client_states[client_socket].euly = euly
# broadcast to all client
self.broadcast('Enter|' + msg_body)
def list_response(self, msg_body, client_socket):
# check the param
if msg_body != "GetAllPlayerStates":
print 'List param Error'
# send the player states to new Enter player
send_msg = "List|"
for client in self.game_server.client_states.values():
desc = client.addr[0] + ':' + str(client.addr[1])
send_msg += desc + ','
send_msg += client.pos_x + ','
send_msg += client.pos_y + ','
send_msg += client.pos_z + ','
send_msg += client.euly + ','
send_msg += str(client.hp) + ','
client_socket.send(send_msg.rstrip(','))
def move_response(self, msg_body, client_socket):
# parse the param
splitmsg = msg_body.split(',')
x = splitmsg[1]
y = splitmsg[2]
z = splitmsg[3]
euly = splitmsg[4]
# update player pos
self.game_server.client_states[client_socket].pos_x = x
self.game_server.client_states[client_socket].pos_y = y
self.game_server.client_states[client_socket].pos_z = z
self.game_server.client_states[client_socket].euly = euly
# broadcast
self.broadcast("Move|" + msg_body)
def leave_response(self, msg_body, client_socket):
# check param
if msg_body != "PlayerLeave":
print "in leave response, the param != Leave"
end_point = self.game_server.client_states[client_socket].addr[0] + ':' + \
str(self.game_server.client_states[client_socket].addr[1])
self.broadcast("Leave|" + end_point)
def attack_response(self, msg_body, client_socket):
if client_socket not in self.game_server.client_states:
print 'a client not in client_states, but it send a msg'
send_msg = "Attack|" + msg_body
self.broadcast(send_msg)
def hit_response(self, msg_body, client_socket):
# parse the param
split_msg = msg_body.split(',')
attack_addr = split_msg[0]
hit_addr = split_msg[1]
print 'hit_desc',hit_addr
# find the hit player
for client in self.game_server.client_states.values():
end_point = client.addr[0] + ':' + str(client.addr[1])
if end_point == hit_addr:
client.hp -= 25
if client.hp <= 0:
# player die, broadcast the die
self.broadcast("Die|" + hit_addr + ',' + attack_addr)
break
demo.py
import Chapter2.ClientStates as cs
import socket
import select
import MessageHandler as mh
class GameServer(object):
def __init__(self):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind(("127.0.0.1", 8888))
self.server.listen(5)
self.buffer_size = 1024
self.connect_socket = [self.server]
self.client_states = {}
self.handler = mh.MessageHandler(self)
def close_fd(self, fd):
self.handler.handle("Leave|PlayerLeave", fd)
self.connect_socket.remove(fd)
self.client_states.pop(fd)
fd.close()
def read_server_fd(self, fd):
# client connect
client_socket, client_address = fd.accept()
print client_address, 'connected'
self.connect_socket.append(client_socket)
self.client_states[client_socket] = cs.ClientStates(client_socket, client_address)
def read_client_fd(self, fd):
try:
data = fd.recv(self.buffer_size)
if data:
print 'receive data from: ', self.client_states[fd].addr, data
self.handler.handle(data, fd)
else:
self.close_fd(fd)
except socket.error:
self.close_fd(fd)
def run(self):
print "Server Start."
# Main Loop
while True:
# select mode
read_fds, write_fds, error_fds = select.select(self.connect_socket, [], [], 1)
for fd in read_fds:
if fd is self.server:
self.read_server_fd(fd)
else:
self.read_client_fd(fd)
server = GameServer()
server.run()
4第晰、結(jié)語
客戶端的收發(fā)消息用的異步socket锁孟,服務(wù)端的復(fù)用模型為select模型,性能較差茁瘦,并且客戶端和服務(wù)端都沒有處理粘包半包的問題品抽,在之后的章節(jié)中會(huì)處理這個(gè)問題。上述的客戶端代碼還存在一個(gè)問題甜熔,就是客戶端是用本機(jī)的ip-port作為自身的標(biāo)識(shí)圆恤,但是這個(gè)方法在局域網(wǎng)中是不行的,因?yàn)槌隽寺酚善髦蟮膇p會(huì)變腔稀,這樣會(huì)導(dǎo)致進(jìn)游戲的時(shí)候會(huì)創(chuàng)建兩個(gè)自己盆昙。修改方法只需將其改用username作為唯一標(biāo)識(shí)即可。