跳到主要內容

精選

對於玩家們常會說的「懷念以前的遊戲」甚至覺得以前的遊戲比較好玩觀點與AI進行探討part1

 接下來針對一些玩家對於很多遊戲習慣會說的以前“ 遊戲比較好玩 ”這點來進行相關的提問 當然我知道我這提問只是一個面向,不過可以看看AI是如和回應的 問題: 有時候,對於遊戲玩家中很常見的一些說法,也覺得有點矛盾,感覺很多人似乎並不是真的理解自己在說些什麼。   像是常常有很多人都喜歡說什麼「懷念以前的遊戲」,或是覺得以前那樣在遊戲中可以做自己想做的事情,覺得很棒,認為現在的遊戲怎麼都只有唯一解,或是只有少數的流派之類的。   但是實際上拆解開來看,以前的遊戲,單純只是因為那時候大家沒有在查攻略玩遊戲而已,難道不是這樣嗎?如果你那時候去查了攻略,不就會發現更好練或是更強的方式,然後就會發現自己在做傻事,結果就是改變自己的玩法。   所以,只要你現在玩家玩新遊戲的時候也都不要查攻略,那你怎麼會覺得只有唯一解?這觀點本身就是有問題的啊。 就是因為你去查了攻略,發現有更好練或是更強的方法,才反而覺得是唯一解不是嗎?   因為照那樣的說法成立,其實現在的新遊戲就不應該開放大家討論,也不要有相關的討論版,任何問那個遊戲的文章都要刪掉,這樣大家就會回到以前那種遊玩模式。 但是,難道大家會想要這樣玩遊戲嗎? 其實現代的遊戲玩家根本就不想那樣玩遊戲啊,結果他們自己的行為根本就是矛盾的,卻整天只會做這種膚淺的言論來抱怨。   如果真的想要玩到更好玩的遊戲,我其實真的覺得,玩家自己本身也是可以提升,甚至帶動整個風氣一起提升不是嗎?而不是只會做那種膚淺的抱怨,卻完全沒說到真正的點上吧。 --- 回應: 「以前遊戲比較自由」這句話,問題出在哪? 你點到一個關鍵: 以前不是沒有唯一解,而是「大家不知道唯一解在哪」 。 為什麼以前看起來比較自由? 沒有現在這麼即時、完整的攻略 沒有 YouTube、Wiki、Discord 秒解 資訊流通慢,玩家彼此之間是「局部知識」 所以結果是: 每個人玩法都不一樣 很多「其實很爛但當下覺得OK」的玩法能存活 玩家會把「不知道更好的解法」誤認成「設計上的自由」 但只要 當年有完整攻略 ,會發生什麼事? 大家一樣會改玩法,一樣會往效率最高的方向靠。 你說得沒錯: 一旦你知道有更快、更強、更省的方式,繼續用...

遊戲伺服器2025年初架設與研究part7遊戲中常見的websocket使用,並且在Unity與伺服器中建立連線

 那上次已經可以透過POST收送封包,來進行一些基本的操作了


當然如果願意鑽研的話,光是POST這邊就能用出非常多種應用


並且即便只使用POST也足夠應付不少小遊戲的封包收送了


不過這邊也在提供另一個方式,也是在遊戲中極為常見的用法(非遊戲領域就不清楚)

也就是透過Websocket的連線來收送封包


那這邊也是簡單的說一下Websocket的優點

當然這邊不是純討論它的優缺點,所以可能會說的不夠完整

大家也可以自行查詢相關的資料來更加完善這方面的知識

首先它可以主動從伺服器對使用者發送封包(當連線已經建立以後),如果用原本的http收送封包的方式基本上得從使用者主動發送,即便是要資料也要使用者主動問才能獲取

不過websocket則不用,它可以主動從伺服器發送給使用者

舉例來說,現在伺服器假設有公告系統,伺服器有公告了,如果有websocket連線,伺服器就可以直接通知所有使用者公告是什麼。

但是如果沒有websocket,那就是使用者透過類似心跳包來詢問,例如每30秒問一次server有沒有公告,也就是說伺服器可能已經發了公告,但是使用者須要等到下一次的心跳包詢問的時候才能知道伺服器有沒有公告,類似這樣的差別。

所以不是沒辦法做到,而是步驟不同。

第二點是websocket建立連線後可以維持較長的時間(這邊要考慮放置太久都沒送封包有可能連線會被一些自動機制切斷),不像http連線基本上就是收送,連線就斷開了,一直收送就要不斷處理這個連線連接跟丟棄的動作。

第三點則是因為不用等使用者問,所以可以持續送封包給使用者,用來節省一半的延遲(原本封包送到伺服器,再從伺服器回去,會有兩段延遲),舉例來說遊戲常用到的房間機制,這邊可能就由伺服器來運算房間邏輯的話,就可以不斷的傳送房間同步資訊給房間內的所有玩家,不用讓玩家每個自己問了再更新,延遲上至少可以減半。

