[Godot] Network Multi Play - Chating 네트워크 멀티 플레이 - 채팅
Godot 2026. 9. 9. 11:21 |고수준 네트워크 멀티 플레이를 사용해 보자.

위와 같이 컨트롤 - 버튼으로 화면을 구성한다.
Control 노드의 스크립트는 아래와 같이 작성한다.
using Godot;
public partial class Test : Control
{
private const int Port = 8910; // 포트 번호 (임의의 4자리 숫자)
private const string ServerIp = "127.0.0.1"; // 내 컴퓨터 안에서 테스트할 주소 (로컬호스트)
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
GetNode<Button>("Button").Pressed += _on_host_button_pressed;
GetNode<Button>("Button2").Pressed += _on_join_button_pressed;
// 누군가 방에 성공적으로 들어왔을 때 모든 사람의 컴퓨터에서 실행된다.
// 방장과 이미 방에 있던 사람들 뿐만 아니라, 새로 들어온 사람의 컴퓨터에서도 실행된다.
Multiplayer.PeerConnected += OnPeerConnected;
// 누군가 접속을 끊고 나갔을 때 나간 사람을 제외한 모든 사람의 컴퓨터에서 실행된다.
Multiplayer.PeerDisconnected += OnPeerDisconnected;
// 클라이언트 전용: 내가 서버에 성공적으로 접속 완료했을 때 실행
Multiplayer.ConnectedToServer += OnConnectedToServer;
// 클라이언트 전용: 내가 서버에 접속 시도했는데 실패했을 때 실행
Multiplayer.ConnectionFailed += OnConnectionFailed;
// 클라이언트 전용: 서버와 연결이 끊어졌을 때 (방장이 나갔을 때)
Multiplayer.ServerDisconnected += OnServerDisconnected;
}
// Host 버튼을 눌렀을 때
public void _on_host_button_pressed()
{
// 네트워크 통신을 담당할 'Peer(피어)' 객체 생성
ENetMultiplayerPeer peer = new ENetMultiplayerPeer();
// 서버 열기 (최대 4명 접속 가능)
Error error = peer.CreateServer(Port, 4);
if (error != Error.Ok)
{
GD.Print($"[시스템] 서버 열기 실패!");
return;
}
// 씬 트리의 멀티플레이어 관리자에게 이 Peer를 등록
Multiplayer.MultiplayerPeer = peer;
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 서버가 열렸습니다! 내 고유 ID: {Multiplayer.GetUniqueId()}");
}
// 접속(Join) 버튼을 눌렀을 때
public void _on_join_button_pressed()
{
ENetMultiplayerPeer peer = new ENetMultiplayerPeer();
// IP 주소와 포트 번호로 서버에 접속
Error error = peer.CreateClient(ServerIp, Port);
if (error != Error.Ok)
{
GD.Print($"[시스템] 접속 시도 불가! 에러 코드: {error}");
return;
}
Multiplayer.MultiplayerPeer = peer;
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 서버에 접속을 시도합니다... (응답 대기 중)");
}
// 누군가 방에 들어왔을 때 방장과 이미 방에 있던 사람들 뿐만 아니라, 새로 들어온 사람의 컴퓨터에서도 실행된다.
// 즉, 모든 사람의 컴퓨터에서 실행된다.
// Godot 엔진에서 Multiplayer.PeerConnected 시그널은 "누군가 방에 새로 들어왔다!"라는 뜻이 아니다.
// 정확한 의미는 "나와 저 사람 사이에 통신 케이블(연결)이 방금 연결되었다!"이다.
// 그래서 새로 들어온 사람의 컴퓨터에서도 실행된다.
private void OnPeerConnected(long id)
{
if (Multiplayer.IsServer())
{
// 방장인지 확인하고, 방장일 때만 접속한 사람의 ID와 안내 메시지를 출력한다.
// 방장 권한으로, 방에 있는 모두의 화면에 접속 메시지를 띄우라고 명령(RPC)한다.
Rpc(MethodName.BroadcastMessage, $"[시스템] 플레이어 {id} 님이 방에 새로 합류했습니다!");
}
}
private void OnPeerDisconnected(long id)
{
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 플레이어 {id} 님이 방을 나갔습니다.");
// 여기에 나간 사람의 캐릭터를 찾아 삭제(QueueFree)하는 코드가 들어갈 수 있다.
}
private void OnServerDisconnected()
{
GD.Print($"[시스템] 방장(서버)과의 연결이 끊어졌습니다. 방이 폭파되었습니다.");
// 방장이 나갔으므로 네트워크 상태를 깨끗하게 초기화 한다.
Multiplayer.MultiplayerPeer = null;
}
// RPC (Remote Procedure Call, 원격 프로시저 호출)
// RpcMode.Authority는 '방장(권위자)만 이 함수를 호출할 수 있다'는 뜻이다.
// CallLocal = true는 '방장 자신의 화면에서도 이 함수를 실행하라'는 뜻이다.
// 따라서 방장이 이 함수를 호출하면 방장의 화면과 접속한 모든 사람의 화면에서 동시에 실행된다.
// RpcMode.AnyPeer는 '누구나 이 함수를 호출할 수 있다'는 뜻이다.
// RpcMode.Disabled는 '이 함수를 RPC로 호출할 수 없다'는 뜻이다.
[Rpc(MultiplayerApi.RpcMode.Authority, CallLocal = true)]
public void BroadcastMessage(string message)
{
// 방장의 명령을 받아 모두의 화면에서 동시에 실행된다.
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] " + message);
}
// 내가 서버에 성공적으로 접속했을 때 (클라이언트에서만 실행됨)
private void OnConnectedToServer()
{
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 서버 접속 성공!");
}
// 내가 서버에 접속 시도했는데 실패했을 때 (클라이언트에서만 실행됨)
private void OnConnectionFailed()
{
// 방장이 없거나, IP가 틀렸거나, 방화벽에 막혔을 때 출력된다.
GD.Print($"[시스템] 서버 접속에 실패했습니다! (방이 없거나 응답 없음)");
// 실패했으므로 네트워크 상태를 깨끗하게 초기화 한다.
Multiplayer.MultiplayerPeer = null;
}
}

