[Godot] Network Multi Play - Character Animation 네트워크 멀티 플레이 - 캐릭터 애니메이션
Godot 2026. 9. 20. 12:10 |네트워크 멀티 플레이에서 상대방 캐릭터의 애니메이션이 플레이 되도록 해 보자.
기본적인 내용은 아래 링크를 참고한다.
2026.09.07 - [Godot] - [Godot] RPG Top-Down View Character 탑다운 뷰 캐릭터 1
2026.09.17 - [Godot] - [Godot] RPG Top-Down View Character 탑다운 뷰 캐릭터 2
2026.09.09 - [Godot] - [Godot] Network Multi Play - Chating 네트워크 멀티 플레이 - 채팅
크게 보자면 캐릭터 소환 및 이동 예제에서 SyncAnimDirection, SyncAnimState 변수만 추가로 동기화 시킨다고 생각하면 된다.
캐릭터의 스크립트는 아래와 같다.
using Godot;
public partial class CharacBody : CharacterBody2D
{
public const float Speed = 100.0f;
private AnimationTree _animationTree;
private AnimationNodeStateMachinePlayback _stateMachine;
[Export]
public Vector2 SyncPosition { get; set; }
[Export]
public Vector2 SyncAnimDirection { get; set; }
[Export]
public string SyncAnimState { get; set; } = "Idle";
// _Ready() 함수보다 먼저, 씬에 등장하는 순간에 실행되는 내장 함수.
public override void _EnterTree()
{
// 서버가 내 이름을 특정 번호(예: "1" 또는 "34567")로 지어준다.
// 그 이름(Name)을 다시 정수(int)로 바꾼다.
int myAuthorityId = int.Parse(Name);
// 그 숫자로 내 조종 권한(Authority)을 스스로 설정한다.
// 이 타이밍(_EnterTree)에 설정해야 MultiplayerSynchronizer가 안정적으로 작동한다.
SetMultiplayerAuthority(myAuthorityId);
}
public override async void _Ready()
{
// StateMachine을 가져온다
_animationTree = GetNode<AnimationTree>("AnimationTree");
_stateMachine = (AnimationNodeStateMachinePlayback)_animationTree.Get("parameters/playback");
// 게임 시작 시 기본 방향을 아래쪽(0, 1)으로 강제 설정.
// Y축이 아래쪽으로 향하는 2D 좌표계에서, (0, 1)은 아래쪽을 의미.
Vector2 startingDirection = new Vector2(0, 1);
_animationTree.Set("parameters/Idle/blend_position", startingDirection);
_animationTree.Set("parameters/Walk/blend_position", startingDirection);
// Area2D 노드의 BodyEntered 시그널에 이벤트 핸들러를 연결한다.
Area2D area2D = GetNode<Area2D>("Area2D");
area2D.BodyEntered += OnBodyEntered;
// 처음 실행되는 서버의 경우 상관 없지만 클라이언트의 경우 서버에 접속해서 캐릭터가 스폰되고 나서 다른 컴퓨터의
// 캐릭터 정보(이 예의 경우 위치 정보인 SyncPosition)가 네트워크로 전송되기까지 약간의 딜레이가 있다.
// 그 사이 _PhysicsProcess가 호출되면 기본 위치(0,0)에서 시작해서 SyncPosition으로 이동하는 과정이 화면에 그대로
// 보이게 된다. 따라서 다른 컴퓨터의 캐릭터가 내 화면에 처음 등장할 때는, SyncPosition이 네트워크로 전송되기
// 전까지는 잠시 대기하는 시간이 필요하다. (아래 코드 참고)
// 물리 엔진 스위치 끄기. _PhysicsProcess 함수가 아예 호출되지 않게 만든다.
SetPhysicsProcess(false);
// 목표 위치를 현재 위치로 초기화.
// 다른 사람의 화면에서 내 캐릭터가 처음 등장할 때 SyncPosition이 아래와 같이
// 초기화되지 않은 상태라면 (0,0) 좌표로 날아가 버린다.
SyncPosition = GlobalPosition;
// 0.5초 동안 기다린다. 0.1초도 상관 없다. 다른 컴퓨터의 캐릭터 정보(SyncPosition)가 업데이트 되는 시간이면 된다.
// 시작하자마자 물리 연산이 멈춰있으므로 공중에서 대기한다.
await ToSignal(GetTree().CreateTimer(0.5f), SceneTreeTimer.SignalName.Timeout);
// 내 캐릭터면 정상 색깔(흰색), 다른 사람의 캐릭터면 빨간색으로 칠하기.
// 내 카메라는 켜고 다른 사람의 카메라는 끈다.
if (IsMultiplayerAuthority())
{
Modulate = Colors.White; // 내 캐릭터는 흰색으로 칠해 구분하기 쉽게 한다.
// 내 캐릭터의 카메라를 켜고 활성화 시킨다.
GetNode<Camera2D>("Camera2D").Enabled = true;
GetNode<Camera2D>("Camera2D").MakeCurrent();
}
else
{
Modulate = Colors.IndianRed; // 상대방 캐릭터는 빨간색으로 칠해 구분하기 쉽게 한다.
// 다른 사람의 캐릭터(기존에 방에 있던 사람)가 내 화면에 처음 스폰될 때는
// Lerp로 미끄러져 오지 않도록, 네트워크로 받은 위치(SyncPosition)로 즉시 이동.
// 충분한 시간이 지나서 SyncPosition이 업데이트 된 후 실행해야 한다.
GlobalPosition = SyncPosition;
// 다른 사람의 캐릭터 카메라는 꺼야 한다. (카메라는 한 화면에 하나만 존재해야 한다.)
// 내 권한의 캐릭터가 아닌 다른 사람의 캐릭터이므로 카메라를 끄면 권한을 가진 캐릭터의 카메라만 남게 된다.
GetNode<Camera2D>("Camera2D").Enabled = false;
}
// 엔진 스위치 켜기: 0.5초가 지났으니 이제부터 매 프레임 _PhysicsProcess를 실행하라고 지시한다.
SetPhysicsProcess(true);
}
private void OnBodyEntered(Node body)
{
Godot.Collections.Array<StringName> groups = body.GetGroups();
if (groups != null && groups.Count > 0)
{
foreach (StringName group in groups)
{
switch (group)
{
case "Flowers":
GD.Print("Flower entered");
break;
case "Rocks":
GD.Print("Rock entered");
break;
default:
GD.Print("Other group entered: " + group.ToString());
break;
}
}
}
}
public override void _PhysicsProcess(double delta)
{
// 엔진에게 이 캐릭터가 내 조종 권한인지 물어본다.
// 내가 주인이 아니라면(false) 다른 사람의 캐릭터이고, 그 캐릭터의 목표 위치(SyncPosition)를 향해 부드럽게(Lerp) 다가간다.
// 그리고 아래에 있는 키보드 입력과 이동 코드는 아예 실행하지 않고 끝낸다.
if (!IsMultiplayerAuthority())
{
// 소수점 진동 방지. 나와 목표 위치의 거리가 1픽셀보다 작다면? (거의 다 왔다면)
if (GlobalPosition.DistanceTo(SyncPosition) < 1.0f)
{
// 진동하지 않고 그냥 목표 위치에 딱 붙여버린다.
GlobalPosition = SyncPosition;
}
else
{
// 거리가 멀 때만 부드럽게(Lerp) 따라간다.
// 15.0f는 다가가는 속도다. 이 숫자를 조절해서 미끄러짐의 정도를 바꿀 수 있다.
GlobalPosition = GlobalPosition.Lerp(SyncPosition, 15.0f * (float)delta);
}
// 애니메이션 동기화. 네트워크로 넘어온 상대방 캐릭터의 방향과 상태를 내 화면의 트리(Tree)에 적용.
_animationTree.Set("parameters/Idle/blend_position", SyncAnimDirection);
_animationTree.Set("parameters/Walk/blend_position", SyncAnimDirection);
_animationTree.Set("parameters/Plow/blend_position", SyncAnimDirection);
_stateMachine.Travel(SyncAnimState);
return;
}
// 캐릭터의 AnimationTree 노드 설정창에서 Plow -> Idle 방향으로 전환되는 화살표(Transition)이 존재해야 하고
// 그 화살표의 Inspector 창에서 Advance - Mode를 "Auto"로 설정해야 한다.
// 그래야 Plow 애니메이션이 끝나면 자동으로 Idle 상태로 전환된다.
if (_stateMachine.GetCurrentNode() == "Plow") // 애니메이션 락(Lock): 현재 상태 머신이 'Plow'라면..
{
// 움직이지 못하게 속도를 0으로 고정.
Velocity = Vector2.Zero;
// 혹시 외부 요인에 밀려날 수 있으니 위치는 계속 동기화.
SyncPosition = GlobalPosition;
// 아래의 키보드 입력과 Walk/Idle 전환 코드를 모두 무시하고 여기서 프레임 종료.
return;
}
// IsActionJustPressed는 계속 누르고 있어도 클릭하는 순간 1번만 true를 반환한다.
// Plow 액션은 Input Map에서 마우스 왼쪽 버튼으로 설정되어 있다.
if (Input.IsActionJustPressed("Plow"))
{
Vector2 rawDirection = GetGlobalMousePosition() - GlobalPosition;
Vector2 plowDirection = Vector2.Zero;
if (Mathf.Abs(rawDirection.X) > Mathf.Abs(rawDirection.Y))
plowDirection = rawDirection.X > 0 ? Vector2.Right : Vector2.Left;
else
plowDirection = rawDirection.Y > 0 ? Vector2.Down : Vector2.Up;
// Plow 애니메이션을 재생하기 위해, 애니메이션 트리의 blend_position을 설정한다.
_animationTree.Set("parameters/Plow/blend_position", plowDirection);
// Walk과 Idle 애니메이션도 같은 방향으로 바라보도록 blend_position을 설정한다.
_animationTree.Set("parameters/Idle/blend_position", plowDirection);
_animationTree.Set("parameters/Walk/blend_position", plowDirection);
_stateMachine.Travel("Plow");
// SyncAnimDirection과 SyncAnimState를 설정해서 다른 사람에게도 Plow 애니메이션이 재생되도록 한다.
SyncAnimDirection = plowDirection;
SyncAnimState = "Plow";
// Plow 명령을 내렸으니 return으로 함수를 끝내서 아래에 있는 Idle(대기) 상태가 되는것을 차단한다.
return;
}
// WASD 또는 방향키 입력을 Vector2로 받아온다.
// 예: 오른쪽 위 대각선 이동 시 (1, -1) 반환
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
// 이동 방향을 애니메이션 트리에 전달.
_animationTree.Set("parameters/Idle/blend_position", direction);
_animationTree.Set("parameters/Walk/blend_position", direction);
// 상태를 '걷기'로 바꾼다.
_stateMachine.Travel("Walk");
// SyncAnimDirection과 SyncAnimState를 설정해서 다른 사람에게도 Walk 애니메이션이 재생되도록 한다.
SyncAnimDirection = direction;
SyncAnimState = "Walk";
}
else
{
// 입력이 없으면 상태를 '대기'로 바꾼다.
// (마지막으로 바라보던 방향의 idle 애니메이션이 자동으로 재생)
_stateMachine.Travel("Idle");
// SyncAnimState를 설정해서 다른 사람에게도 Idle 애니메이션이 재생되도록 한다.
SyncAnimState = "Idle";
}
// 입력된 방향에 속도를 곱해서 바로 Velocity(속력)로 설정한다.
Velocity = direction * Speed;
// 엔진이 알아서 벽 충돌을 계산하며 이동시킨다.
MoveAndSlide();
// 내 캐릭터의 이동이 다 끝난 후 내 최종 위치를 목표 위치 변수에 담아 다른 사람에게 보내준다.
// MultiplayerSynchronizer가 자동으로 SyncPosition을 다른 사람에게 보내준다.
SyncPosition = GlobalPosition;
}
}

