[Godot] Network Multi Play - Character Spawn and Move 네트워크 멀티 플레이 - 캐릭터 소환 및 이동
Godot 2026. 9. 10. 23:08 |서버-클라이언트 네트워크 구조를 만들고 캐릭터를 소환해 움직여 보자.
기본적인 내용은 아래 링크를 참고 한다.
2026.09.09 - [Godot] - [Godot] Network Multi Play - Chating 네트워크 멀티 플레이 - 채팅

위와 같이 캐릭터 씬을 구성한다.

CharacterBody2D - CollisionObject2D - Collision - Layer 1, Mask 2로 설정한다.
캐릭터의 레이어는 1이고 2(지형)와의 충돌만을 감지한다는 의미다.

MultiplayerSynchronizer - Root Path - CharacterBody2D를 지정한다.
캐릭터 스크립트 (Cat.cs)
using Godot;
public partial class Cat : CharacterBody2D
{
public const float Speed = 300.0f;
public const float JumpVelocity = -400.0f;
// 네트워크로 주고받을 '목표 위치' 변수를 만든다.
// 상대방 화면에서 볼 때 내 캐릭터가 어디에 있어야 하는지 알려주는 변수다.
// [Export]를 붙여야 에디터 - MultiplayerSynchronizer - Replication(동기화) 패널에서 이 변수를 찾을 수 있다.
[Export]
public Vector2 SyncPosition { get; set; }
public override async void _Ready()
{
// 처음 실행되는 서버의 경우 상관 없지만 클라이언트의 경우 서버에 접속해서 캐릭터가 스폰되고 나서 다른 컴퓨터의
// 캐릭터 정보(이 예의 경우 위치 정보인 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;
}
else
{
Modulate = Colors.Red; // 상대방 캐릭터는 빨간색으로 칠해 구분하기 쉽게 한다.
// 다른 사람의 캐릭터(기존에 방에 있던 사람)가 내 화면에 처음 스폰될 때는
// Lerp로 미끄러져 오지 않도록, 네트워크로 받은 위치(SyncPosition)로 즉시 이동.
// 충분한 시간이 지나서 SyncPosition이 업데이트 된 후 실행해야 한다.
GlobalPosition = SyncPosition;
}
// 엔진 스위치 켜기: 0.5초가 지났으니 이제부터 매 프레임 _PhysicsProcess를 실행하라고 지시한다.
SetPhysicsProcess(true);
}
// _Ready() 함수보다 먼저, 씬에 등장하는 순간에 실행되는 내장 함수.
public override void _EnterTree()
{
// 서버가 내 이름을 특정 번호(예: "1" 또는 "34567")로 지어준다.
// 그 이름(Name)을 다시 정수(int)로 바꾼다.
int myAuthorityId = int.Parse(Name);
// 그 숫자로 내 조종 권한(Authority)을 스스로 설정한다.
// 이 타이밍(_EnterTree)에 설정해야 MultiplayerSynchronizer가 안정적으로 작동한다.
SetMultiplayerAuthority(myAuthorityId);
}
// CharacterBody2D - CollisionObject2D - Collision - Layer 1, Mask 2로 바꿔서 플레이어끼리 충돌하지 않도록 설정한다.
// 바닥은 Layer 2, Mask 1로 설정해 플레이어가 바닥과 충돌하도록 설정 한다.
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);
}
return;
}
Vector2 velocity = Velocity;
// Add the gravity.
if (!IsOnFloor())
{
velocity += GetGravity() * (float)delta;
}
// Handle Jump.
if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
{
velocity.Y = JumpVelocity;
}
// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
velocity.X = direction.X * Speed;
}
else
{
velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
}
Velocity = velocity;
MoveAndSlide();
// 내 캐릭터의 이동이 다 끝난 후 내 최종 위치를 목표 위치 변수에 담아 다른 사람에게 보내준다.
// MultiplayerSynchronizer가 자동으로 SyncPosition을 다른 사람에게 보내준다.
SyncPosition = GlobalPosition;
}
}

