这章主要是实现一个大乱斗的游戏,搭场景部分省略,人物动作状态机省略。

第一版角色类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),那么字符串MoveOnMove函数就存在着映射关系,这时候调用这一行代码listeners[msgName](msgArgs);msgName实际上就是MovemsgArgs实际上就是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的。

相关文章:

  • 2021-09-01
  • 2021-07-06
  • 2021-11-29
  • 2021-11-17
  • 2021-12-25
  • 2021-06-03
  • 2021-06-18
  • 2021-12-28
猜你喜欢
  • 2021-06-23
  • 2021-12-17
  • 2021-06-22
  • 2021-12-02
  • 2021-07-26
  • 2021-12-25
  • 2021-11-19
相关资源
相似解决方案