캐릭터 AnimationTree - Plow -> Idle 방향의 Transition - Advance - Mode - Auto로 설정한다.
Auto로 설정하면 Plow 애니메이션이 끝나고 자동으로 Idle 애니메이션으로 돌아간다. 이걸 하지 않으면 스크립트에 의해 Plow에서 Idle로 돌아가는 코드가 없기 때문에 Plow 애니메이션 마지막에 멈추게 되고 더 이상 조작 할 수 없게 된다.

MultiplayerSynchronizer - Replication에 SyncAnimDirection과 SyncAnimState를 추가한다.
서버-클라이언트 스크립트는 아래와 같다.
using Godot;
public partial class TestStage : Control
{
private const int Port = 8910; // 포트 번호 (임의의 4자리 숫자)
private const string ServerIp = "127.0.0.1"; // 내 컴퓨터 안에서 테스트할 주소 (로컬호스트)
// 캐릭터 씬과 캐릭터 컨테이너 노드를 담을 변수 추가
private PackedScene _playerScene = GD.Load<PackedScene>("res://test.tscn"); // 캐릭터 씬
private Node2D _playersContainer; // 캐릭터들을 담을 컨테이너 노드 (Node2D)
// 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;
// 컨테이너 노드 가져오기, 컨테이너 노드는 특별한 기능을 하는 노드가 아니라 단순히 캐릭터들을 담는 용도의 Node2D다.
_playersContainer = GetNode<Node2D>("Node2D");
// 누군가 방에 성공적으로 들어왔을 때 모든 사람의 컴퓨터에서 실행된다.
// 방장과 이미 방에 있던 사람들 뿐만 아니라, 새로 들어온 사람의 컴퓨터에서도 실행된다.
Multiplayer.PeerConnected += OnPeerConnected;
// 누군가 접속을 끊고 나갔을 때 나간 사람을 제외한 모든 사람의 컴퓨터에서 실행된다.
Multiplayer.PeerDisconnected += OnPeerDisconnected;
// 클라이언트 전용: 내가 서버에 성공적으로 접속 완료했을 때 실행
Multiplayer.ConnectedToServer += OnConnectedToServer;
// 클라이언트 전용: 내가 서버에 접속 시도했는데 실패했을 때 실행
Multiplayer.ConnectionFailed += OnConnectionFailed;
// 클라이언트 전용: 서버와 연결이 끊어졌을 때 (방장이 나갔을 때)
Multiplayer.ServerDisconnected += OnServerDisconnected;
}
// 스폰 함수 (서버에서만 실행됨)
private void SpawnPlayer(long id)
{
// 캐릭터를 메모리에 하나 만든다.
Node2D player = _playerScene.Instantiate<Node2D>();
// 나중에 네트워크 권한(Authority)을 주기 위해 노드 이름을 ID 숫자로 바꾼다.
// Cat.tscn 씬에서 _EnterTree() 함수가 실행될 때, 노드 이름(Name)을 정수(int)로 바꿔서 SetMultiplayerAuthority()를 호출한다.
player.Name = id.ToString();
// 캐릭터를 컨테이너에 넣는다. 이 순간 MultiplayerSpawner 노드가 알아채고 손님 화면에도 똑같이 복사해 준다.
// MultiplayerSpawner 노드는 코드에 등장하지 않아 아무것도 안하는거 같지만 서버가 자신의 캐릭터 컨테이너에 캐릭터를
// 넣거나 빼면, 각 클라이언트 컨테이너에도 똑같이 넣거나 빼준다.
_playersContainer.AddChild(player);
}
// 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()}");
// 서버가 열리면 방장 본인의 캐릭터를 소환한다.
// 서버의 ID는 항상 1번이다. 그 외 클라이언트들은 예상 할 수 없는 큰 무작위 난수가 부여된다. (해킹 방지)
SpawnPlayer(Multiplayer.GetUniqueId());
// 서버가 항상 1번이므로 클라이언트는 RpcId()를 사용해 서버에게 특별한 요청을 할 수 있다.
// RpcId(1, MethodName.RequestOpenChest, chestId); // 예시: 서버에게 요청을 보내는 코드
GetNode<Button>("Button").Disabled = true; // Host 버튼 비활성화
GetNode<Button>("Button2").Disabled = true; // Join 버튼 비활성화
}
// 접속(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} 님이 방에 새로 합류했습니다!");
// 새로운 클라이언트가 들어오면, 서버가 그 클라이언트의 아이디로 캐릭터를 소환해 준다.
SpawnPlayer(id);
}
}
private void OnPeerDisconnected(long id)
{
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 플레이어 {id} 님이 방을 나갔습니다.");
// 누군가 방을 나가면, 서버가 해당 아이디의 캐릭터를 찾아서 삭제한다.
if (Multiplayer.IsServer())
{
Node playerToDestroy = _playersContainer.GetNodeOrNull(id.ToString());
if (playerToDestroy != null)
{
playerToDestroy.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)
{
// 방장의 명령을 받아 모두의 화면에서 동시에 실행된다. OnPeerConnected() 함수에서 방장이 사용.
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] " + message);
}
// 내가 서버에 성공적으로 접속했을 때 (클라이언트에서만 실행됨)
private void OnConnectedToServer()
{
GD.Print($"[ID: {Multiplayer.GetUniqueId()}] [시스템] 서버 접속 성공!");
GetNode<Button>("Button").Disabled = true; // Host 버튼 비활성화
GetNode<Button>("Button2").Disabled = true; // Join 버튼 비활성화
}
// 내가 서버에 접속 시도했는데 실패했을 때 (클라이언트에서만 실행됨)
private void OnConnectionFailed()
{
// 방장이 없거나, IP가 틀렸거나, 방화벽에 막혔을 때 출력된다.
GD.Print($"[시스템] 서버 접속에 실패했습니다! (방이 없거나 응답 없음)");
// 실패했으므로 네트워크 상태를 깨끗하게 초기화 한다.
Multiplayer.MultiplayerPeer = null;
}
}

고양이 캐릭터는 Motion Mode가 Floating 이므로 중력이 작용되지 않기 때문에 지형은 필요 없다.
게임을 실행하면 서버와 클라이언트의 캐릭터 애니메이션이 동기화 된다.

'Godot' 카테고리의 다른 글
| [Godot] Flickering Candles 바람에 흔들리는 촛불 (0) | 2026.09.22 |
|---|---|
| [Godot] Physics 물리 (0) | 2026.09.19 |
| [Godot] RPG Top-Down View Character 탑다운 뷰 캐릭터 2 (0) | 2026.09.17 |
| [Godot] Platformer Ladder Movement 사다리에서 캐릭터 움직임 (0) | 2026.09.15 |
| [Godot] Network Multi Play - Character Spawn and Move 네트워크 멀티 플레이 - 캐릭터 소환 및 이동 (0) | 2026.09.10 |