MultiplayerSynchronizer - Replication - Add property to sync... 버튼 클릭 - CharacterBody2D 선택하고 OK 클릭 - Cat 클래스의 SyncPosition 선택하고 Open 버튼 클릭
MultiplayerSynchronizer는 Replication에 설정된 속성을 네트워크에 연결된 모든 컴퓨터가 동기화 할 수 있도록 모두에게 계속해서 전달한다.

위와 같이 서버-클라이언트 화면과 지형으로 사용할 StaticBody2D를 구성한다.
Node2D는 그 자체로 특별한 기능을 하지는 않고 MultiplayerSpawner에 의해 사용된다.

MultiplayerSpawner - SpawnPath - 씬 구성에서 추가된 Node2D 노드를 선택한다.
MultiplayerSpawner - Auto Spawn List - 캐릭터 씬을 선택한다.

지형으로 사용할 StaticBody2D - CollisionObejct2D - Collision - Layer 2, Mask 1로 설정한다.
지형의 레이어는 2이고 1(캐릭터)과의 충돌만을 감지한다는 의미다.

지형으로 사용할 StaticBody2D - Sprite2D - Textrue는 적당한 지형 텍스처가 있다면 그걸 사용해도 되지만 여기서는 간단하게 GradientTexture2D를 이용해 검은색 텍스쳐를 만들어 사용했다.
서버-클라이언트 스크립트 (Test.cs)
using Godot;
public partial class Test : Control
{
private const int Port = 8910; // 포트 번호 (임의의 4자리 숫자)
private const string ServerIp = "127.0.0.1"; // 내 컴퓨터 안에서 테스트할 주소 (로컬호스트)
// 캐릭터 씬과 캐릭터 컨테이너 노드를 담을 변수 추가
private PackedScene _playerScene = GD.Load<PackedScene>("res://Cat.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;
}
}
설정이 완료되면 게임을 실행한다.

서버와 클라이언트 화면이 동기화되고 각각의 캐릭터를 조종할 수 있다.
여기까지 설정된 충돌에 대한 내용을 보면 CharacterBody는 Layer 1, Mask 2로 설정되었고 지형으로 사용된 StaticBody2D는 Layer 2, Mask 1로 설정되었다. 그래서 캐릭터끼리는 충돌하지 않고 지형과만 충돌한다.
이렇게 설정한 이유는 CharacterBody2D의 Mask를 1, 2로 설정해 지형과 캐릭터 모두 충돌에 반응하도록 하면 이상한 현상이 벌어지기 때문이다.
예를 들어 서버 실행 후 처음 서버의 캐릭터 생성시엔 문제가 없는데 클라이언트가 접속해 캐릭터가 생성되면 클라이언트 캐릭터가 생성되는 순간 서버 캐릭터의 위치로 이동해버리거나 근처로 순간 이동하는 현상이 발생한다. CharacterBody2D의 Mask를 1로만 설정하면 이런 문제가 발생하지 않는다. 이유는 잘 모르겠다.
그래서 처음 CharacterBody2D의 Mask는 2로만 설정하고 아래와 같이 캐릭터 스크립트 _Ready()를 수정해서 _Ready() 진행 중 마지막 0.1초 후 Mask 1을 세팅하면 별 문제는 없게된다. 캐릭터끼리의 충돌 처리도 잘 된다.
public override async void _Ready()
{
.
.
.
await ToSignal(GetTree().CreateTimer(0.1f), SceneTreeTimer.SignalName.Timeout);
// 0.1초 후...
SetCollisionMaskValue(1, true); // 캐릭터끼리 충돌하도록 설정
}
그런데 네트워크 환경에서 캐릭터간의 물리적 충돌을 계산하고 감지하도록 하는게 좋은 선택은 아니라고 한다. 매 순간 계산되는 물리 작용의 결과가 네트워크상의 상대방과 완벽하게 동기화 될 수 없는 현실적인 이유 때문이 아닐까 싶다.
Area2D를 이용해 충돌을 구현해 보자.

