《Unity网络游戏实战》Chapter3: 乱斗小游戏

1、介绍

《Unity网络游戏实战》的第三章节是做一个乱斗小游戏。实现的功能是玩家进入到一个场景,右键点击地面移动,左键点击为攻击,击中其他玩家就扣血,血量为0就死亡。


image.png

2、客户端

本地玩家的控制脚本CtrlHuman和同步其他玩家的SyncHuman都继承于BaseHuman,玩家的控制逻辑都写在这三个脚本里面。网络消息的发送和接收处理,则是用了一个静态类NetManager和NetWorkManager。NetWorkManager可以挂在在游戏的任何一个物体中。


image.png

贴上代码,和书上的源码有些许不同。
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作为唯一标识即可。

你可能感兴趣的:(《Unity网络游戏实战》Chapter3: 乱斗小游戏)