h662

메이플스토리 월드 강의 / 무한 점프맵 만들기 / 6화

무한 점프맵 만들기 6화, 서버와 클라이언트

2026년 10월 10일

무한 점프맵 만들기 6화, 서버와 클라이언트 완성 화면

플레이어가 지금 몇 층에 있는지 기억하게 만듭니다. DefaultPlayer 에 층 번호를 담는 컴포넌트를 붙이고, 위층으로 옮기는 일을 플레이어에게 맡긴 뒤, 서버에서 바뀐 값이 내 화면(클라이언트)까지 어떻게 오는지 Sync 로 확인합니다.

지난 글에서는 출구에 닿으면 위층으로 올라가게 만들었습니다. 그런데 완성본을 떠올려 보면, 화면 위쪽에 지금 몇 층인지가 보이고, 떨어지면 몇 층까지 올랐는지 알려 주고, 가장 높이 오른 층이 기록으로 남습니다. 이 세 가지는 모두 같은 값 하나, 지금 몇 층에 있는지에서 나옵니다.

지금 우리 게임은 위층으로 올라가긴 하지만 몇 층인지 기억하는 곳이 아무 데도 없습니다. 출구는 플레이어를 옮기기만 하고 잊어버립니다. 이번 글에서는 층 번호를 기억하게 만들고, 서버에서 바뀐 그 값을 내 화면까지 가져오는 방법을 봅니다.

지금 층, 떨어졌을 때 알림, 최고 기록이 모두 층 번호 하나에서 나온다
지금 층, 떨어졌을 때 알림, 최고 기록이 모두 층 번호 하나에서 나온다

층 번호는 어디에 둘까

층을 바꾸는 것이 서버이니, 값을 정하는 것도 서버입니다. 그리고 여러 명이 함께 오르면 사람마다 있는 층이 다릅니다. 그래서 층 번호는 플레이어 한 사람 한 사람이 갖고 있어야 합니다.

MSW 에서 접속한 사람의 캐릭터는 DefaultPlayer 라는 모델로 만들어집니다. 이 모델에 스크립트를 붙이면, 들어오는 모든 플레이어가 그 스크립트를 하나씩 갖게 됩니다.

DefaultPlayer 모델에 붙인 스크립트는 들어오는 플레이어마다 하나씩 생긴다
DefaultPlayer 모델에 붙인 스크립트는 들어오는 플레이어마다 하나씩 생긴다
  1. MyDesk 에서 새 컴포넌트 스크립트를 만들고 이름을 TowerClimber 로 바꿉니다. 탑을 오르는 사람이라는 뜻입니다.
  2. Property 옆의 + 버튼으로 floor 를 만들고 타입을 integer 로 바꿉니다. Sync 는 일단 None 으로 둡니다(뒤에서 다시 봅니다).
  3. Workspace 에서 DefaultPlayer 를 고르고, Add Component 의 Script 에서 TowerClimber 를 찾아 붙입니다.

오르는 일을 플레이어에게 (ClimbTo, GetComponent)

지금은 출구가 플레이어를 직접 옮기고 있습니다. 그런데 층 번호는 플레이어가 갖고 있으니, 옮기는 일도 플레이어가 맡는 편이 자연스럽습니다. 출구는 이제 몇 층으로 보내는지만 알려 주면 됩니다. 하는 일은 같게 두고 일을 맡는 자리만 바꾸는 것도 리팩터링의 한 종류입니다.

TowerClimber 에 ClimbTo 라는 함수를 만들고 integer 매개변수 nextFloor 를 붙입니다. 몇 층으로 갈지를 nextFloor 로 넘겨받는 것입니다. 함수 안에는 세 가지를 적습니다.

  1. nextFloor 가 지금 층보다 높지 않으면 아무것도 하지 않고 끝냅니다.
  2. floor 를 nextFloor 로 바꿉니다.
  3. 지난 글과 같은 자리로 플레이어를 옮깁니다. 이번에는 옮길 대상이 이 플레이어 자신, self.Entity 입니다.

첫째 줄이 필요한 이유는, 출구에 닿았다는 알림이 한 번만 온다는 보장이 없기 때문입니다. 같은 출구에 두 번 닿아도 한 층만 올라가야 합니다. 이 함수도 ServerOnly 로 둡니다. 몇 층인지 정하는 것도, 플레이어를 옮기는 것도 서버가 해야 모두에게 같게 보입니다. 확인용으로 log 로 지금 층을 찍어 둡니다.

다음은 FloorExit 입니다. 닿은 플레이어에게서 GetComponent 로 TowerClimber 를 꺼냅니다. GetComponent 는 엔티티에 붙어 있는 컴포넌트를 찾아 주는 함수이고, 찾은 컴포넌트의 함수를 부르면 다른 스크립트에게 일을 시킬 수 있습니다. TowerClimber 가 없으면 플레이어가 아니니 그냥 끝내고, 있으면 ClimbTo 에 nextFloor 를 넘깁니다. 플레이어를 옮기던 두 줄은 FloorExit 에서 지웁니다.