那這邊就只是概略的提了三點,詳細可以直接去搜尋更多用法。

接下來就直接來開始說明如何簡單的使用。

首先自然是要先安裝

pip install websockets

這邊記得要先切到我們的虛擬環境中再執行安裝,這樣套件就會安裝在虛擬環境中

接下來我們現到伺服器程式碼的專案中新增一個新的.py檔

import asyncio

import websockets

最上面先import

接下來是主要的程式碼的內容



# 儲存所有已連線的使用者(key 是玩家 ID,value 是 websocket)
connected_users = {}

async def handler(websocket):
    player_id = None
    try:
        async for message in websocket:
            print(f"[接收訊息] {message}")
            data = json.loads(message)

            if data["type"] == "login":
                player_id = data["playerId"]
                connected_users[player_id] = websocket
                await websocket.send(json.dumps({
                    "type": "login_ok",
                    "message": f"登入成功,歡迎 {player_id}"
                }))
                print(f"[登入] 玩家 {player_id} 連線")

            elif data["type"] == "echo":
                await websocket.send(json.dumps({
                    "type": "echo_reply",
                    "message": data["message"]
                }))

            elif data["type"] == "broadcast":
                msg = data["message"]
                for pid, ws in connected_users.items():
                    if ws.open:
                        await ws.send(json.dumps({
                            "type": "broadcast",
                            "from": player_id,
                            "message": msg
                        }))

            else:
                await websocket.send(json.dumps({
                    "type": "error",
                    "message": "未知的封包類型"
                }))

    except websockets.exceptions.ConnectionClosed:
        print(f"[斷線] 玩家 {player_id} 離線")

    finally:
        if player_id and player_id in connected_users:
            del connected_users[player_id]
            print(f"[清除連線] 已移除玩家 {player_id}")
        
async def main():
    async with websockets.serve(handler, "127.0.0.1", 8765):
        print("WebSocket Server 已啟動在 ws://127.0.0.1:8765")
        await asyncio.Future()  # 永遠不結束

asyncio.run(main())


那這邊只是簡單的範例,詳細邏輯請根據自己的需要調整

我這邊就是簡單的登入,然後印log,並且保存連線,並且廣播訊息的處理

然後如果連線中斷,則把連線移除,做一個基本的處理

不過這邊要注意的是client端帶來的資料是要進行判斷處理的

另外一點是

async def handler(websocket):

這個語法本身,如果現在查應該有不少範例還會寫著

async def handler(websocket , path):

不過實際上目前好像是沒有path了,應該是版本到某個版本以後就沒有了

所以這邊發現有跳錯誤,可以檢查一下python的版本

目前新版是沒有path的

那這邊伺服器的程式碼已經準備好了,接下來要處理轉發的部分

轉發的話我們要回到Nginx 設定反向代理

用先前的指令打開Nginx的設定檔進行編輯

例如放在這邊的話/etc/nginx/sites-enabled/your-site.conf

當然請找到自己的設定檔進行編輯


server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location /ws {
        proxy_pass http://127.0.0.1:8765/;  # 傳給 WebSocket Server

        # 必要 WebSocket 標頭,讓 Nginx 允許 Upgrade
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        # 一些常見補充
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # 可加其他 API 或靜態網站處理(例如 Flask 的 API)
    location /api/ {
        proxy_pass http://127.0.0.1:6666/;
    }
}



補上websocket要使用的語法

方便對應我也把上下文保留的,大家可以自己比對來放入正確的區域

主要就是找443這個server區塊

然後關鍵就是/ws這邊

IP對應請對應上我們先前在python裡面打的,包含port接口要打對

當然這部分可以自己定義,只是也要避免打到其它服務使用的接口(會衝突)

那接下來沒問題後,我們就可以到Unity內進行websocket的設置處理

首先unity要使用websocket常見的插件應該是使用

NativeWebSocket

可以搜尋了解一下

那我這邊也直接說明要如何匯入專案

首先在Unity專案中打開Package Manager

點選左上角的+號

然後選擇透過網址安裝(有顯示Git URL的那個選項)

接著在上面填寫安裝的網址https://github.com/endel/NativeWebSocket.git#upm

這邊也提供Git的網址讓大家進行確認

https://github.com/endel/NativeWebSocket

安裝好之後就可以使用了(記得匯入專案)

接下來創建新的腳本來進行撰寫相關的程式碼


using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

首先是using的部分

接下來創建新的腳本來進行撰寫相關的程式碼



private ClientWebSocket ws;
private CancellationTokenSource cancelToken = new CancellationTokenSource();
private Uri serverUri = new Uri("wss://你的網域/ws"); 

