Unity로 node.js WebSocket 통신하기 (socket.io를 이용해서 룸형식 채팅방 구현)-2

2022. 7. 23. 03:28유니티 unity

 

 

 

 

이번에는 socket.io를 이용해서 룸 형식 채팅방을 만들어보겠습니다.

웹소켓으로도 충분히 가능하지만. socket.io를 이용하면 좀 더 편하게 만들 수 있을 거 같아서 이용해봤습니다.

 

소켓은 프로토콜,IP 주소, 포트 넘버로 정의됩니다.

이것들을 합쳐서 네트워크 상에서 서버와 클라이언트를 통신할 수 있도록 해주는 장치입니다

 

여기서 socket.io는 Websocket기반으로 좀 더 편리하게 만들어주는 모듈 중 하나입니다.

브라우저와 서버 간의 실시간, 양방향 및 이벤트 기반 통신을 가능하게 해줍니다.

 

socket.io 기능 중에 방기능 과 네임스페이스가 있는데 말 그대로 클라이언트들을 분류해줍니다.

https://socket.io/docs/v4/namespaces/

 

 

 

https://socket.io/get-started/chat

 

Get started | Socket.IO

In this guide we’ll create a basic chat application. It requires almost no basic prior knowledge of Node.JS or Socket.IO, so it’s ideal for users of all knowledge levels.

socket.io

https://runebook.dev/ko/docs/socketio/-index-

 

Socket.IO 4.1 한국어

 

runebook.dev

 

 

간단한 룸 형식의 채팅 프로그램을 만들 거라서

네임스페이스까지 사용할 필요는 없어 보여서 룸만 사용했습니다.

딱 4개만 알면 쉽게 만들 수 있습니다

 

on 수신

emit 전달

join 방에 들어갑니다

leave 방에 나갑니다.

 

const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const port = 7777;
const { Server } = require("socket.io");
const io = new Server(server);

