文章目录
这章主要是实现一个大乱斗的游戏,搭场景部分省略,人物动作状态机省略。
第一版角色类Human
BaseHuman
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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 == false) {
return;
}
Vector3 pos = transform.position;
transform.position = Vector3.MoveTowards(pos, targetPosition, speed*Time.deltaTime);
transform.LookAt(targetPosition);
if(Vector3.Distance(pos, targetPosition) < 0.05f){
isMoving = false;
animator.SetBool("isMoving", false);
}
}
//攻击动作
public void Attack(){
isAttacking = true;
attackTime = Time.time;
animator.SetBool("isAttacking", true);
}
//攻击Update
public void AttackUpdate(){
if(!isAttacking) return;
if(Time.time - attackTime < 1.2f) return;
isAttacking = false;
animator.SetBool("isAttacking", false);
}
// Use this for initialization
internal void Start () {
animator = GetComponent<Animator>();
}
// Update is called once per frame
internal void Update () {
MoveUpdate();
AttackUpdate();
}
}
BaseHuman作为基类,这部分主要实现的逻辑是,给定一个目标地点,那么人物就会朝目标地点移动,不涉及任何网络部分。
CtrlHuman
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CtrlHuman : BaseHuman {
// Use this for initialization
new void Start () {
base.Start();
}
// Update is called once per frame
new void Update () {
base.Update();
//移动
if(Input.GetMouseButtonDown(0)){
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
if (hit.collider.tag == "Terrain"){
MoveTo(hit.point);
}
}
}
}
暂时也不涉及网络部分,主要逻辑是鼠标点击到Terrain,玩家就会向点击的地方移动。
SyncHuman
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SyncHuman : BaseHuman {
// Use this for initialization
new void Start () {
base.Start();
}
// Update is called once per frame
new void Update () {
base.Update();
}
}
暂时不做逻辑处理。
通信协议
目前的通信协议采用字符串协议,形式如下消息名|参数1,参数2,参数3,...
例如Move|127.0.0.1:1234,10,0,8可以代表127.0.0.1:1234这位用户向(10,0,8)坐标移动。
消息队列
将通信记录保存下来,采用数据结构List<String>,主要是在Update里面读取消息处理。
NetManager类
辅助方法类,便于客户端对网络的操作。
主要提供三个主要接口
- Connect 发起连接
- AddListener 消息监听
- Send 发送消息给服务端
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using UnityEngine.UI;
using System;
public static class NetManager {
//定义套接字
static Socket socket;
//接收缓冲区
static byte[] readBuff = new byte[1024];
//委托类型
public delegate void MsgListener(String str);
//监听列表
private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();
//消息列表
static List<String> msgList = new List<string>();
//添加监听
public static void AddListener(string msgName, MsgListener listener){
listeners[msgName] = listener;
}
//获取描述
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
socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//Connect
socket.Connect(ip, port);
//BeginReceive
socket.BeginReceive( readBuff, 0, 1024, 0,
ReceiveCallback, socket);
}
//Receive回调
private static void ReceiveCallback(IAsyncResult ar){
try {
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndReceive(ar);
string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
msgList.Add(recvStr);
socket.BeginReceive( readBuff, 0, 1024, 0,
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.Default.GetBytes(sendStr);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
}
//Send回调
private static void SendCallback(IAsyncResult ar){
try {
Socket socket = (Socket) ar.AsyncState;
//int count = socket.EndSend(ar);
}
catch (SocketException ex){
Debug.Log("Socket Send fail" + ex.ToString());
}
}
//Update
public static void Update(){
if(msgList.Count <= 0)
return;
String msgStr = msgList[0];
msgList.RemoveAt(0);
string[] split = msgStr.Split('|');
string msgName = split[0];
string msgArgs = split[1];
//监听回调;
if(listeners.ContainsKey(msgName)){
listeners[msgName](msgArgs);
}
}
}
再解释一下上面的代码,首先用到了delegate,不解释。用private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();表示了字符串和要调用函数之间的映射关系,这个功能很强大,在Update函数中的最后一个判断就用到了这个映射关系
if(listeners.ContainsKey(msgName)){
listeners[msgName](msgArgs);
}
客户端从服务端接收消息解析之后,比如Move|127.0.0.1:1234,10,0,8解析之后得到的msgName = split[0],实际得到的就是Move这个字符串,然后listeners[msgName]就代表了一个delegate,再给delegate传递参数msgArgs,相当于给相应的函数传递参数,下面更详细说一下过程。
public static void AddListener(string msgName, MsgListener listener){
listeners[msgName] = listener;
}
AddListener,相当于事先自定了字符串到委托函数的映射,比如一开始调用了NetManager.AddListener("Move",OnMove),那么字符串Move和OnMove函数就存在着映射关系,这时候调用这一行代码listeners[msgName](msgArgs);,msgName实际上就是Move,msgArgs实际上就是127.0.0.1:1234,10,0,8,而listeners[msgName]就是listeners["Move"],也就是OnMove函数,所以最后结果就是调用了OnMove(127.0.01:1234,10,0,8)函数。那么就实现了从服务端接收一个字符串,就在客户端调用相应函数的功能,方便服务端给客户端发消息。
最后图解一下整个过程。
加入网络功能
最直白的实现的效果是,有多个客户端同时启动,当你移动的时候,别人也可以看到你在移动,当你攻击的时候,相应的逻辑也正确,攻击传入的参数是方向,而不是一个玩家。
消息处理
因为要显示别的玩家,所以用一个数据结构来存储其他玩家public Dictionary<string, BaseHuman> otherHumans = new Dictionary<string, BaseHuman>();
客户端主要处理六个信息,玩家一开始会发送Enter消息告知服务端自己的位置以及角度,再发一个空的List消息用来查询玩家信息。
- Enter 接受服务端的信息,类似于
Enter|127.0.0.1:4564,3,0,5,0一个消息的名称,加上玩家的唯一表示,加上位置的信息,再加上一个欧拉角,判断如果不是自己,那么就利用信息,生成一个GameObject,并且在otherHumans中加入他。
//客户端
void OnEnter (string msgArgs) {
Debug.Log("OnEnter " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float eulY = float.Parse(split[4]);
//是自己
if(desc == NetManager.GetDesc())
return;
//添加一个角色
GameObject obj = (GameObject)Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();
h.desc = desc;
otherHumans.Add(desc, h);
}
//服务端
public static void MsgEnter(ClientState c, string msgArgs){
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float eulY = float.Parse(split[4]);
//赋值
c.hp = 100;
c.x = x;
c.y = y;
c.z = z;
c.eulY = eulY;
//广播
string sendStr = "Enter|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
- List 查询所有玩家,所以消息中包含多个值,类似
List|127.0.0.1:4564,3,0,5,0,100,127.0.0.1:4578,4,0,9,0,100这里多了一个参数用作HP。如果不是自己的话,就生成一个GameObject,并且放到otherHumans中。
//客户端
void OnList (string msgArgs) {
Debug.Log("OnList " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
int count = (split.Length-1)/6;
for(int i = 0; i < count; i++){
string desc = split[i*6+0];
float x = float.Parse(split[i*6+1]);
float y = float.Parse(split[i*6+2]);
float z = float.Parse(split[i*6+3]);
float eulY = float.Parse(split[i*6+4]);
int hp = int.Parse(split[i*6+5]);
//是自己
if(desc == NetManager.GetDesc())
continue;
//添加一个角色
GameObject obj = (GameObject)Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();
h.desc = desc;
otherHumans.Add(desc, h);
}
}
//服务端
public static void MsgList(ClientState c, string msgArgs){
string sendStr = "List|";
foreach (ClientState cs in MainClass.clients.Values){
sendStr+=cs.socket.RemoteEndPoint.ToString() + ",";
sendStr+=cs.x.ToString() + ",";
sendStr+=cs.y.ToString() + ",";
sendStr+=cs.z.ToString() + ",";
sendStr+=cs.eulY.ToString() + ",";
sendStr+=cs.hp.ToString() + ",";
}
MainClass.Send(c, sendStr);
}
- Move 处理移动逻辑,消息类似
Move|127.0.0.1:4564,3.0.5,如果是其他玩家,就让其他玩家移动。因为本地会自己处理自己的移动逻辑,不需要服务端的介入。
//客户端
void OnMove (string msgArgs) {
Debug.Log("OnMove " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//移动
if(!otherHumans.ContainsKey(desc))
return;
BaseHuman h = otherHumans[desc];
Vector3 targetPos = new Vector3(x, y, z);
h.MoveTo(targetPos);
}
//服务端
public static void MsgMove(ClientState c, string msgArgs){
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//赋值
c.x = x;
c.y = y;
c.z = z;
//广播
string sendStr = "Move|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
- Leave 玩家退出或者下线,消息类似
···Leave|127.0.0.1:4564,如果是其他玩家,则销毁其他玩家,并且从字典中删除。
//客户端
void OnLeave (string msgArgs) {
Debug.Log("OnLeave " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
//删除
if(!otherHumans.ContainsKey(desc))
return;
BaseHuman h = otherHumans[desc];
Destroy(h.gameObject);
otherHumans.Remove(desc);
}
//服务端
public static void OnDisconnect(ClientState c){
string desc = c.socket.RemoteEndPoint.ToString();
string sendStr = "Leave|" + desc + ",";
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
- Attack 攻击 消息类似
Attack|127.0.0.1:4564,45最后一个参数表示攻击方向,如果是其他玩家的话,则让其他玩家攻击,本地会处理自己的攻击逻辑。还有一个Hit消息是发送给服务端的,由于做的游戏比较简单,暂时没有血条的UI,所以只要服务端知道人物的HP就可以了,不必返还给客户端,所以客户端也就没有Hit的协议接受。Attack是玩家攻击,只要右击了就可以认为是在攻击,而Hit是被攻击到了,要在Attack的前提下再加上判断条件.
//客户端
void OnAttack (string msgArgs) {
Debug.Log("OnAttack " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float eulY = float.Parse(split[1]);
//攻击动作
if(!otherHumans.ContainsKey(desc))
return;
SyncHuman h = (SyncHuman)otherHumans[desc];
h.SyncAttack(eulY);
}
//服务端
public static void MsgAttack(ClientState c, string msgArgs){
//广播
string sendStr = "Attack|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
//服务端
public static void MsgHit(ClientState c, string msgArgs){
//解析参数
string[] split = msgArgs.Split(',');
string attDesc = split[0];
string hitDesc = split[1];
//被攻击
ClientState hitCS = null;
foreach (ClientState cs in MainClass.clients.Values){
if(cs.socket.RemoteEndPoint.ToString() == hitDesc)
hitCS = cs;
}
if(hitCS == null)
return;
hitCS.hp -= 25;
if(hitCS.hp <= 0){
string sendStr = "Die|" + hitCS.socket.RemoteEndPoint.ToString();
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
}
- Die 玩家死亡,消息类似
Die|127.0.0.1:4664,如果是其他玩家的话,则删除该角色。
//客户端
void OnDie (string msgArgs) {
Debug.Log("OnAttack " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string attDesc = split[0];
string hitDesc = split[0];
//自己死了
if(hitDesc == myHuman.desc){
Debug.Log("Game Over");
return;
}
//死了
if(!otherHumans.ContainsKey(hitDesc))
return;
SyncHuman h = (SyncHuman)otherHumans[hitDesc];
h.gameObject.SetActive(false);
}
服务端的处理在MsgHit中处理了。
第二版角色类
BaseHuman没有变化
CtrlHuman
现在需要在移动和攻击的时候发出消息给服务端
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CtrlHuman : BaseHuman {
// Use this for initialization
new void Start () {
base.Start();
}
// Update is called once per frame
new void Update () {
base.Update();
//移动
if(Input.GetMouseButtonDown(0)){
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
if (hit.collider.tag == "Terrain"){
MoveTo(hit.point);
//发送协议
string sendStr = "Move|";
sendStr += NetManager.GetDesc()+ ",";
sendStr += hit.point.x + ",";
sendStr += hit.point.y + ",";
sendStr += hit.point.z + ",";
NetManager.Send(sendStr);
}
}
//攻击
if(Input.GetMouseButtonDown(1)){
if(isAttacking) return;
if(isMoving) return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
if(hit.collider.tag != "Terrain") return;
transform.LookAt(hit.point);
Attack();
//发送协议
string sendStr = "Attack|";
sendStr += NetManager.GetDesc()+ ",";
sendStr += transform.eulerAngles.y + ",";
NetManager.Send(sendStr);
//攻击判定
Vector3 lineEnd = transform.position + 0.5f*Vector3.up;
Vector3 lineStart = lineEnd + 20*transform.forward;
if(Physics.Linecast(lineStart, lineEnd, out hit)){
GameObject hitObj = hit.collider.gameObject;
if(hitObj == gameObject)
return;
SyncHuman h = hitObj.GetComponent<SyncHuman>();
if(h == null)
return;
sendStr = "Hit|";
sendStr += NetManager.GetDesc()+ ",";
sendStr += h.desc + ",";
NetManager.Send(sendStr);
}
}
}
}
SyncHuman
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SyncHuman : BaseHuman {
// Use this for initialization
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();
}
}
Main(客户端)
这是客户端的主函数,将之前提到的都整合在一起形成完整的逻辑
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour {
//人物模型预设
public GameObject humanPrefab;
//人物列表
public BaseHuman myHuman;
public Dictionary<string, BaseHuman> otherHumans = 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 obj = (GameObject)Instantiate(humanPrefab);
float x = Random.Range(-5, 5);
float z = Random.Range(-5, 5);
obj.transform.position = new Vector3(x, 0, z);
myHuman = obj.AddComponent<CtrlHuman>();
myHuman.desc = NetManager.GetDesc();
//发送协议
Vector3 pos = myHuman.transform.position;
Vector3 eul = myHuman.transform.eulerAngles;
string sendStr = "Enter|";
sendStr += NetManager.GetDesc()+ ",";
sendStr += pos.x + ",";
sendStr += pos.y + ",";
sendStr += pos.z + ",";
sendStr += eul.y + ",";
NetManager.Send(sendStr);
NetManager.Send("List|");
}
void Update(){
NetManager.Update();
}
void OnEnter (string msgArgs) {
Debug.Log("OnEnter " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float eulY = float.Parse(split[4]);
//是自己
if(desc == NetManager.GetDesc())
return;
//添加一个角色
GameObject obj = (GameObject)Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();
h.desc = desc;
otherHumans.Add(desc, h);
}
void OnList (string msgArgs) {
Debug.Log("OnList " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
int count = (split.Length-1)/6;
for(int i = 0; i < count; i++){
string desc = split[i*6+0];
float x = float.Parse(split[i*6+1]);
float y = float.Parse(split[i*6+2]);
float z = float.Parse(split[i*6+3]);
float eulY = float.Parse(split[i*6+4]);
int hp = int.Parse(split[i*6+5]);
//是自己
if(desc == NetManager.GetDesc())
continue;
//添加一个角色
GameObject obj = (GameObject)Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();
h.desc = desc;
otherHumans.Add(desc, h);
}
}
void OnMove (string msgArgs) {
Debug.Log("OnMove " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//移动
if(!otherHumans.ContainsKey(desc))
return;
BaseHuman h = otherHumans[desc];
Vector3 targetPos = new Vector3(x, y, z);
h.MoveTo(targetPos);
}
void OnLeave (string msgArgs) {
Debug.Log("OnLeave " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
//删除
if(!otherHumans.ContainsKey(desc))
return;
BaseHuman h = otherHumans[desc];
Destroy(h.gameObject);
otherHumans.Remove(desc);
}
void OnAttack (string msgArgs) {
Debug.Log("OnAttack " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float eulY = float.Parse(split[1]);
//攻击动作
if(!otherHumans.ContainsKey(desc))
return;
SyncHuman h = (SyncHuman)otherHumans[desc];
h.SyncAttack(eulY);
}
void OnDie (string msgArgs) {
Debug.Log("OnAttack " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string attDesc = split[0];
string hitDesc = split[0];
//自己死了
if(hitDesc == myHuman.desc){
Debug.Log("Game Over");
return;
}
//死了
if(!otherHumans.ContainsKey(hitDesc))
return;
SyncHuman h = (SyncHuman)otherHumans[hitDesc];
h.gameObject.SetActive(false);
}
}
服务端的处理
MsgHandler
专门用来处理消息
using System;
public class MsgHandler
{
public static void MsgEnter(ClientState c, string msgArgs){
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float eulY = float.Parse(split[4]);
//赋值
c.hp = 100;
c.x = x;
c.y = y;
c.z = z;
c.eulY = eulY;
//广播
string sendStr = "Enter|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
public static void MsgList(ClientState c, string msgArgs){
string sendStr = "List|";
foreach (ClientState cs in MainClass.clients.Values){
sendStr+=cs.socket.RemoteEndPoint.ToString() + ",";
sendStr+=cs.x.ToString() + ",";
sendStr+=cs.y.ToString() + ",";
sendStr+=cs.z.ToString() + ",";
sendStr+=cs.eulY.ToString() + ",";
sendStr+=cs.hp.ToString() + ",";
}
MainClass.Send(c, sendStr);
}
public static void MsgMove(ClientState c, string msgArgs){
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//赋值
c.x = x;
c.y = y;
c.z = z;
//广播
string sendStr = "Move|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
public static void MsgAttack(ClientState c, string msgArgs){
//广播
string sendStr = "Attack|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
public static void MsgHit(ClientState c, string msgArgs){
//解析参数
string[] split = msgArgs.Split(',');
string attDesc = split[0];
string hitDesc = split[1];
//被攻击
ClientState hitCS = null;
foreach (ClientState cs in MainClass.clients.Values){
if(cs.socket.RemoteEndPoint.ToString() == hitDesc)
hitCS = cs;
}
if(hitCS == null)
return;
hitCS.hp -= 25;
if(hitCS.hp <= 0){
string sendStr = "Die|" + hitCS.socket.RemoteEndPoint.ToString();
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
}
}
EventHandler
用来处理事件,其他可以合并,这么做应该是为了以后更好的代码风格吧。
using System;
public class EventHandler
{
public static void OnDisconnect(ClientState c){
string desc = c.socket.RemoteEndPoint.ToString();
string sendStr = "Leave|" + desc + ",";
foreach (ClientState cs in MainClass.clients.Values){
MainClass.Send(cs, sendStr);
}
}
}
MainClass(服务端)
服务端也是要用到和客户端类似的,根据字符串调用函数的操作,只不过这里没有用委托,而是用的反射机制,关于委托和反射机制的区别以及用法,可以参考这里。
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
public class ClientState
{
public Socket socket;
public byte[] readBuff = new byte[1024];
public int hp = -100;
public float x = 0;
public float y = 0;
public float z = 0;
public float eulY = 0;
}
class MainClass
{
//监听Socket
public static Socket listenfd;
//客户端Socket及状态信息
public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
public static void Main (string[] args)
{
//Socket
listenfd = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
listenfd.Bind(ipEp);
//Listen
listenfd.Listen(0);
Console.WriteLine("[服务器]启动成功");
//checkRead
List<Socket> checkRead = new List<Socket>();
//主循环
while(true){
//填充checkRead列表
checkRead.Clear();
checkRead.Add(listenfd);
foreach (ClientState s in clients.Values){
checkRead.Add(s.socket);
}
//select
Socket.Select(checkRead, null, null, 1000);
//检查可读对象
foreach (Socket s in checkRead){
if(s == listenfd){
ReadListenfd(s);
}
else{
ReadClientfd(s);
}
}
}
}
//读取Listenfd
public static void ReadListenfd(Socket listenfd){
Console.WriteLine("Accept");
Socket clientfd = listenfd.Accept();
ClientState state = new ClientState();
state.socket = clientfd;
clients.Add(clientfd, state);
}
//读取Clientfd
public static bool ReadClientfd(Socket clientfd){
ClientState state = clients[clientfd];
//接收
int count = 0;
try{
count = clientfd.Receive(state.readBuff);
}catch(SocketException ex){
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");
object[] ob = {state};
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Receive SocketException " + ex.ToString());
return false;
}
//客户端关闭
if(count <= 0){
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");
object[] ob = {state};
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Close");
return false;
}
//消息处理
string recvStr =
System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
string[] split = recvStr.Split('|');
Console.WriteLine("Recv " + recvStr);
string msgName = split[0];
string msgArgs = split[1];
string funName = "Msg" + msgName;
MethodInfo mi = typeof(MsgHandler).GetMethod(funName);
object[] o = {state, msgArgs};
mi.Invoke(null, o);
return true;
}
//发送
public static void Send(ClientState cs, string sendStr){
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
cs.socket.Send(sendBytes);
}
}
总结一下
举个攻击的例子,说一下完整的流程。一开始玩家A连接到了服务端,那么就会向服务端发送Enter,服务端接受到消息之后,会向玩家A发送Enter消息,但是由于是自己,并不处理,这时候玩家A再发送List消息,服务端的List便会添加玩家A的信息,然后发给所有的客户端,目前只有玩家A,由于HP是在服务端处理的,所以玩家A会知道自己血量。这时候玩家B也连上了服务端,向服务端发送Enter消息,那么服务端就会转发Enter消息,所以玩家A就知道了玩家B的存在,并且在玩家A的屏幕上显示了玩家B,但是玩家B不知道玩家A的存在,所以发送了List请求,这时候就拿到了玩家A的信息。并且屏幕上也会显示玩家A的位置。假设这时候玩家A右击了玩家B,实际上判定并不是是否点击了玩家B,而是是否点到了地板,点的时候会发送Attack消息,本地播放攻击动画,然后服务端接收到Attack,会向所有的客户端广播消息,玩家A本身接收到消息之后不会处理,而玩家B接收到消息之后会处理,所以现在玩家B也可以看到玩家A播放攻击动画了。如果玩家B是站在玩家A的正前方,并且本地判定为攻击到了玩家B,那么玩家A就会发出Hit消息,服务端接收到Hit消息之后,会在后台默默的扣除玩家B的血量,并不广播,所以如果这时候在本地打印血量的时候,是不会有变化的,但是服务端会记录,当玩家B的血条为0时,服务端会广播Die消息,玩家A会看到玩家B消失了,而玩家B会直接显示Game Over,所以说玩家B的直接死亡是服务端发送消息,并且玩家B的客户端接收之后才触发的,由于血量是在服务端计算的,所以不经过服务端处理玩家A是打不死玩家B的。