출구가 플레이어의 TowerClimber 를 찾아 ClimbTo 를 부른다
출구가 플레이어의 TowerClimber 를 찾아 ClimbTo 를 부른다
RootDesk/MyDesk/FloorExit.mlua
@Component
script FloorExit extends Component

    property integer nextFloor = 0

    @ExecSpace("ServerOnly")
    @EventSender("Self")
    handler HandleTriggerEnterEvent(TriggerEnterEvent event)
        local player = event.TriggerBodyEntity
        ---@type TowerClimber
        local climber = player:GetComponent("script.TowerClimber")
        if climber == nil then
            return
        end
        climber:ClimbTo(self.nextFloor)
    end

end

실행해서 1층 출구에 닿으면 Console 에 floor 1 이, 2층 출구에 닿으면 floor 2 가 찍힙니다. 코드의 층 번호는 0 부터 세기 때문에 1층이 0, 2층이 1 입니다. 줄 앞의 SERVER 는 이 값을 서버가 바꾸고 서버가 찍었다는 뜻입니다.

2층에서 발을 헛디뎌 1층으로 떨어진 뒤 1층 출구에 다시 닿으면 이번에는 올라가지 않습니다. 1 은 지금 층 1 보다 높지 않으니까요. 떨어졌을 때의 규칙은 다음 글에서 게임오버를 만들며 정리합니다.

서버와 클라이언트 (OnSyncProperty, Sync)

MSW 게임은 서버 하나와 클라이언트 여럿으로 돌아갑니다. 서버는 게임의 진짜 상태를 갖고 있는 컴퓨터이고, 클라이언트는 플레이어 한 사람 한 사람의 화면입니다. 서버에도, 클라이언트에도 같은 플레이어 엔티티와 같은 TowerClimber 가 하나씩 있습니다.

floor 는 서버에서 바뀌었습니다. 그럼 내 화면, 클라이언트의 floor 는 몇일까요? 알아보려면 클라이언트에서 값을 찍어 봐야 합니다.

  1. TowerClimber 의 + 버튼에서 OnSyncProperty 를 고릅니다. 서버에서 바뀐 값이 클라이언트에 도착했을 때 불리는 함수라서 ClientOnly 에서만 실행됩니다.
  2. 매개변수 name 에는 바뀐 Property 의 이름이, value 에는 새 값이 들어옵니다. name 이 floor 일 때 value 를 log 로 찍습니다.

실행해서 1층 출구에 닿아 보면, SERVER 의 floor 1 은 찍히는데 CLIENT 쪽은 아무것도 찍히지 않습니다. floor 의 Sync 가 None 이기 때문입니다. None 은 값을 서버 안에만 두고 클라이언트로 보내지 않습니다. 처음에 None 으로 둔 것은 이 장면을 먼저 보기 위해서였습니다.

floor 위의 Sync 글자를 눌러 Sync 로 바꾸고 다시 실행하면, 이번에는 SERVER 다음에 CLIENT 의 floor 1 도 찍힙니다. 서버의 값이 내 화면까지 온 것입니다.

Sync 는 서버의 값을 모든 클라이언트에 보낸다
Sync 는 서버의 값을 모든 클라이언트에 보낸다

Sync 글자는 누를 때마다 셋 중 하나로 바뀝니다.

  • None 아무에게도 보내지 않고 서버 안에서만 씁니다.
  • Sync 접속한 모든 클라이언트에 보냅니다.
  • TargetUserSync 그 플레이어 한 사람의 클라이언트에만 보냅니다. 내 층을 나만 알면 되는 게임이라면 보내는 양이 더 적습니다.

우리는 다른 사람이 몇 층에 있는지도 쓸 수 있게 Sync 로 둡니다.

RootDesk/MyDesk/TowerClimber.mlua
@Component
script TowerClimber extends Component

    @Sync
    property integer floor = 0

    @ExecSpace("ServerOnly")
    method void ClimbTo(integer nextFloor)
        if nextFloor <= self.floor then
            return
        end
        self.floor = nextFloor
        log("floor " .. self.floor)
        local start = Vector3(0, nextFloor * 4 + 0.5, 0)
        _TeleportService:TeleportToMapPosition(self.Entity, start, "map01")
    end

    @ExecSpace("ClientOnly")
    method void OnSyncProperty(string name, any value)
        if name == "floor" then
            log("floor " .. value)
        end
    end

end

한 가지 꼭 기억할 것이 있습니다. 클라이언트에서 값을 바꿔도 서버로는 가지 않습니다. 보내는 길은 서버에서 클라이언트, 한 방향뿐입니다. 값은 서버가 정하고, 화면에 필요한 값만 Sync 로 클라이언트에 보냅니다.

값이 가는 길은 서버에서 클라이언트로 한 방향이다
값이 가는 길은 서버에서 클라이언트로 한 방향이다

정리

  • DefaultPlayer 에 TowerClimber 를 붙여 플레이어마다 층 번호 floor 를 갖게 했습니다.
  • 위층으로 옮기는 일은 ClimbTo 함수로 플레이어에게 맡겼습니다.
  • 출구는 GetComponent 로 TowerClimber 를 찾아 몇 층으로 갈지만 알려 줍니다.
  • 서버에서 바뀐 값은 Sync 로 클라이언트에 보냅니다.

다음 글에서는 게임오버를 만듭니다. 발을 헛디뎌 떨어지면 탑 1층부터 다시 시작하게 할 텐데, 서버는 플레이어가 떨어졌다는 것을 어떻게 알 수 있을까요?

막힌 곳이 있으면 디스코드에 몇 화의 어느 부분인지 같이 적어 주세요.