io.on('connection', socket => {
//접속

socket.emit('이벤트이름',데이터1,데이터2,....) // 클라이언트에게 보냅니다. 데이터는 여러개 보낼수잇습니다

    socket.on('이벤트이름',(date1,date2...)=>{
    	//이벤트를 수신합니다.
    }
socket.join("방이름") // 방에 들어갑니다
socket.leave("방이름") // 방에 나갑니다.

socket.to("방이름").emit(~~) // 본인을 제외한 방이름에 들어간 클라이언트에게 이벤트를 보냅니다.
io.to("방이름").emit(~~) // 방이름에 들어간 클라이언트에게 이벤트를 보냅니다(본인포함)
io.emit(~~) // 모든 클라이언트에게 이벤트를 보냅니다.

    socket.on('disconnect',a=>{
		//연견끊김
    }
}


server.listen(port, () => {
//연결합니다.
  console.log('listening on *:' + port);
});

 

몇 개 안 되는 함수들 가지고 충분히 구현이 가능합니다.

 

유니티에서 socket.io를 사용을 해야 하는데 

현재 유니티 에셋스토어에는 무료 에셋은 없고 유료 에셋만 있습니다.

검색을 좀 하다 보니까 괜찮은 자료가 있어서 사용했습니다.

 

https://github.com/itisnajim/SocketIOUnity

 

GitHub - itisnajim/SocketIOUnity: A Wrapper for socket.io-client-csharp to work with Unity.

A Wrapper for socket.io-client-csharp to work with Unity. - GitHub - itisnajim/SocketIOUnity: A Wrapper for socket.io-client-csharp to work with Unity.

github.com

 

서버랑 거의 똑같습니다.

 

socket.Emit("이벤트이름", 데이터); // 
socket.EmitStringAsJSON("이벤트이름", "{\"foo\": \"bar\"}"); //Json

socket.On("이벤트이름", (response) =>
{
   //처리
});

socket.OnUnityThread("이벤트이름", (response) =>
{
    //메인쓰레드로 처리하게 합니다.
});

 

 

처리할 때는 메인 스레드가 아닌 다른 스레드를 사용하기 때문에 UI나 게임 오브젝트 등등 작동이 안 되기 때문에 따로 처리를 해주거나 OnUnityThread를 쓰면 됩니다.

따로 처리를 해줄 때는 큐에 이벤트들을 담아서 처리를 하거나

 

https://github.com/PimDeWitte/UnityMainThreadDispatcher

 

GitHub - PimDeWitte/UnityMainThreadDispatcher: A simple, thread-safe way of executing actions (Such as UI manipulations) on the

A simple, thread-safe way of executing actions (Such as UI manipulations) on the Unity Main Thread - GitHub - PimDeWitte/UnityMainThreadDispatcher: A simple, thread-safe way of executing actions (S...

github.com

이 스크립트 사용하시면 됩니다. 사용법은 제가 멀티스레드 관련 글 올렸던 거 보시면 됩니다. 첫 페이지에 있습니다.

 

또 Json변환을 해야 하기 때문에

 

패키지 매니저에서 com.unity.nuget.newtonsoft-json 추가해주세요.

 

클라이언트에서 데이터 받는 법에 대해서 알아보겠습니다.

 

데이터는 매개변수를 여러 개 써서 보내거나 배열로 보내거나 json으로 보낼 때입니다.

 

 

 

예시 - 매개변수 여러 개 해서 보냄

클라이언트에서는 GetValue를 사용하면 됩니다.

GetValue<T> 이거는 오류가 나더라고요

//서버
socket.emit('Event1',3,5,7,9,11,13)



//클라이언트

socket.On("Event1",(data)=>{


//data.GetValue(0).ToString() //문자열로 3 
//data.GetValue(1).ToString() //문자열로 5 
//data.GetValue(2).ToString() //문자열로 7 
//data.GetValue(3).ToString() //문자열로 9 
//data.GetValue(4).ToString() //문자열로 11 
//data.GetValue(5).ToString() //문자열로 13


Debug.Log(data.GetValue(0).GetInt32()) // 정수로 3
Debug.Log(data.GetValue(1).GetInt32()) // 정수로 5
Debug.Log(data.GetValue(2).GetInt32()) // 정수로 7
Debug.Log(data.GetValue(3).GetInt32()) // 정수로 9
Debug.Log(data.GetValue(4).GetInt32()) // 정수로 11
Debug.Log(data.GetValue(5).GetInt32()) // 정수로 13

});

 

 

예시 - Json으로 보내기

클라이언트에서 받을 때는 여러 가지 방법이 있는데

String으로 받은 뒤 딕셔너리로 받아도 되고 클래스로 받아도 됩니다.

//서버

socket.emit('Event1',{
    data1:"데이터1",
    data2:"데이터2",
    data3:"데이터3",
    data4:5,
  })
  
  ===========
  //위랑 같습니다
  
  var datass={
    data1:"데이터1",
    data2:"데이터2",
    data3:"데이터3",
    data4:5
  }
  
  socket.emit('Event1',datass)
  
  
  
  
  
  //클라이언트
  
  
  //바로 사용하기
  
    socket.On("Event1", (data) =>
   {

       JsonElement a = data.GetValue(0);
       
       Debug.Log(a.GetProperty("data1")); // 데이터1
       Debug.Log(a.GetProperty("data2")); // 데이터2
       Debug.Log(a.GetProperty("data3")); // 데이터3
       Debug.Log(a.GetProperty("data4").GetInt32()); // 5
   });
       
  
  
  //딕셔너리로 받는법
  
  socket.On("Event1", (data) =>
   {

       JsonElement a = data.GetValue(0);

       Dictionary<String, String> dic = JsonConvert.DeserializeObject<Dictionary<string, string>>(a.ToString());

       Debug.Log(dic["data1"]); // 데이터1
       Debug.Log(dic["data2"]); // 데이터2
       Debug.Log(dic["data3"]); // 데이터3



   });
       
       
       
 //클래스로 받는법
 
 public class data
{
    public string data1;
    public string data2;
    public string data3;
}


socket.On("Event1", (data) =>
   {

       JsonElement a = data.GetValue(0);

	
       


       data d = JsonConvert.DeserializeObject<data>(a.ToString());

       Debug.Log(d.data1);//데이터1
       Debug.Log(d.data2);//데이터2
       Debug.Log(d.data3);//데이터3



   });

 

 

배열로 받는 법은

 

 

//서버

  var datass=[3,5,7]
  
  socket.emit('Event1',datass)
  
  
  
  //클라이언트
  
  socket.On("Event1", (data) =>
   {

       var a = data.GetValue(0);

       string[] d = JsonConvert.DeserializeObject<string[]>(a.ToString());

       Debug.Log(d[0]);//3
       Debug.Log(d[1]);//5
       Debug.Log(d[2]);//7



   });

 

클라이언트에서 json으로 보내는 법

EmitStringAsJSON를 사용하면 됩니다.

 

//클라이언트


public class data
{
    public string data1;
    public string data2;
    public string data3;
    public int data4;
}

data d = new data();
d.data1 = "데이터1";
d.data2 = "데이터2";
d.data3 = "데이터3";
d.data4 = 5;
String s = JsonConvert.SerializeObject(d);
socket.EmitStringAsJSON("Event1",s);


===========

//서버

socket.on('Event1',data=>{
    console.log(data.data1);
    console.log(data.data2);
    console.log(data.data3);
    console.log(data.data4);
  })

 

 

 

 

우선 비주얼 스튜디오 코드를 열어서 폴더 하나를 만들어봅시다.

 

 

터미널을 열어서

 

npm -i express 

 

npm -i socket.io

 

 

다음은 선택사항입니다.

서버에서 파일을 수정하면 서버를 껐다가 다시 켜줘야 하는 번거로움이 있는데

파일 수정하고 저장 시 자동으로 서버를 재시작하게 해주는 유틸이 있습니다

nodemon입니다.

 

npm install -g nodemon

 

 

실행하는 방법은 nodemon 파일 이름입니다

만약 오류 뜬다면 npx nodemon 파일 이름 써보세요

 

파일 server.js를 만들어주세요

 

 

server.js

const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const port = 7777;
const { Server } = require("socket.io");
const io = new Server(server);

io.use((socket, next) => {
  if (socket.handshake.query.token === "UNITY" && socket.handshake.query.version === "0.1") {
    next();
  } else {
    next(new Error("인증 오류 "));
  }
});
var Users = [];
var Rooms = [];

io.on('connection', socket => {
  Users[socket.id] = {
    id: socket.id,
    nickname: "",
    Room: "",
  }

  socket.on('disconnect', zz => {
    console.log("연결끊김 : " + socket.id);
  })

})


server.listen(port, () => {
  console.log('listening on *:' + port);
});

 

 

유니티 스크립트 하나 만들어주세요

using System;
using System.Collections.Generic;
using SocketIOClient;
using UnityEngine;
using UnityEngine.SceneManagement;


public class SocketManager : MonoBehaviour
{
    public SocketIOUnity socket;

    void Start()
    {
        var uri = new Uri("http://127.0.0.1:7777");
        socket = new SocketIOUnity(uri, new SocketIOOptions
        {
            Query = new Dictionary<string, string>
            {
                { "token", "UNITY" },
                { "version", "0.1" }
            },
            Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
        });
        //인증 부분 입니다. 저는 따로 version 만들었습니다.
        
        
        socket.Connect();
        //연결합니다.


        socket.OnConnected += (sender, e) =>
        {
            Debug.Log("연결");
        };
        socket.OnDisconnected += (sender, e) => { Debug.Log("disconnect: " + e); };
    }


    private void OnDestroy()
    {
        socket.Disconnect();
    }
}

만약 네임스페이스를 사용하고 싶다면

var uri2 = new Uri("http://127.0.0.1:7777/네임스페이스이름 ");

 

이런 식으로 사용 가능합니다.

 

저 같은 경우에는 Intro 씬에서 접속 확인을 받고 접속이 완료라면 Loby씬으로 이동합니다.

 

Loby씬에서 닉네임을 설정하고

같은 닉네임이 있다면 못 들어갑니다

그리고 방을 들어가거나 생성을 하는데

방도 같은 이름의 방이 있다면 오류가 뜨게 했습니다.

 

void Start()
{
	//...

    socket.OnConnected += (sender, e) =>
    {
        UnityMainThreadDispatcher.Instance().Enqueue(() => { SceneManager.LoadScene("Loby"); });
    };
    
    
    //UnityMainThreadDispatcher.Instance().Enqueue(() 는 큐를 이용해서 메인쓰레드에서 처리하도록 합니다.
    //이 방식이 아니여도 따로 큐를 처리해주게 하거나 코루틴을 이용해서 처리하시면 됩니다.
    //아니면 따로 socket.OnUnityThread를 받는 이벤트를 받아서 넘기셔도 됩니다.
    
}

 

 

 

 

 

 

전 딕셔너리를 이용해서 해당 닉네임이 있거나 방 이름이 있다면 오류가 뜨게 했습니다.

 

socket.io 에는 id 가 있는데 연결 시 고유 문자를 하나 줍니다. 이걸로

User들을 파악했습니다.

 

var Users = [];
var Rooms = [];

io.on('connection', socket => {
  
  Users[socket.id] = {
    id: socket.id,
    nickname: "",
    Room: "",
  }
  
  
 socket.on('disconnect', zz => {
    console.log("연결끊김 : " + socket.id);
    delete Users[socket.id]
    //연결이 끊기면 딕셔너리 삭제
  })
 
 
}

 

Users의 딕셔너리는 

id와 nickname 그리고 Room 3가지를 저장합니다.

 

연결 시 User를 저장하고

연결이 끊기면 삭제합니다.

 

딕셔너리 관련 함수는 이 블로그 글 한번 보시면 됩니다.

https://ourcstory.tistory.com/158

 

size를 구해서 현재 접속한 모든 플레이어의 수를 파악이 가능합니다.

 

("이름" in 딕셔너리변수이름) 를 사용해서 해당 딕셔너리가 있는지도 파악도 가능합니다.

 

 

server.js

socket.on('LoginCheck', name => {
    //접속하기 버튼을 누르면 입장합니다.

    var check = true;
    //변수 하나를 생성

    for (var k in Users) {
      if (Users[k].nickname == name) {
        check = false;
        break;
      }
    }
    //닉네임이 있는지 없는지 파악합니다.

    if (check) {
      //닉네임이 없다면 생성
      Users[socket.id].nickname = name
      //nickname 설정
      console.log(name + ": 로비진입 성공!")
      socket.emit('Login')
    }
    else {
      //닉네임이 있다면 오류
      console.log("닉네임 겹침!")
      socket.emit('LoginFailed')
    }
})

 

 

 

 

GameManager.cs

 

using System;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager inst;


    public String nickName; //닉네임
    public String room; // 현재 접속한 방이름
    [Range(2, 8)] public int maxRoom = 2; // 방옵션
    public bool IsChat = false;//현재 채팅중인지 아닌지

    public GameObject lobyOb;
    public GameObject joinOb;
    public GameObject chatOb;
    public GameObject loadingOb;
    public GameObject loginWarningOb;
    public GameObject roomWarningOb;
    public ChatManager chatManager;
    public LobyManager lobyManager;
    
    //UI들

    private void Awake()
    {
        inst = this;
    }
}

 

 

LobyManager.cs

 

 

using System.Collections.Generic;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;

public class LobyManager : MonoBehaviour
{
    public TextMeshProUGUI nickname;
    public TMP_InputField logiInputField;

    public void LoginBtn()
    
    {
        if (logiInputField.text == "")
        {
            return;
        }
        GameManager.inst.loadingOb.SetActive(true);
        //통신을 보내고 잠시 로딩창이 뜨게 했습니다. 연속누르게 방지용
        SocketManager.inst.socket.Emit("LoginCheck", logiInputField.text);
        //LoginCheck 라는 이벤트를 보냅니다. 
    }


    private void Start()
    {
        logiInputField.Select();
        //맨처음 닉네임인풋필드를 선택되게 설정
        
        SocketManager.inst.socket.OnUnityThread("Login", data =>
        {
        //접속 성공!
            GameManager.inst.nickName = logiInputField.text;
            GameManager.inst.loadingOb.SetActive(false);
            GameManager.inst.lobyOb.SetActive(false);
            GameManager.inst.joinOb.SetActive(true);
            nickname.text = logiInputField.text;
            
            SocketManager.inst.socket.Emit("RoomListCheck", null);
            //룸리스트를 요청해서 받습니다.
        });
        SocketManager.inst.socket.OnUnityThread("LoginFailed", data =>
        {
        //로그인 실패
        
            GameManager.inst.loadingOb.SetActive(false);
            GameManager.inst.loginWarningOb.SetActive(true);
            //경고창 쓰게합니다
        });
    }


}

 

 

 

 

맨 위에는 본인 닉네임이 설정되어있고

왼쪽 최대 인원은 토글 그룹으로 설정했습니다

오른쪽은 스크롤 뷰를 이용해서 룸 리스트들이 나오게 설정했습니다.

 

방을 만들 때는 Rooms이라는 딕셔너리를 이용해서 처리했습니다.

 

Server.js

io.on('connection', socket => {

// .....

  function RoomResetGo() {
	//모든 방을 보내는 함수입니다.
    var roomcheck = [];

    for (room in Rooms) {
    
      roomcheck.push({
        currentCnt: Rooms[room].currentCnt,
        RoomMaxCnt: Rooms[room].maxCnt,
        name: room
      })
      //currentCnt , RoomMaxCnt,name 이라는 데이터를 보냅니다.

    }
    
    io.emit('RoomReset', roomcheck)
  }




socket.on('CreateCheck', (data, data2) => {
    if (data in Rooms) {
      //방이 있는지 없는지 확인
      console.log(" 방이름 겹침!")
      socket.emit('CreateFailed')
      //방생성 실패

    }
    else {
      //방생성 성공
      socket.join(data);
      //들어갑니다.

      Users[socket.id].Room = data


      Rooms[data] = {
        currentCnt: 1,
        maxCnt: Number(data2)
      }

      console.log(data + ": 방진입 성공!")

      socket.emit('Create')
      //성공했다고 이벤트를 보냅니다.


      RoomResetGo()
      //방 목록을 전부 보내는 이벤트를 실행합니다.
    }
    
    
 }

 

 

LobyManager.cs

public void CreateBtn()
//방생성 버튼시 실행하는 함수
{
    if (createInputField.text == "")
    {
        return;
    }

    GameManager.inst.loadingOb.SetActive(true);

    SocketManager.inst.socket.Emit("CreateCheck", createInputField.text, GameManager.inst.maxRoom);
}




SocketManager.inst.socket.OnUnityThread("Create", data =>
{
    GameManager.inst.room = createInputField.text;
    GameManager.inst.loadingOb.SetActive(false);
    GameManager.inst.joinOb.SetActive(false);
    GameManager.inst.chatOb.SetActive(true);
    
    GameManager.inst.chatManager.ChatStart();
    //채팅 실행
});
SocketManager.inst.socket.OnUnityThread("CreateFailed", data =>
{
    GameManager.inst.loadingOb.SetActive(false);
    GameManager.inst.roomWarningOb.SetActive(true);
    //방생성 오류메세지
});

 

 

방생성은 미리 프리팹을 만들어서 오른쪽 스크롤 뷰에다가 생성시켰습니다.

생성시킬 때는 RoomInfo라는 클래스를 만들어서 데이터 저장용으로 했습니다.

 

LobyManager.cs

[System.Serializable]
public class RoomInfo
{
    public int currentCnt;
    public int RoomMaxCnt;
    public string name;
}


public class LobyManager : MonoBehaviour
{
	//...

    public RoomInfo[] roomsInfo; //방정보를 배열
    
    public Transform roomParent;//생성시킬 곳
    public GameObject roomPrefab;//프리팹
    
    public List<GameObject> roomobs;
    
    
    
    private void Start()
    {
    
        SocketManager.inst.socket.OnUnityThread("RoomReset", data =>
        //룸 리셋 ,모두가 받는 이벤트
            {
                if (data.ToString() == "[[]]")
                {
                    return;
                }
                roomsInfo = JsonConvert.DeserializeObject<RoomInfo[]>(data.GetValue(0).ToString());
                RoomReset();
            });
            
        SocketManager.inst.socket.OnUnityThread("RoomList", data =>
        //룸 리셋 ,개인이 받을 때 이벤트
        {
            if (data.ToString() == "[[]]")
            {
                return;
            }

            roomsInfo = JsonConvert.DeserializeObject<RoomInfo[]>(data.GetValue(0).ToString());
            //받아봅니다.
            RoomReset();
        });
    }
    
    
    public void RoomReset()
    {
        if (roomobs.Count > 0)
        {
            for (int i = 0; i < roomobs.Count; i++)
            {
                Destroy(roomobs[i]);
                //기존에 있는 방을 전부 파괴시킵니다.
                //오브젝트 풀링방법을 사용하는게 가장 베스트인데 임시로 만들었습니다.
            }
        }
        roomobs.Clear();
        
        
        for (int i = 0; i < roomsInfo.Length; i++)
        {
            if (roomsInfo[i].currentCnt < roomsInfo[i].RoomMaxCnt)
            //인원을 체크합니다.
            {
                GameObject room = Instantiate(roomPrefab, roomParent);
                //프리팹 생성
                
                var roominfo = room.GetComponent<RoomPrefab>();
                //프리팹안에있는 스크립트 가져옴
                
                roominfo.nameText.text = roomsInfo[i].name;
                //이름 설정
                roominfo.name = roomsInfo[i].name;
                roominfo.cntText.text = $"{roomsInfo[i].currentCnt}/{roomsInfo[i].RoomMaxCnt}";
                //인원수 설정
                roomobs.Add(room);
                //리스트안에 넣음
            }
        }
    }
    
    
    
    
    
using System;
using TMPro;
using UnityEngine;

public class RoomPrefab : MonoBehaviour
{
    public TextMeshProUGUI nameText;
    public TextMeshProUGUI cntText;
    public String name;

    public void ClickFunc()
    {
        GameManager.inst.lobyManager.JoinRoom(name);
    }
}
    
    
    
    
    
}

 

 

방을 클릭할 때 함수입니다

 

Server.cs

 

socket.on('JoinRoomCheck', (roomname) => {

    if (roomname in Rooms && Rooms[roomname].currentCnt < Rooms[roomname].maxCnt) {
		//체크를 합니다. 방이없거나 최대인원을 초과하면 오류를냅니다.
      socket.join(roomname)
      //join합니다
      socket.emit('Join', roomname)
      
      Users[socket.id].Room = roomname
      Rooms[roomname].currentCnt++
      //방의 정보를 갱신합니다.



      var check = []
      socket.adapter.rooms.get(roomname).forEach((a) => {
        check.push(Users[a].nickname)
      })
      socket.to(roomname).emit('PlayerReset', check)
      //이 함수는 방안에있는 플레이어의 이름을 보냅니다.
      
      
      RoomResetGo()
    }
    else {
    //참가 실패
      socket.emit('JoinFailed')
    }
  })

 

 

LobyManager.cs

 

SocketManager.inst.socket.OnUnityThread("Join", data =>
{
    GameManager.inst.room = data.GetValue(0).ToString();
    GameManager.inst.loadingOb.SetActive(false);
    GameManager.inst.joinOb.SetActive(false);
    GameManager.inst.chatOb.SetActive(true);
    
    
    GameManager.inst.chatManager.ChatStart();
    //방을 참가합니다.
});
SocketManager.inst.socket.OnUnityThread("JoinFailed", data =>
{
    GameManager.inst.loadingOb.SetActive(false);
    SocketManager.inst.socket.Emit("RoomListCheck", null);
    //오류가 났으니까 방을 갱신시켜봅니다.
});

 

 

 

왼쪽 대화창을 스크롤 뷰로 만들었고요

오른쪽은 현재 참가한 플레이어들이 나옵니다.

 

 

ChatManager.cs

 

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;

public class ChatManager : MonoBehaviour
{
    [SerializeField] private GameObject textPrefab; // 채팅프리팹
    [SerializeField] private Transform textParent; //채팅 생성시킬곳
    [SerializeField] private TMP_InputField inputField;//채팅 입력칸
    [SerializeField] private Transform playerparent; //플레이어 목록
    [SerializeField] private string[] players; //플레이어 목록들
    
    [SerializeField] private List<GameObject> textobs;


    private void Start()
    {
        SocketManager.inst.socket.OnUnityThread("ChatOn", data =>
        //채팅을 시작합니다.
        {
            GameManager.inst.loadingOb.SetActive(false);
            //로딩 끄게합니다.
            
            players = JsonConvert.DeserializeObject<String[]>(data.GetValue(0).ToString());
            PlayerReSet();
            //플레이어 목록을 받고 설정합니다.
        });
        SocketManager.inst.socket.OnUnityThread("PlayerReset", data =>
        //플레이어 목록을 갱신합니다. 나가거나 들어올 때 방안에있는 사람들이 받는 이벤트입니다.
        {
            players = JsonConvert.DeserializeObject<String[]>(data.GetValue(0).ToString());
            PlayerReSet();
        });
        SocketManager.inst.socket.OnUnityThread("LeaveRoom", data =>
        //방을 나갑니다
        {
            GameManager.inst.loadingOb.SetActive(false);
            
            GameManager.inst.lobyManager.RoomReset();
            //방을 나갔으니 방갱신을 해야합니다.
        });
        
        SocketManager.inst.socket.OnUnityThread("ChatGet",
            data => { ChatGet(data.GetValue(0).ToString(), data.GetValue(1).ToString()); });
        //채팅을 받고 올리는 이벤트입니다.
    }

    public void LeaveBtn()
    //나가기 버튼을 누를 때
    {
        SocketManager.inst.socket.Emit("LeaveRoomCheck", GameManager.inst.room, players.Length);
        GameManager.inst.chatOb.SetActive(false);
        GameManager.inst.joinOb.SetActive(true);
        GameManager.inst.loadingOb.SetActive(true);
        GameManager.inst.room = "";
        GameManager.inst.IsChat = false;
    }

    private void PlayerReSet()
    {
        for (int i = 0; i < 8; i++)
        {
            playerparent.GetChild(i).GetComponent<TextMeshProUGUI>().text = "";
        }

        for (int i = 0; i < players.Length; i++)
        {
            playerparent.GetChild(i).GetComponent<TextMeshProUGUI>().text = players[i];
        }
    }


    public void ChatStart()
    //채팅 실행
    {
        GameManager.inst.IsChat = true;
        for (int i = 0; i < 8; i++)
        {
            playerparent.GetChild(i).GetComponent<TextMeshProUGUI>().text = "";
        }
        if (textobs.Count > 0)
        {
            for (int i = 0; i < textobs.Count; i++)
            {
                Destroy(textobs[i]);
            }
        }
        textobs.Clear();
        //기존에 있던 채팅 모두 삭제합니다.
        GameManager.inst.loadingOb.SetActive(true);
        //로딩
        SocketManager.inst.socket.Emit("ChatCheck", GameManager.inst.room);
        
    }

    public void UpdateChat()
    //채팅을 입력시 이벤트
    {
        if (inputField.text.Equals(""))
        {
            return;
        }
        //아무것도없다면 리턴

        GameObject ob = Instantiate(textPrefab, textParent);
        ob.GetComponent<TextMeshProUGUI>().text = $"<color=red>{GameManager.inst.nickName} </color>: {inputField.text}";
        textobs.Add(ob);
        
        SocketManager.inst.socket.Emit("Chat", GameManager.inst.nickName, inputField.text, GameManager.inst.room);
        //딴사람들에게도 채팅내용을 받아야하니 이벤트를 보냅니다.
        
        inputField.text = "";
    }

    public void ChatGet(string nickname, string text)
    //다른사람들이 채팅 이벤트 받으면 생성시킵니다.
    {
        GameObject ob = Instantiate(textPrefab, textParent);
        textobs.Add(ob);
        ob.GetComponent<TextMeshProUGUI>().text = $"{nickname} : {text}";
    }

}

 

 

 

 

Server.cs

socket.on('Chat', (nick, text, room) => {
    //채팅을 보냅니다.

    console.log(nick + ": " + text)
    socket.to(room).emit('ChatGet', nick, text)
    //보인을 제외한 방에 존재한 사람들에게 보냅니다.

  })
  socket.on('ChatCheck', (data) => {


    var check = []
    socket.adapter.rooms.get(data).forEach((a) => {
      check.push(Users[a].nickname)
    })
    //방안에있는 플레이어들의 목록을 불러옵니다.


    socket.emit('ChatOn', check)
  })
  
  
  socket.on('LeaveRoomCheck', (data, data2) => {
    //방을 나갑니다

    socket.leave(data)
    //leave를 사용합니다.

    if (Number(data2) <= 1) {
      //현재 방인원이 1이라면 삭제를 시킵니다.
      delete Rooms[Users[socket.id].Room]
    }
    else {

      Rooms[data].currentCnt--
      //방 인원 하나 뺍니다


      var check = []
      socket.adapter.rooms.get(data).forEach((a) => {
        check.push(Users[a].nickname)
      })
      socket.to(data).emit('PlayerReset', check)
      //현재 방인원 플레이어 목록을 갱신 시켜줍니다.

    }
    RoomResetGo()
    //방갱신 
    socket.emit('LeaveRoom')
    //이벤트보냄
    
    Users[socket.id].Room = ""
    //정보 초기화
  })

 

 

여기서 방에 참가한 상태에서 나가게 돼버리면 오류가 발생하니 따로 처리를 해야 합니다.

 

socket.on('disconnect', zz => {
    console.log("연결끊김 : " + socket.id);
    //console.log(Users[socket.id].Room);
    if (Users[socket.id].Room != "") {
      //해당 유저가 방안이라면 
      if (Rooms[Users[socket.id].Room].currentCnt == 1) {
        //인원이 1이라면 삭제
        delete Rooms[Users[socket.id].Room]
      }
      else {
        Rooms[Users[socket.id].Room].currentCnt--
        //룸 인원 빼기


        var check = []
        socket.adapter.rooms.get(Users[socket.id].Room).forEach((a) => {
          check.push(Users[a].nickname)
        })
        socket.to(Users[socket.id].Room).emit('PlayerReset', check)
        //그 안에있는 사람들 플레이어 목록 갱신



        RoomResetGo()
        //방 리셋 



      }
    }

    delete Users[socket.id]
    //유저 정보 삭제
  })

 

간단한 룸 형식의 채팅방을 구현해봤습니다.

 

유니티 자체적으로 비활성화 시 화면 갱신이 안 되는 부분이 있는데

 

Edit->Project Settings->에서 Run In Background 체크하면 됩니다.

 

 

 

 

로컬 서버에서 열고 외부에서 접속을 하고 싶으면

방화벽 풀고 포트를 허용해줘야 하는데

따로 설정없이 바로 가능한 방법이 있습니다.

ngrok라는 모듈을 사용하면 바로 가능합니다.

무료 버전은 8시간밖에 유지가 안 되는 단점이 있는데 잠깐 테스트용이라면 괜찮습니다

저도 테스트해봤는데 외부에서 접속이 잘 됩니다.

https://ngrok.com/

 

사용법

https://velog.io/@kya754/ngrok-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

 

 

 

 

깃허브 주소

 

https://github.com/wolstar415/unity_socket-Chat

 

 

코드보기

 

server.js

더보기
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const port = 7777;
const { Server } = require("socket.io");
const io = new Server(server);

io.use((socket, next) => {
  if (socket.handshake.query.token === "UNITY" && socket.handshake.query.version === "0.1") {
    next();
  } else {
    next(new Error("인증 오류 "));
  }
});
var Users = [];
var Rooms = [];

io.on('connection', socket => {

  Users[socket.id] = {
    id: socket.id,
    nickname: "",
    Room: "",
  }

  function RoomResetGo() {

    var roomcheck = [];

    for (room in Rooms) {
      roomcheck.push({
        currentCnt: Rooms[room].currentCnt,
        RoomMaxCnt: Rooms[room].maxCnt,
        name: room
      })

    }
    io.emit('RoomReset', roomcheck)
  }


  socket.on('LoginCheck', name => {
    //접속하기 버튼을 누르면 입장합니다.

    var check = true;
    //변수 하나를 생성

    for (var k in Users) {
      if (Users[k].nickname == name) {
        check = false;
        break;
      }
    }
    //닉네임이 있는지 없는지 파악합니다.

    if (check) {
      //닉네임이 없다면 생성
      Users[socket.id].nickname = name
      //nickname 설정
      console.log(name + ": 로비진입 성공!")
      socket.emit('Login')
    }
    else {
      //닉네임이 있다면 오류
      console.log("닉네임 겹침!")
      socket.emit('LoginFailed')
    }
  })



  socket.on('JoinRoomCheck', (roomname) => {

    if (roomname in Rooms && Rooms[roomname].currentCnt < Rooms[roomname].maxCnt) {

      socket.join(roomname)
      socket.emit('Join', roomname)
      Users[socket.id].Room = roomname
      Rooms[roomname].currentCnt++

      var check = []
      socket.adapter.rooms.get(roomname).forEach((a) => {
        check.push(Users[a].nickname)
      })

      socket.to(roomname).emit('PlayerReset', check)
      RoomResetGo()
    }
    else {
      socket.emit('JoinFailed')
    }
  })
  socket.on('CreateCheck', (data, data2) => {
    if (data in Rooms) {
      //방이 있는지 없는지 확인
      console.log(" 방이름 겹침!")
      socket.emit('CreateFailed')
      //방생성 실패

    }
    else {
      //방생성 성공
      socket.join(data);
      //들어갑니다.

      Users[socket.id].Room = data


      Rooms[data] = {
        currentCnt: 1,
        maxCnt: Number(data2)
      }

      console.log(data + ": 방진입 성공!")

      socket.emit('Create')
      //성공했다고 이벤트를 보냅니다.


      RoomResetGo()
      //방 목록을 전부 보내는 이벤트를 실행합니다.
    }

  })
  socket.on('LeaveRoomCheck', (data, data2) => {
    //방을 나갑니다

    socket.leave(data)
    //leave를 사용합니다.

    if (Number(data2) <= 1) {
      //현재 방인원이 1이라면 삭제를 시킵니다.
      delete Rooms[Users[socket.id].Room]
    }
    else {

      Rooms[data].currentCnt--
      //방 인원 하나 뺍니다


      var check = []
      socket.adapter.rooms.get(data).forEach((a) => {
        check.push(Users[a].nickname)
      })
      socket.to(data).emit('PlayerReset', check)
      //현재 방인원 플레이어 목록을 갱신 시켜줍니다.

    }
    RoomResetGo()
    socket.emit('LeaveRoom')
    Users[socket.id].Room = ""
  })
  socket.on('RoomListCheck', (data) => {
    if (socket.adapter.rooms.size == 1) {
      return
    }
    var roomcheck = [];

    for (room in Rooms) {
      roomcheck.push({
        currentCnt: Rooms[room].currentCnt,
        RoomMaxCnt: Rooms[room].maxCnt,
        name: room
      })

    }

    console.log(roomcheck)
    socket.emit('RoomList', roomcheck)
  })


  socket.on('Chat', (nick, text, room) => {
    //채팅을 보냅니다.

    console.log(nick + ": " + text)
    socket.to(room).emit('ChatGet', nick, text)
    //보인을 제외한 방에 존재한 사람들에게 보냅니다.

  })
  socket.on('ChatCheck', (data) => {


    var check = []
    socket.adapter.rooms.get(data).forEach((a) => {
      check.push(Users[a].nickname)
    })
    //방안에있는 플레이어들의 목록을 불러옵니다.


    socket.emit('ChatOn', check)
  })
  console.log("연결함 : " + socket.id);


  socket.on('disconnect', zz => {
    console.log("연결끊김 : " + socket.id);
    //console.log(Users[socket.id].Room);
    if (Users[socket.id].Room != "") {
      //해당 유저가 방안이라면 
      if (Rooms[Users[socket.id].Room].currentCnt == 1) {
        //인원이 1이라면 삭제
        delete Rooms[Users[socket.id].Room]
      }
      else {
        Rooms[Users[socket.id].Room].currentCnt--
        //룸 인원 빼기


        var check = []
        socket.adapter.rooms.get(Users[socket.id].Room).forEach((a) => {
          check.push(Users[a].nickname)
        })
        socket.to(Users[socket.id].Room).emit('PlayerReset', check)
        //그 안에있는 사람들 플레이어 목록 갱신



        RoomResetGo()
        //방 리셋 



      }
    }

    delete Users[socket.id]
    //유저 정보 삭제
  })



});




server.listen(port, () => {
  console.log('listening on *:' + port);
});

SocketManager.cs

더보기
using System;
using System.Collections.Generic;
using SocketIOClient;
using UnityEngine;
using UnityEngine.SceneManagement;


public class SocketManager : MonoBehaviour
{
    public SocketIOUnity socket;

    public static SocketManager inst;

    private void Awake()
    {
        if (inst == null) //instance가 null. 즉, 시스템상에 존재하고 있지 않을때
        {
            inst = this; //내자신을 instance로 넣어줍니다.
            DontDestroyOnLoad(gameObject); //OnLoad(씬이 로드 되었을때) 자신을 파괴하지 않고 유지
        }
        else
        {
            if (inst != this) //instance가 내가 아니라면 이미 instance가 하나 존재하고 있다는 의미
                Destroy(this.gameObject); //둘 이상 존재하면 안되는 객체이니 방금 AWake된 자신을 삭제
        }
    }


    void Start()
    {
        var uri = new Uri("http://127.0.0.1:7777");
        socket = new SocketIOUnity(uri, new SocketIOOptions
        {
            Query = new Dictionary<string, string>
            {
                { "token", "UNITY" },
                { "version", "0.1" }
            },
            Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
        });
        socket.Connect();

        socket.OnConnected += (sender, e) =>
        {
            UnityMainThreadDispatcher.Instance().Enqueue(() => { SceneManager.LoadScene("Loby"); });
        };
        socket.OnDisconnected += (sender, e) => { Debug.Log("disconnect: " + e); };
    }

    

    private void OnDestroy()
    {
        socket.Disconnect();
    }
}

 

 

 

 

GamaManager.cs

더보기
using System;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager inst;


    public String nickName; //닉네임
    public String room; // 현재 접속한 방이름
    [Range(2, 8)] public int maxRoom = 2; // 방옵션
    public bool IsChat = false;//현재 채팅중인지 아닌지

    public GameObject lobyOb;
    public GameObject joinOb;
    public GameObject chatOb;
    public GameObject loadingOb;
    public GameObject loginWarningOb;
    public GameObject roomWarningOb;
    public ChatManager chatManager;
    public LobyManager lobyManager;
    
    //UI들

    private void Awake()
    {
        inst = this;
    }
}

 

 

LobyManager.cs

더보기
using System.Collections.Generic;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;

[System.Serializable]
public class RoomInfo
{
    public int currentCnt;
    public int RoomMaxCnt;
    public string name;
}

public class LobyManager : MonoBehaviour
{
    public TextMeshProUGUI nickname;
    public TMP_InputField logiInputField;
    public TMP_InputField createInputField;
    public RoomInfo[] roomsInfo;
    public Transform roomParent;
    public GameObject roomPrefab;
    public List<GameObject> roomobs;

    public void LoginBtn()
    //접속 버튼 누르면 실행
    {
        if (logiInputField.text == "")
        {
            return;
        }


        GameManager.inst.loadingOb.SetActive(true);

        SocketManager.inst.socket.Emit("LoginCheck", logiInputField.text);
    }

    public void CreateBtn()
    //방생성 버튼시 실행하는 함수
    {
        if (createInputField.text == "")
        {
            return;
        }

        GameManager.inst.loadingOb.SetActive(true);

        SocketManager.inst.socket.Emit("CreateCheck", createInputField.text, GameManager.inst.maxRoom);
    }

    public void OnEndEditEventMethod()
    {
        if (Input.GetKeyDown(KeyCode.Return))
        {
            LoginBtn();
        }
    }

    private void Start()
    {
        logiInputField.Select();


        SocketManager.inst.socket.OnUnityThread("Login", data =>
        {
            GameManager.inst.nickName = logiInputField.text;
            GameManager.inst.loadingOb.SetActive(false);
            GameManager.inst.lobyOb.SetActive(false);
            GameManager.inst.joinOb.SetActive(true);
            nickname.text = logiInputField.text;
            SocketManager.inst.socket.Emit("RoomListCheck", null);
        });
        SocketManager.inst.socket.OnUnityThread("LoginFailed", data =>
        {
            GameManager.inst.loadingOb.SetActive(false);
            GameManager.inst.loginWarningOb.SetActive(true);
        });

        SocketManager.inst.socket.OnUnityThread("Create", data =>
        {
            GameManager.inst.room = createInputField.text;
            GameManager.inst.loadingOb.SetActive(false);
            GameManager.inst.joinOb.SetActive(false);
            GameManager.inst.chatOb.SetActive(true);
            GameManager.inst.chatManager.ChatStart();
        });
        SocketManager.inst.socket.OnUnityThread("CreateFailed", data =>
        {
            GameManager.inst.loadingOb.SetActive(false);
            GameManager.inst.roomWarningOb.SetActive(true);
        });
        SocketManager.inst.socket.OnUnityThread("RoomReset", data =>
        {
            roomsInfo = JsonConvert.DeserializeObject<RoomInfo[]>(data.GetValue(0).ToString());
            RoomReset();
        });
        SocketManager.inst.socket.OnUnityThread("RoomList", data =>
        {
            if (data.ToString() == "[[]]")
            {
                return;
            }

            roomsInfo = JsonConvert.DeserializeObject<RoomInfo[]>(data.GetValue(0).ToString());
            RoomReset();
        });
        SocketManager.inst.socket.OnUnityThread("Join", data =>
        {
            GameManager.inst.room = data.GetValue(0).ToString();
            GameManager.inst.loadingOb.SetActive(false);
            GameManager.inst.joinOb.SetActive(false);
            GameManager.inst.chatOb.SetActive(true);
            GameManager.inst.chatManager.ChatStart();
        });
        SocketManager.inst.socket.OnUnityThread("JoinFailed", data =>
        {
            GameManager.inst.loadingOb.SetActive(false);
            SocketManager.inst.socket.Emit("RoomListCheck", null);
        });
    }


    public void MaxRoomChange(int value)
    {
        GameManager.inst.maxRoom = value;
    }

    public void JoinRoom(string name)
    {
        GameManager.inst.loadingOb.SetActive(true);
        SocketManager.inst.socket.Emit("JoinRoomCheck", name);
    }

    public void RoomReset()
    {
        if (roomobs.Count > 0)
        {
            for (int i = 0; i < roomobs.Count; i++)
            {
                Destroy(roomobs[i]);
            }
        }

        roomobs.Clear();
        for (int i = 0; i < roomsInfo.Length; i++)
        {
            if (roomsInfo[i].currentCnt < roomsInfo[i].RoomMaxCnt)
            {
                GameObject room = Instantiate(roomPrefab, roomParent);
                var roominfo = room.GetComponent<RoomPrefab>();
                roominfo.nameText.text = roomsInfo[i].name;
                roominfo.name = roomsInfo[i].name;
                roominfo.cntText.text = $"{roomsInfo[i].currentCnt}/{roomsInfo[i].RoomMaxCnt}";
                roomobs.Add(room);
            }
        }
    }
}

 

RoomPrefab.cs

더보기
using System;
using TMPro;
using UnityEngine;

public class RoomPrefab : MonoBehaviour
{
    public TextMeshProUGUI nameText;
    public TextMeshProUGUI cntText;
    public String name;

    public void ClickFunc()
    {
        GameManager.inst.lobyManager.JoinRoom(name);
    }
}

 

ChatManager

더보기
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using TMPro;
using UnityEngine;

public class ChatManager : MonoBehaviour
{
    [SerializeField] private GameObject textPrefab; // 채팅프리팹
    [SerializeField] private Transform textParent; //채팅 생성시킬곳
    [SerializeField] private TMP_InputField inputField;//채팅 입력칸
    [SerializeField] private Transform playerparent; //플레이어 목록
    [SerializeField] private string[] players; //플레이어 목록들
    
    [SerializeField] private List<GameObject> textobs;


    private void Start()
    {
        SocketManager.inst.socket.OnUnityThread("ChatOn", data =>
        //채팅을 시작합니다.
        {
            GameManager.inst.loadingOb.SetActive(false);
            //로딩 끄게합니다.
            
            players = JsonConvert.DeserializeObject<String[]>(data.GetValue(0).ToString());
            PlayerReSet();
            //플레이어 목록을 받고 설정합니다.
        });
        SocketManager.inst.socket.OnUnityThread("PlayerReset", data =>
        //플레이어 목록을 갱신합니다. 나가거나 들어올 때 방안에있는 사람들이 받는 이벤트입니다.
        {
            players = JsonConvert.DeserializeObject<String[]>(data.GetValue(0).ToString());
            PlayerReSet();
        });
        SocketManager.inst.socket.OnUnityThread("LeaveRoom", data =>
        //방을 나갑니다
        {
            GameManager.inst.loadingOb.SetActive(false);
            
            GameManager.inst.lobyManager.RoomReset();
            //방을 나갔으니 방갱신을 해야합니다.
        });
        
        SocketManager.inst.socket.OnUnityThread("ChatGet",
            data => { ChatGet(data.GetValue(0).ToString(), data.GetValue(1).ToString()); });
        //채팅을 받고 올리는 이벤트입니다.
    }

    public void LeaveBtn()
    //나가기 버튼을 누를 때
    {
        SocketManager.inst.socket.Emit("LeaveRoomCheck", GameManager.inst.room, players.Length);
        GameManager.inst.chatOb.SetActive(false);
        GameManager.inst.joinOb.SetActive(true);
        GameManager.inst.loadingOb.SetActive(true);
        GameManager.inst.room = "";
        GameManager.inst.IsChat = false;
    }

    private void PlayerReSet()
    {
        for (int i = 0; i < 8; i++)
        {
            playerparent.GetChild(i).GetComponent<TextMeshProUGUI>().text = "";
        }

        for (int i = 0; i < players.Length; i++)
        {
            playerparent.GetChild(i).GetComponent<TextMeshProUGUI>().text = players[i];
        }
    }

    public void OnEndEditEventMethod()
    {
        if (Input.GetKeyDown(KeyCode.Return))
        {
            UpdateChat();
        }
    }

    public void ChatStart()
    //채팅 실행
    {
        GameManager.inst.IsChat = true;
        for (int i = 0; i < 8; i++)
        {
            playerparent.GetChild(i).GetComponent<TextMeshProUGUI>().text = "";
        }
        if (textobs.Count > 0)
        {
            for (int i = 0; i < textobs.Count; i++)
            {
                Destroy(textobs[i]);
            }
        }
        textobs.Clear();
        //기존에 있던 채팅 모두 삭제합니다.
        GameManager.inst.loadingOb.SetActive(true);
        //로딩
        SocketManager.inst.socket.Emit("ChatCheck", GameManager.inst.room);
        
    }

    public void UpdateChat()
    //채팅을 입력시 이벤트
    {
        if (inputField.text.Equals(""))
        {
            return;
        }
        //아무것도없다면 리턴

        GameObject ob = Instantiate(textPrefab, textParent);
        ob.GetComponent<TextMeshProUGUI>().text = $"<color=red>{GameManager.inst.nickName} </color>: {inputField.text}";
        textobs.Add(ob);
        
        SocketManager.inst.socket.Emit("Chat", GameManager.inst.nickName, inputField.text, GameManager.inst.room);
        //딴사람들에게도 채팅내용을 받아야하니 이벤트를 보냅니다.
        
        inputField.text = "";
    }

    public void ChatGet(string nickname, string text)
    //다른사람들이 채팅 이벤트 받으면 생성시킵니다.
    {
        GameObject ob = Instantiate(textPrefab, textParent);
        textobs.Add(ob);
        ob.GetComponent<TextMeshProUGUI>().text = $"{nickname} : {text}";
    }

    private void Update()
    {
        if (GameManager.inst.IsChat)
        {
            if (Input.GetKeyDown(KeyCode.Return) && inputField.isFocused == false)
            {
                inputField.ActivateInputField();
            }
        }
    }
}