F5/F6으로 실행 시 3개의 인스턴스가 생성되도록 Dubug - Customize Run Instances... 를 위와 같이 설정한다.

F6으로 게임을 실행하고 나타나는 3개의 인스턴스 중 하나를 Host로, 나머지 두 개를 Client로 하고 로그를 확인하면 위와 같이 기록된다.

클라이언트 부터 종료하면 위와 같이 처리된다.

서버가 먼저 종료해도 위와 같이 잘 처리된다.
채팅 서버-클라이언트를 만들어 보자.

위와 같이 TextEdit, LineEdit, Button을 추가한다.
using Godot;
public partial class Test : Control
{
...
private TextEdit _chatDisplay;
private LineEdit _chatInput;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
GetNode<Button>("Button").Pressed += _on_host_button_pressed;
GetNode<Button>("Button2").Pressed += _on_join_button_pressed;
_chatDisplay = GetNode<TextEdit>("TextEdit");
_chatDisplay.Editable = false; // 채팅창은 읽기 전용으로 설정
_chatInput = GetNode<LineEdit>("LineEdit");
// 채팅 보내기 버튼을 눌렀을 때
GetNode<Button>("Button3").Pressed += _on_send_button_pressed;
...
}
.
.
.
// 채팅 보내기 버튼을 눌렀을 때
public void _on_send_button_pressed()
{
// 텍스트 칸이 비어있으면 무시
string text = _chatInput.Text.Trim();
if (string.IsNullOrEmpty(text)) return;
// ReceiveChatMessage 함수를 모두의 컴퓨터에서 실행하도록 명령(RPC)한다.
// 방장이든 손님이든 누구나 이 함수를 실행시킬 권한이 있음.
Rpc(MethodName.ReceiveChatMessage, text);
// 내 채팅창은 다시 비워줌
_chatInput.Text = "";
}
// 모두의 컴퓨터에서 실행될 채팅 수신 함수
// AnyPeer: 방장이든 손님이든 누구나 이 함수를 실행시킬 권한이 있음
// CallLocal = true: 보낸 사람 본인의 컴퓨터에서도 이 함수를 실행함
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true)]
public void ReceiveChatMessage(string message)
{
// 이 RPC 명령을 나에게 보낸 사람의 ID 번호를 추적해서 가져온다.
long senderId = Multiplayer.GetRemoteSenderId();
// 채팅창(TextEdit)에 새로운 줄(\n)과 함께 메시지를 추가한다.
_chatDisplay.Text += $"\n[Player {senderId}]: {message}";
// RichTextLabel을 사용하면 _chatDisplay.Text += "text" 대신
// 메모리를 훨씬 적게 사용하는 _chatDisplay.AddText("text")를 사용할 수 있다.
_chatDisplay.ScrollVertical = _chatDisplay.GetLineCount(); // 채팅창을 맨 아래로 스크롤
// RichTextLabel을 사용하면 Scroll Follow를 true로 설정하면 자동으로 맨 아래로 스크롤되지만
// TextEdit은 그런 기능이 없어서 수동으로 스크롤을 내려야 한다.
}
}
스크립트를 위와 같이 수정/추가 한다.

