《Unity網(wǎng)絡(luò)游戲?qū)崙?zhàn)》Chapter2: 聊天室

1擂橘、介紹

上一章節(jié)是用unity制作客戶端,Python制作服務(wù)器的簡單Echo程序摩骨。接下來這一章節(jié)是編寫簡單的聊天室程序通贞。

2、客戶端

客戶端的界面如下:


image.png

最上面那個黑色帶滾動條的就是聊天窗口。

聊天窗口的制作步驟比較多邪铲,大家可以參考下面的鏈接罢屈,步驟很詳細(xì):
https://zhuanlan.zhihu.com/p/33583772
其中有一個坑的就是如果設(shè)置了text的content size fitter之后,文本窗口會變成居中茎用;這個時候只要將文本的軸點(Pivot )設(shè)置為x=0,y=0即可睬罗。

其他用到的組件都很平常轨功,4個InputField,兩個按鍵容达。

客戶端代碼:
Echo.cs

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Net;
using System.Net.Sockets;

// 使用異步API BeginXXX
public class Echo : MonoBehaviour
{
    public InputField inputField_ip;
    public InputField inputField_port;
    public InputField inputField_msg;
    public InputField inputField_username;

    public Text showText;
    public ScrollRect scrollRect;

    private IPAddress ip;
    private int port;

    private bool updateUI = false;
    private string message;
    private string username;

    private byte[] recvBuff = new byte[1024];

    // 定義服務(wù)器套接字
    Socket socket;

    // 輸入ip
    public void InputIP(string _ip)
    {
        ip = IPAddress.Parse(inputField_ip.text);
    }

    // 輸入port
    public void InputPort(string _port)
    {
        port = int.Parse(inputField_port.text);
    }

    public void InputUsername(string _name)
    {
        username = inputField_username.text;
    }

    public void OnButtonConnectClick()
    {
        socket.BeginConnect(ip, port, ConnectCallBack, socket);
    }

