《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<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))
        {
            // 点击鼠标右键
            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<SyncHuman>();
                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<string, MsgListener> listeners = new Dictionary<string, MsgListener>();

    static List<string> msgList = new List<string>();

    // 添加监听
    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<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);

        // 发送协议
        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<SyncHuman>();
        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<SyncHuman>();
            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作为唯一标识即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容