async void Start()
    {
	await ConnectWebSocket();
        await SendLogin("player_001");

        // 測試封包
        await SendEcho("Hello Server!");
        await SendBroadcast("玩家發送了公告!");

        
    }
    
void Update()
    {
#if !UNITY_WEBGL || UNITY_EDITOR
        websocket?.DispatchMessageQueue();
#endif
    }
    
private async Task ConnectWebSocket()
    {
        ws = new ClientWebSocket();
        try
        {
            await ws.ConnectAsync(serverUri, cancelToken.Token);
            Debug.Log("已連接到伺服器");

            // 開啟接收協程
            StartCoroutine(ReceiveLoop());
        }
        catch (Exception ex)
        {
            Debug.LogError("無法連接到伺服器: " + ex.Message);
        }
    }

    private IEnumerator ReceiveLoop()
    {
        var buffer = new byte[4096];
        while (ws.State == WebSocketState.Open)
        {
            ArraySegment<byte> seg = new ArraySegment<byte>(buffer);
            WebSocketReceiveResult result = null;

            try
            {
                result = await ws.ReceiveAsync(seg, cancelToken.Token);
                string msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
                Debug.Log("收到訊息: " + msg);
            }
            catch (Exception ex)
            {
                Debug.LogError("接收失敗: " + ex.Message);
                break;
            }

            yield return null;
        }
    }

    private async Task SendLogin(string playerId)
    {
        var loginData = new Dictionary<string, object>
        {
            { "type", "login" },
            { "playerId", playerId }
        };
        await SendJson(loginData);
    }

    private async Task SendEcho(string message)
    {
        var data = new Dictionary<string, object>
        {
            { "type", "echo" },
            { "message", message }
        };
        await SendJson(data);
    }

    private async Task SendBroadcast(string message)
    {
        var data = new Dictionary<string, object>
        {
            { "type", "broadcast" },
            { "message", message }
        };
        await SendJson(data);
    }

    private async Task SendJson(Dictionary<string, object> dict)
    {
        string json = JsonUtility.ToJson(new JsonWrapper(dict));
        byte[] bytes = Encoding.UTF8.GetBytes(json);
        ArraySegment<byte> segment = new ArraySegment<byte>(bytes);
        try
        {
            await ws.SendAsync(segment, WebSocketMessageType.Text, true, cancelToken.Token);
            Debug.Log("📤 發送封包: " + json);
        }
        catch (Exception ex)
        {
            Debug.LogError("發送失敗: " + ex.Message);
        }
    }

    // JsonUtility 不能直接序列化 Dictionary,所以加個 wrapper
    [Serializable]
    public class JsonWrapper
    {
        public string type;
        public string playerId;
        public string message;

        public JsonWrapper(Dictionary<string, object> data)
        {
            if (data.ContainsKey("type")) type = data["type"].ToString();
            if (data.ContainsKey("playerId")) playerId = data["playerId"].ToString();
            if (data.ContainsKey("message")) message = data["message"].ToString();
        }
    }
    


那這邊主要就是跟server進行串接的部分,可以看到是對應需要的資料來處理

當然剛開始可以不用直接打這麼多,可以用類似底下這樣測試

	websocket = new WebSocket("wss://你的網域/ws");

	websocket.OnOpen += () => Debug.Log("連線成功!");
        websocket.OnMessage += HandleWebSocketMessage;
        websocket.OnError += (e) => Debug.Log("錯誤:" + e);
        websocket.OnClose += (e) => Debug.Log("連線關閉");

        await websocket.Connect();
        
        void Update()
    {
#if !UNITY_WEBGL || UNITY_EDITOR
        websocket?.DispatchMessageQueue();
#endif
    }
        
        private void HandleWebSocketMessage(byte[] bytes)
      {
      	Debug.Log("收到了websocket封包");
      }


可以先確保這段可以正常運作,才開始慢慢添加或是調整,慢慢加到跟上面那樣稍微複雜的程度。

那這邊程式碼我們一樣可以放在async void Start()的地方

要發送websocket封包主要的語法是

await websocket.SendText(message);

message就是主要帶過去的內容

接著就是在場景中掛上這個腳本

然後unity按下play

當然我們這邊要確保伺服器這個腳本已經有在運作了

例如想要手動執行的話可以輸入python3 你的腳本名稱.py

就可以讓伺服器開始接收websocket的連線了(Nginx也記得要重啟)

並且這邊也可以延伸開始去測試多個websocket連線,如何讓A玩家的資訊可以同步到例如B玩家C玩家那邊了

另外Websocket的網域開頭,wss對應就是https,所以如果大家還使用http連線的話要測試則是使用ws開頭進行測試

這次就先分享到這邊,如果還有什麼疑問的也可以提出來,或是有什麼講錯的部分也歡迎指正~







留言