    public void ConnectCallBack(IAsyncResult _ar)
    {
        try
        {
            Socket socket = (Socket)_ar.AsyncState;
            socket.EndConnect(_ar);
            Debug.Log("Connect server successfully.");
            socket.BeginReceive(recvBuff, 0, recvBuff.Length, 0, ReceiveCallBack, socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Connect server failed. " + ex.ToString());
        }
    }

    public void ReceiveCallBack(IAsyncResult _ar)
    {
        try
        {
            Socket socket = (Socket)_ar.AsyncState;
            // 接收的數(shù)據(jù)長度
            int count = socket.EndReceive(_ar);
            string recvMsg = System.Text.Encoding.UTF8.GetString(recvBuff, 0, count);
            message = recvMsg;
            updateUI = true;
            Debug.Log("Receive message: " + recvMsg);

            socket.BeginReceive(recvBuff, 0, recvBuff.Length, 0, ReceiveCallBack, socket);
        }
        catch(SocketException ex)
        {
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    public void OnButtonSendClick()
    {
        string sendMsg = inputField_msg.text;

        if (sendMsg == "")
        {
            Debug.Log("message not null");
            return;
        }

        // 發(fā)送數(shù)據(jù)
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
        socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);

        showText.text = showText.text + "<color=green>" + username + ": " + sendMsg + "</color>\n";

        inputField_msg.text = "";
    }

    public void SendCallBack(IAsyncResult _ar)
    {
        try
        {
            Socket socket = (Socket)_ar.AsyncState;
            // 發(fā)送的數(shù)據(jù)長度
            int count = socket.EndSend(_ar);
        }
        catch(SocketException ex)
        {
            Debug.Log("Send message failed. " + ex.ToString());
        }
    }

    void Start()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    }

    void Update()
    {
        if(updateUI)
            showText.text = showText.text + message + '\n';
            //Canvas.ForceUpdateCanvases();
            //scrollRect.verticalNormalizedPosition = 0f;
            //Canvas.ForceUpdateCanvases();
    }

    private void LateUpdate()
    {
        if(updateUI)
        {
            scrollRect.verticalNormalizedPosition = 0f;
            updateUI = false;
        }
    }
}

上述代碼中的BeginConnect古涧,BeginReceive和BeginSend都是Connect、Receive和Send的異步版本花盐。(其實就是在另一條線程上做這個事情)

所做的處理和之前的Echo程序不同的只是將發(fā)送的消息和接收到的消息都打印在聊天窗口上羡滑。

在Update中注釋的代碼菇爪,Canvas.ForceUpdateCanvases();就是立刻更新所有Canvases的content;scrollRect.verticalNormalizedPosition = 0f;是將滾動欄滾動到最低部柒昏。不立即調(diào)用是因為空間的寫入值需要一定的時間繪制凳宙,此時滾動條的位置不確定,需要等待繪制完成才調(diào)用职祷。用注釋中的代碼或者在LateUpdate中調(diào)用都行氏涩。或者參考[1]這樣做有梆。都是可以的削葱。

實現(xiàn)的效果為:


image.png

3、服務(wù)端

服務(wù)器代碼用Python編寫淳梦,用的Select模型析砸,主要做的事情就是將一個客戶端發(fā)送來的消息遍歷發(fā)送到其它所有客戶端中。直接上代碼:
demo.py

import socket
import select
import ClientStates as cs

# server socket
ip = "127.0.0.1"
port = 8888
server_address = (ip, port)
buffer_size = 1024
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(server_address)
server.listen(5)
connect_socket = [server]
client_states = {}

print "Server Start."
# Main Loop
while True:
    read_fds, write_fds, error_fds = select.select(connect_socket, [], [], 1)
    for fd in read_fds:
        if fd is server:
            # client connect
            client_socket, client_address = fd.accept()
            print client_address, 'connected'
            connect_socket.append(client_socket)
            client_states[client_socket] = cs.ClientStates(client_socket, client_address)
        else:
            data = fd.recv(buffer_size)
            if data:
                print 'receive data from: ', client_states[fd].addr
                for client in client_states.values():
                    if client.socket is fd:
                        continue
                    client.socket.send(data)
            else:
                connect_socket.remove(fd)
                client_states.pop(fd)
                fd.close()

ClientSates.py

class ClientStates(object):
    def __init__(self, sock, address):
        self.socket = sock
        self.addr = address
        self.recv_buff = []

4爆袍、效果

image.png

在不同的客戶端都能夠看到消息首繁。本地客戶端發(fā)送的消息是綠色,其它客戶端發(fā)送的消息是白色陨囊。實現(xiàn)了簡單的聊天功能弦疮,沒有什么其它額外的功能,只是作為一個最簡單的聊天室蜘醋。

5胁塞、結(jié)語

KEEP LEARNING。

附錄

客戶端實現(xiàn)的代碼是用的C#異步API压语,還可以使用poll狀態(tài)監(jiān)測和select模型啸罢。因為對于客戶端而言一直在主線程進(jìn)行檢測,消耗的性能較高胎食,因此多用異步扰才。這里就只附上代碼:
Echo1.cs

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Net;
using System.Net.Sockets;

// 使用Socket的狀態(tài)監(jiān)測Poll
public class Echo1 : MonoBehaviour
{
    public InputField inputField_ip;
    public InputField inputField_port;
    public InputField inputField_msg;
    public InputField inputField_username;

    public Text showText;
    public ScrollRect scrollRect;

    private IPAddress ip;
    private int port;

    private bool updateUI = false;
    private string username;

    // 定義服務(wù)器套接字
    Socket socket;

    // 輸入ip
    public void InputIP(string _ip)
    {
        ip = IPAddress.Parse(inputField_ip.text);
    }

    // 輸入port
    public void InputPort(string _port)
    {
        port = int.Parse(inputField_port.text);
    }

    public void InputUsername(string _name)
    {
        username = inputField_username.text;
    }

    public void OnButtonConnectClick()
    {
        // 對于connect不必要用異步連接, 使用同步connect
        socket.Connect(ip, port);
    }

    public void OnButtonSendClick()
    {
        string sendMsg = inputField_msg.text;

        if (sendMsg == "")
        {
            Debug.Log("message not null");
            return;
        }

        // 發(fā)送數(shù)據(jù)
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
        socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);

        showText.text = showText.text + "<color=green>" + username + ": " + sendMsg + "</color>\n";

        inputField_msg.text = "";
    }

    public void SendCallBack(IAsyncResult _ar)
    {
        try
        {
            Socket socket = (Socket)_ar.AsyncState;
            // 發(fā)送的數(shù)據(jù)長度
            int count = socket.EndSend(_ar);
        }
        catch (SocketException ex)
        {
            Debug.Log("Send message failed. " + ex.ToString());
        }
    }