캐릭터 씬에 Area2D - CollisionShape2D를 추가한다.

Area2D - Collision - Layer는 모두 끈다. 다른 물체가 Area2D와 반응하지 못할 것이다.
Mask만 1로 남겨둔다. Area2D가 캐릭터와만 반응한다.
스크립트는 아래와 같이 수정한다.
using Godot;
public partial class Cat : CharacterBody2D
{
public const float Speed = 300.0f;
public const float JumpVelocity = -400.0f;
// 캐릭터가 넉백 중인지 확인하는 변수. 넉백 중이면 키보드 입력을 무시하고, 넉백이 끝나면 다시 키보드 입력을 받는다.
private bool _isKnockedBack = false;
// 네트워크로 주고받을 '목표 위치' 변수를 만든다.
// 상대방 화면에서 볼 때 내 캐릭터가 어디에 있어야 하는지 알려주는 변수다.
// [Export]를 붙여야 에디터 - MultiplayerSynchronizer - Replication(동기화) 패널에서 이 변수를 찾을 수 있다.
[Export]
public Vector2 SyncPosition { get; set; }
public override async void _Ready()
{
// 처음 실행되는 서버의 경우 상관 없지만 클라이언트의 경우 서버에 접속해서 캐릭터가 스폰되고 나서 다른 컴퓨터의
// 캐릭터 정보(이 예의 경우 위치 정보인 SyncPosition)가 네트워크로 전송되기까지 약간의 딜레이가 있다.
// 그 사이 _PhysicsProcess가 호출되면 기본 위치(0,0)에서 시작해서 SyncPosition으로 이동하는 과정이 화면에 그대로
// 보이게 된다. 따라서 다른 컴퓨터의 캐릭터가 내 화면에 처음 등장할 때는, SyncPosition이 네트워크로 전송되기
// 전까지는 잠시 대기하는 시간이 필요하다. (아래 코드 참고)
// 물리 엔진 스위치 끄기. _PhysicsProcess 함수가 아예 호출되지 않게 만든다.
SetPhysicsProcess(false);
// 목표 위치를 현재 위치로 초기화.
SyncPosition = GlobalPosition;
// 0.5초 동안 기다린다. 0.1초도 상관 없다. 다른 컴퓨터의 캐릭터 정보(SyncPosition)가 업데이트 되는 시간이면 된다.
// 시작하자마자 물리 연산이 멈춰있으므로 공중에서 대기한다.
await ToSignal(GetTree().CreateTimer(0.5f), SceneTreeTimer.SignalName.Timeout);
// 내 캐릭터면 정상 색깔(흰색), 다른 사람의 캐릭터면 빨간색으로 칠하기.
if (IsMultiplayerAuthority())
{
Modulate = Colors.White;
}
else
{
Modulate = Colors.Red; // 상대방 캐릭터는 빨간색으로 칠해 구분하기 쉽게 한다.
// 다른 사람의 캐릭터(기존에 방에 있던 사람)가 내 화면에 처음 스폰될 때는
// Lerp로 미끄러져 오지 않도록, 네트워크로 받은 위치(SyncPosition)로 즉시 이동.
// 충분한 시간이 지나서 SyncPosition이 업데이트 된 후 실행해야 한다.
GlobalPosition = SyncPosition;
}
// 엔진 스위치 켜기: 0.5초가 지났으니 이제부터 매 프레임 _PhysicsProcess를 실행하라고 지시한다.
SetPhysicsProcess(true);
// 다른 캐릭터(CharacterBody2D)가 내 센서에 닿으면 OnInteractionBoxBodyEntered() 실행.
GetNode<Area2D>("Area2D").BodyEntered += OnInteractionBoxBodyEntered;
//await ToSignal(GetTree().CreateTimer(0.1f), SceneTreeTimer.SignalName.Timeout);
// 0.1초 후...
//SetCollisionMaskValue(1, true); // 캐릭터끼리 충돌하도록 설정
}
// _Ready() 함수보다 먼저, 씬에 등장하는 순간에 실행되는 내장 함수.
public override void _EnterTree()
{
// 서버가 내 이름을 특정 번호(예: "1" 또는 "34567")로 지어준다.
// 그 이름(Name)을 다시 정수(int)로 바꾼다.
int myAuthorityId = int.Parse(Name);
// 그 숫자로 내 조종 권한(Authority)을 스스로 설정한다.
// 이 타이밍(_EnterTree)에 설정해야 MultiplayerSynchronizer가 안정적으로 작동한다.
SetMultiplayerAuthority(myAuthorityId);
}
// 다른 캐릭터와 부딪혔을 때 실행될 넉백 함수
private async void OnInteractionBoxBodyEntered(Node2D body)
{
// 내가 조종하는 캐릭터(Authority)일 때만 밀려나는 계산을 실행.
// 상대방 화면에서는 SyncPosition을 통해 밀려난 내 위치가 자동으로 동기화 된다.
if (!IsMultiplayerAuthority())
return;
// 부딪힌 대상이 나 자신이 아니고, 다른 플레이어인지 확인.
if (body != this && body is Cat otherCat)
{
// 방향 계산: 상대방의 위치에서 내 위치를 향하는 방향을 구합니다. (밀려날 방향)
Vector2 knockbackDir = (GlobalPosition - otherCat.GlobalPosition).Normalized();
// 뒤로만 밀리면 심심하므로, Y축(위쪽)으로 살짝 띄워준다.
knockbackDir.Y = -0.5f;
// 내 캐릭터의 Velocity(속도)에 강력한 순간 힘을 적용한다. (500은 넉백 파워)
Velocity = knockbackDir * 500.0f;
_isKnockedBack = true;
GD.Print($"앗! {otherCat.Name} 님과 부딪혀서 밀려납니다!");
// 1.0초 동안 뒤로 밀려가도록 내버려 둔다. (공중 체공 시간)
await ToSignal(GetTree().CreateTimer(1.0f), SceneTreeTimer.SignalName.Timeout);
// 시간이 지나면 다시 키보드로 조종할 수 있게 한다.
_isKnockedBack = false;
}
}
// CharacterBody2D - CollisionObject2D - Collision - Layer 1, Mask 2로 바꿔서 플레이어끼리 충돌하지 않도록 설정한다.
// 바닥은 Layer 2, Mask 1로 설정해 플레이어가 바닥과 충돌하도록 설정 한다.
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);
}
return;
}
Vector2 velocity = Velocity;
// Add the gravity.
if (!IsOnFloor())
{
velocity += GetGravity() * (float)delta;
}
// Handle Jump.
if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
{
velocity.Y = JumpVelocity;
}
if (!_isKnockedBack) // 넉백 중이 아닐 때만 키보드 입력을 받는다.
{
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
velocity.X = direction.X * Speed;
}
else
{
velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
}
}
else
{
// 넉백 중일 때는 키보드 입력을 무시하고, X축 속도를 점점 줄여서 멈추게 한다.
velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed * 2.0f * (float)delta);
}
Velocity = velocity;
MoveAndSlide();
// 내 캐릭터의 이동이 다 끝난 후 내 최종 위치를 목표 위치 변수에 담아 다른 사람에게 보내준다.
// MultiplayerSynchronizer가 자동으로 SyncPosition을 다른 사람에게 보내준다.
SyncPosition = GlobalPosition;
}
}

'Godot' 카테고리의 다른 글
| [Godot] RPG Top-Down View Character 탑다운 뷰 캐릭터 2 (0) | 2026.09.17 |
|---|---|
| [Godot] Platformer Ladder Movement 사다리에서 캐릭터 움직임 (0) | 2026.09.15 |
| [Godot] Network Multi Play - Chating 네트워크 멀티 플레이 - 채팅 (0) | 2026.09.09 |
| [Godot] RPG Top-Down View Character 탑다운 뷰 캐릭터 1 (0) | 2026.09.07 |
| [Godot] Basic RPG Item Inventory System 기본 아이템 인벤토리 시스템 만들기 (0) | 2026.09.06 |