1、介绍
《Unity网络游戏实战》的第三章节是做一个乱斗小游戏。实现的功能是玩家进入到一个场景,右键点击地面移动,左键点击为攻击,击中其他玩家就扣血,血量为0就死亡。
2、客户端
本地玩家的控制脚本CtrlHuman和同步其他玩家的SyncHuman都继承于BaseHuman,玩家的控制逻辑都写在这三个脚本里面。网络消息的发送和接收处理,则是用了一个静态类NetManager和NetWorkManager。NetWorkManager可以挂在在游戏的任何一个物体中。
贴上代码,和书上的源码有些许不同。
BaseHuman.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 作为CtrlHuman和SyncHuman的基类,实现共同的功能
public class BaseHuman : MonoBehaviour
{
// 是否正在移动
internal bool isMoving = false;
// 移动目标点
private Vector3 targetPosition;
// 移动速度
public float speed = 1.2f;
// 动画组件
private Animator animator;
// 是否正在攻击
internal bool isAttacking = false;
internal float attackTime = float.MinValue;
// 描述
public string desc = "";
// 移动 -- 动作
public void MoveTo(Vector3 pos)
{
targetPosition = pos;
isMoving = true;
animator.SetBool("isMoving", true);
}
// 移动Update,每一帧的移动
public void MoveUpdate()
{
if (!isMoving)
return;
if (isAttacking)
{
isAttacking = false;
animator.SetBool("isAttacking", false);
}
// 角色当前的位置
Vector3 pos = transform.position;
// 移动到targetPosition
transform.position = Vector3.MoveTowards(pos, targetPosition, speed * Time.deltaTime);
// 用transform.Translate也可以实现运动的效果 -- 但是要在space.world中
// transform.Translate(transform.forward * Time.deltaTime, Space.World);
// 看向目标位置
transform.LookAt(targetPosition);
// 当距离目标小于0.1时,停下
if (Vector3.Distance(transform.position, targetPosition) < 0.1f)
{
isMoving = false;
animator.SetBool("isMoving", false);
}
}
// 攻击Attack -- 动作
public void Attack()
{
isAttacking = true;
attackTime = Time.time;
animator.SetBool("isAttacking", true);
}
// 攻击Attack Update --- 每一帧更新(判断一次attack是否结束)
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();
}
// 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))
{
// 点击鼠标右键
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// Debug.DrawRay(ray.origin, ray.direction * 10, Color.yellow);
RaycastHit hit;
Physics.Raycast(ray, out hit);
// 点击地板 移动
if (hit.collider.tag == "Enviroment")
{
MoveTo(hit.point);
transform.LookAt(hit.point);
}
// 组装Move协议
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;
// 点击鼠标左键,攻击
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
transform.LookAt(hit.point);
Attack();
// 组装协议
string sendStr = "Attack|";
sendStr += desc + ",";
sendStr += transform.eulerAngles.y;
NetManager.Send(sendStr);
// 攻击判定 -- Hit协议(客户端不需要处理)
// 线段起点
Vector3 startPoint = transform.position + 0.5f * Vector3.up;
// 线段终点
Vector3 endPoint = startPoint + transform.forward * 20.0f;
// 检测是否击中敌人
if(Physics.Linecast(startPoint, endPoint, out hit))
{
GameObject gobj = hit.collider.gameObject;
if (gobj == gameObject)
return;
SyncHuman human = gobj.GetComponent();
if (human == null)
return;
// 组装协议 -- 服务器判断谁打中了谁
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];
// 监听协议的委托类型
public delegate void MsgListener(string str);
private static Dictionary listeners = new Dictionary();
static List msgList = new List();
// 添加监听
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();
}
// 连接服务器
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 otherHuman = new Dictionary();
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();
myHuman.desc = NetManager.GetDesc();
Debug.Log(myHuman.desc);
// 发送协议
Vector3 pos = myHuman.transform.position;
float euly = myHuman.transform.eulerAngles.y;
// 组装消息
string sendStr = "Enter|";
// PS: 如果写成 pos.x + ','; 则逗号不会加进去
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);
// 解析参数
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]);
// 如果是自己进入则不处理
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();
human.desc = desc;
// 用endpoint作为主键,但是正常应该是用username
otherHuman.Add(desc, human);
}
void OnList(string msgbody)
{
Debug.Log(msgbody);
// 解析参数
string[] splitmsg = msgbody.Split(',');
// count: 玩家个数
int count = splitmsg.Length / 6;
// 生成每一个玩家
for (int i = 0; i < count; i++)
{
// 解析每一个参数
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;
// 创建其他玩家
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();
human.desc = desc;
otherHuman.Add(human.desc, human);
}
}
void OnMove(string msgbody)
{
// 解析参数
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]);
// 同步移动其他玩家
if (!otherHuman.ContainsKey(desc))
return;
otherHuman[desc].MoveTo(new Vector3(x, y, z));
}
void OnLeave(string msgbody)
{
// 解析参数
string desc = msgbody;
// 删除离线玩家
if (!otherHuman.ContainsKey(desc))
return;
Destroy(otherHuman[desc].gameObject);
otherHuman.Remove(desc);
}
void OnAttack(string msgbody)
{
// 解析参数
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)
{
// 解析参数
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();
}
}
客户端的实现就这样,代码也比较简单。
3、服务器
服务器用python编写,MessageHandler为处理网络消息的类;GameServer为服务器类,用于接收消息;ClientStaes为客户端状态类。
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、结语
客户端的收发消息用的异步socket,服务端的复用模型为select模型,性能较差,并且客户端和服务端都没有处理粘包半包的问题,在之后的章节中会处理这个问题。上述的客户端代码还存在一个问题,就是客户端是用本机的ip-port作为自身的标识,但是这个方法在局域网中是不行的,因为出了路由器之后的ip会变,这样会导致进游戏的时候会创建两个自己。修改方法只需将其改用username作为唯一标识即可。