게임을 실행하면 채팅을 할 수 있다.
Send 버튼을 누르지 않고 Enter키로 바로 채팅을 보낼 수 있도록 바꿔보자.
using Godot;
public partial class Test : Control
{
...
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
...
_chatInput = GetNode<LineEdit>("LineEdit");
_chatInput.TextSubmitted += _on_chat_input_text_submitted;
...
}
.
.
.
// LineText에서 Enter 키를 눌렀을 때 실행될 함수
// newText 변수에 방금 입력한 텍스트가 들어있다.
private void _on_chat_input_text_submitted(string newText)
{
string text = newText.Trim();
if (string.IsNullOrEmpty(text))
{
// 공백만 입력했을 때도 입력창을 비워주고 포커스를 다시 잡아준다.
_chatInput.Text = "";
_chatInput.ReleaseFocus();
_chatInput.CallDeferred(Control.MethodName.GrabFocus);
return;
}
// 채팅을 서버(모두)에게 보낸다. (RPC 호출)
Rpc(MethodName.ReceiveChatMessage, text);
// 내 채팅창 비우기
_chatInput.Text = "";
// 채팅을 치고 나서 마우스로 다시 클릭할 필요 없이 계속해서 키보드로 다음 채팅을 칠 수 있도록
// 입력창에 커서(포커스)를 잡아둔다. ReleaseFocus()를 먼저 호출하고, CallDeferred로 GrabFocus()를 호출해야 한다.
_chatInput.ReleaseFocus();
_chatInput.CallDeferred(Control.MethodName.GrabFocus);
}
}
위와 같이 수정하면 Send 버튼을 누르지 않고 Enter키로 채팅을 보낼 수 있다.
최종 코드
using Godot;
public partial class Test : Control
{
private const int Port = 8910; // 포트 번호 (임의의 4자리 숫자)
private const string ServerIp = "127.0.0.1"; // 내 컴퓨터 안에서 테스트할 주소 (로컬호스트)
private TextEdit _chatDisplay;
private LineEdit _chatInput;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
GetNode<Button>("Button").Pressed += _on_host_button_pressed;
GetNode<Button>("Button2").Pressed += _on_join_button_pressed;
_chatDisplay = GetNode<TextEdit>("TextEdit");
_chatDisplay.Editable = false; // 채팅창은 읽기 전용으로 설정
_chatInput = GetNode<LineEdit>("LineEdit");
_chatInput.TextSubmitted += _on_chat_input_text_submitted;
_chatInput.Editable = false; // 처음에는 채팅 입력창을 비활성화 상태로 설정 (방에 들어가기 전까지는 입력 불가)
// 채팅 보내기 버튼을 눌렀을 때
GetNode<Button>("Button3").Pressed += _on_send_button_pressed;
// 누군가 방에 성공적으로 들어왔을 때 모든 사람의 컴퓨터에서 실행된다.
// 방장과 이미 방에 있던 사람들 뿐만 아니라, 새로 들어온 사람의 컴퓨터에서도 실행된다.
Multiplayer.PeerConnected += OnPeerConnected;
// 누군가 접속을 끊고 나갔을 때 나간 사람을 제외한 모든 사람의 컴퓨터에서 실행된다.
Multiplayer.PeerDisconnected += OnPeerDisconnected;
// 클라이언트 전용: 내가 서버에 성공적으로 접속 완료했을 때 실행
Multiplayer.ConnectedToServer += OnConnectedToServer;
// 클라이언트 전용: 내가 서버에 접속 시도했는데 실패했을 때 실행
Multiplayer.ConnectionFailed += OnConnectionFailed;
// 클라이언트 전용: 서버와 연결이 끊어졌을 때 (방장이 나갔을 때)
Multiplayer.ServerDisconnected += OnServerDisconnected;
}
// Host 버튼을 눌렀을 때
public void _on_host_button_pressed()
{
// 네트워크 통신을 담당할 'Peer(피어)' 객체 생성
ENetMultiplayerPeer peer = new ENetMultiplayerPeer();
// 서버 열기 (최대 4명 접속 가능)
Error error = peer.CreateServer(Port, 4);
if (error != Error.Ok)
{
GD.Print($"[시스템] 서버 열기 실패!");
return;
}
// 씬 트리의 멀티플레이어 관리자에게 이 Peer를 등록
Multiplayer.MultiplayerPeer = peer;
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 서버가 열렸습니다! 내 고유 ID: {Multiplayer.GetUniqueId()}");
// 채팅 입력창을 활성화 상태로 설정 (방에 들어왔으므로 입력 가능)
_chatInput.Editable = true;
}
// 접속(Join) 버튼을 눌렀을 때
public void _on_join_button_pressed()
{
ENetMultiplayerPeer peer = new ENetMultiplayerPeer();
// IP 주소와 포트 번호로 서버에 접속
Error error = peer.CreateClient(ServerIp, Port);
if (error != Error.Ok)
{
GD.Print($"[시스템] 접속 시도 불가! 에러 코드: {error}");
return;
}
Multiplayer.MultiplayerPeer = peer;
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 서버에 접속을 시도합니다... (응답 대기 중)");
}
// 누군가 방에 들어왔을 때 방장과 이미 방에 있던 사람들 뿐만 아니라, 새로 들어온 사람의 컴퓨터에서도 실행된다.
// 즉, 모든 사람의 컴퓨터에서 실행된다.
// Godot 엔진에서 Multiplayer.PeerConnected 시그널은 "누군가 방에 새로 들어왔다!"라는 뜻이 아니다.
// 정확한 의미는 "나와 저 사람 사이에 통신 케이블(연결)이 방금 연결되었다!"이다.
// 그래서 새로 들어온 사람의 컴퓨터에서도 실행된다.
private void OnPeerConnected(long id)
{
if (Multiplayer.IsServer())
{
// 방장인지 확인하고, 방장일 때만 접속한 사람의 ID와 안내 메시지를 출력한다.
// 방장 권한으로, 방에 있는 모두의 화면에 접속 메시지를 띄우라고 명령(RPC)한다.
Rpc(MethodName.BroadcastMessage, $"[시스템] 플레이어 {id} 님이 방에 새로 합류했습니다!");
}
}
private void OnPeerDisconnected(long id)
{
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 플레이어 {id} 님이 방을 나갔습니다.");
// 여기에 나간 사람의 캐릭터를 찾아 삭제(QueueFree)하는 코드가 들어갈 수 있다.
}
private void OnServerDisconnected()
{
GD.Print($"[시스템] 방장(서버)과의 연결이 끊어졌습니다. 방이 폭파되었습니다.");
// 방장이 나갔으므로 네트워크 상태를 깨끗하게 초기화 한다.
Multiplayer.MultiplayerPeer = null;
// 채팅 입력창을 비활성화 상태로 설정 (방에서 나갔으므로 입력 불가)
_chatInput.Editable = false;
}
// RPC (Remote Procedure Call, 원격 프로시저 호출)
// RpcMode.Authority는 '방장(권위자)만 이 함수를 호출할 수 있다'는 뜻이다.
// CallLocal = true는 '방장 자신의 화면에서도 이 함수를 실행하라'는 뜻이다.
// 따라서 방장이 이 함수를 호출하면 방장의 화면과 접속한 모든 사람의 화면에서 동시에 실행된다.
// RpcMode.AnyPeer는 '누구나 이 함수를 호출할 수 있다'는 뜻이다.
// RpcMode.Disabled는 '이 함수를 RPC로 호출할 수 없다'는 뜻이다.
[Rpc(MultiplayerApi.RpcMode.Authority, CallLocal = true)]
public void BroadcastMessage(string message)
{
// 방장의 명령을 받아 모두의 화면에서 동시에 실행된다.
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] " + message);
}
// 내가 서버에 성공적으로 접속했을 때 (클라이언트에서만 실행됨)
private void OnConnectedToServer()
{
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 서버 접속 성공!");
// 채팅 입력창을 활성화 상태로 설정 (서버에 접속했으므로 입력 가능)
_chatInput.Editable = true;
}
// 내가 서버에 접속 시도했는데 실패했을 때 (클라이언트에서만 실행됨)
private void OnConnectionFailed()
{
// 방장이 없거나, IP가 틀렸거나, 방화벽에 막혔을 때 출력된다.
GD.Print($"[시스템] 서버 접속에 실패했습니다! (방이 없거나 응답 없음)");
// 실패했으므로 네트워크 상태를 깨끗하게 초기화 한다.
Multiplayer.MultiplayerPeer = null;
}
// 채팅 보내기 버튼을 눌렀을 때
public void _on_send_button_pressed()
{
// 텍스트 칸이 비어있으면 무시
string text = _chatInput.Text.Trim();
if (string.IsNullOrEmpty(text)) return;
// ReceiveChatMessage 함수를 모두의 컴퓨터에서 실행하도록 명령(RPC)한다.
// 방장이든 손님이든 누구나 이 함수를 실행시킬 권한이 있음.
Rpc(MethodName.ReceiveChatMessage, text);
// 내 채팅창은 다시 비워줌
_chatInput.Text = "";
}
// 모두의 컴퓨터에서 실행될 채팅 수신 함수
// AnyPeer: 방장이든 손님이든 누구나 이 함수를 실행시킬 권한이 있음
// CallLocal = true: 보낸 사람 본인의 컴퓨터에서도 이 함수를 실행함
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true)]
public void ReceiveChatMessage(string message)
{
// 이 RPC 명령을 나에게 보낸 사람의 ID 번호를 추적해서 가져온다.
long senderId = Multiplayer.GetRemoteSenderId();
// 채팅창(TextEdit)에 새로운 줄(\n)과 함께 메시지를 추가한다.
_chatDisplay.Text += $"\n[Player {senderId}]: {message}";
// RichTextLabel을 사용하면 _chatDisplay.Text += "text" 대신
// 메모리를 훨씬 적게 사용하는 _chatDisplay.AddText("text")를 사용할 수 있다.
_chatDisplay.ScrollVertical = _chatDisplay.GetLineCount(); // 채팅창을 맨 아래로 스크롤
// RichTextLabel을 사용하면 Scroll Follow를 true로 설정하면 자동으로 맨 아래로 스크롤되지만
// TextEdit은 그런 기능이 없어서 수동으로 스크롤을 내려야 한다.
}
// LineText에서 Enter 키를 눌렀을 때 실행될 함수
// newText 변수에 방금 입력한 텍스트가 들어있다.
private void _on_chat_input_text_submitted(string newText)
{
string text = newText.Trim();
if (string.IsNullOrEmpty(text))
{
// 공백만 입력했을 때도 입력창을 비워주고 포커스를 다시 잡아준다.
_chatInput.Text = "";
_chatInput.ReleaseFocus();
_chatInput.CallDeferred(Control.MethodName.GrabFocus);
return;
}
// 채팅을 서버(모두)에게 보낸다. (RPC 호출)
Rpc(MethodName.ReceiveChatMessage, text);
// 내 채팅창 비우기
_chatInput.Text = "";
// 채팅을 치고 나서 마우스로 다시 클릭할 필요 없이 계속해서 키보드로 다음 채팅을 칠 수 있도록
// 입력창에 커서(포커스)를 잡아둔다. ReleaseFocus()를 먼저 호출하고, CallDeferred로 GrabFocus()를 호출해야 한다.
_chatInput.ReleaseFocus();
_chatInput.CallDeferred(Control.MethodName.GrabFocus);
}
}
접속 버튼을 클릭하고 방에 완전히 입장하기 전에 채팅을 입력하면 에러가 발생하기 때문에 방에 입장하기 전까지 채팅 입력창이 비활성화되는 코드가 추가되어 있다. (예를 들어 서버가 없는 상태에서 클라이언트가 접속 버튼을 클릭하면 접속 중인 상태가 꽤 오래 지속된다. 이때 채팅을 보내면 에러가 발생한다)
네트워크는 워낙 다양한 상황이 벌어질 수 있기 때문에 그 외 예상하지 못한 에러가 발생할 수 있다.
※ 참고
'Godot' 카테고리의 다른 글
| [Godot] Platformer Ladder Movement 사다리에서 캐릭터 움직임 (0) | 2026.09.15 |
|---|---|
| [Godot] Network Multi Play - Character Spawn and Move 네트워크 멀티 플레이 - 캐릭터 소환 및 이동 (0) | 2026.09.10 |
| [Godot] RPG Top-Down View Character 탑다운 뷰 캐릭터 1 (0) | 2026.09.07 |
| [Godot] Basic RPG Item Inventory System 기본 아이템 인벤토리 시스템 만들기 (0) | 2026.09.06 |
| [Godot] Making Plugin or Add On 플러그인, 애드온 만들기 3 (0) | 2026.09.03 |