    void Start()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    }

    void Update()
    {
        if (socket == null)
            return;

        if (socket.Poll(0, SelectMode.SelectRead))
        {
            // socket可讀
            byte[] recvBuff = new byte[1024];
            int count = socket.Receive(recvBuff);
            string recvMsg = System.Text.Encoding.UTF8.GetString(recvBuff, 0, count);
            showText.text = showText.text + recvMsg + '\n';
            updateUI = true;
        }
    }

    private void LateUpdate()
    {
        if (updateUI)
        {
            scrollRect.verticalNormalizedPosition = 0f;
            updateUI = false;
        }
    }
}

Echo2.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Net;
using System.Net.Sockets;

// 使用Select模型
public class Echo2 : MonoBehaviour
{
    public InputField inputField_ip;
    public InputField inputField_port;
    public InputField inputField_msg;
    public InputField inputField_username;

    public Text showText;
    public ScrollRect scrollRect;

    private IPAddress ip;
    private int port;

    private bool updateUI = false;
    private string username;

    List<Socket> readfds = new List<Socket>();

    // 定義服務(wù)器套接字
    Socket socket;

    // 輸入ip
    public void InputIP(string _ip)
    {
        ip = IPAddress.Parse(inputField_ip.text);
    }

    // 輸入port
    public void InputPort(string _port)
    {
        port = int.Parse(inputField_port.text);
    }

    public void InputUsername(string _name)
    {
        username = inputField_username.text;
    }

    public void OnButtonConnectClick()
    {
        // 對于connect不必要用異步連接, 使用同步connect
        socket.Connect(ip, port);
    }

    public void OnButtonSendClick()
    {
        string sendMsg = inputField_msg.text;

        if (sendMsg == "")
        {
            Debug.Log("message not null");
            return;
        }

        // 發(fā)送數(shù)據(jù)
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
        socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);

        showText.text = showText.text + "<color=green>" + username + ": " + sendMsg + "</color>\n";

        inputField_msg.text = "";
    }

    public void SendCallBack(IAsyncResult _ar)
    {
        try
        {
            Socket socket = (Socket)_ar.AsyncState;
            // 發(fā)送的數(shù)據(jù)長度
            int count = socket.EndSend(_ar);
        }
        catch (SocketException ex)
        {
            Debug.Log("Send message failed. " + ex.ToString());
        }
    }

    void Start()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    }

    void Update()
    {
        if (socket == null)
            return;

        readfds.Clear();
        readfds.Add(socket);

        // Select模型
        Socket.Select(readfds, null, null, 1);

        // 遍歷可讀的socket --- 其實就只有自己的socket
        foreach (Socket fd in readfds)
        {
            byte[] recvBuff = new byte[1024];
            int count = fd.Receive(recvBuff);
            string recvMsg = System.Text.Encoding.UTF8.GetString(recvBuff, 0, count);
            showText.text = showText.text + recvMsg + '\n';
            updateUI = true;
        }
    }

    private void LateUpdate()
    {
        if (updateUI)
        {
            scrollRect.verticalNormalizedPosition = 0f;
            updateUI = false;
        }
    }
}

參考:
[1] https://zhuanlan.zhihu.com/p/33583772

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市厕怜,隨后出現(xiàn)的幾起案子衩匣,更是在濱河造成了極大的恐慌,老刑警劉巖粥航,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琅捏,死亡現(xiàn)場離奇詭異,居然都是意外死亡递雀,警方通過查閱死者的電腦和手機柄延,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來映之,“玉大人拦焚,你說我怎么就攤上這事蜡坊「苁洌” “怎么了赎败?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蠢甲。 經(jīng)常有香客問我僵刮,道長,這世上最難降的妖魔是什么鹦牛? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任搞糕,我火速辦了婚禮,結(jié)果婚禮上曼追,老公的妹妹穿的比我還像新娘窍仰。我一直安慰自己,他們只是感情好礼殊,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布驹吮。 她就那樣靜靜地躺著,像睡著了一般晶伦。 火紅的嫁衣襯著肌膚如雪碟狞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天婚陪,我揣著相機與錄音族沃,去河邊找鬼。 笑死泌参,一個胖子當(dāng)著我的面吹牛脆淹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沽一,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼未辆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锯玛?” 一聲冷哼從身側(cè)響起咐柜,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎攘残,沒想到半個月后拙友,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡歼郭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年遗契,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片病曾。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡牍蜂,死狀恐怖漾根,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鲫竞,我是刑警寧澤辐怕,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站从绘,受9級特大地震影響寄疏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜僵井,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一陕截、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧批什,春花似錦农曲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至却汉,卻和暖如春驯妄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背合砂。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工青扔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人翩伪。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓微猖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缘屹。 傳聞我的和親對象是個殘疾皇子凛剥,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354