공모전 일지11 - 멀티플레이

Boyeong·2023년 5월 21일
0

제페토

목록 보기
11/13
post-thumbnail

230501

[2023 메타버스 달서 공모전] 출품을 위해, 제페토를 공부하는 일지

1. 멀티플레이 설정

멀티플레이 제작하기

ZEPETO MultiPlay Guide - 멀티플레이 설정하기

(1) 서버 생성 및 실행

  • [Assets] 폴더에서 우클릭 → [Create] → [ZEPETO] → [Multiplay Server] 선택

  • 그러면 [Wolrd.multiplay] 폴더가 생성된다.

    • index.ts: 서버 메인 로직 코드를 담당한다.
    • schemas: 서버와 클라이언트 간 통신용 Data Structure를 확인할 수 있고, 인스펙터 창에서 통신에 필요한 타입을 추가할 수도 있다.
  • World ID를 생성하는 부분은 이미 했으니 스킵

  • 유니티 상단 메뉴 [Window] → [ZEPETO] → [Multiplay Server] 선택하면 다음과 같은 창이 뜬다.

  • 툴바에서, [Start Multiplay Server] 버튼을 눌러 서버를 실행한다.

    • 서버가 실행되면서, 버튼의 아이콘에 초록색 불이 켜지고, 아래의 창에서 멀티플레이 서버 로그도 확인할 수 있다.
    • 로그에서는 Gateway Port, 호스트 주소 등 현재 실행된 서버의 정보를 볼 수 있다.
    • 기본적으로 개발 서버는 로컬호스트에서 실행되고, 포트 번호는 프로젝트를 로드할 때마다 변경된다.

(2) 클라이언트 생성

  • 하이어라키 뷰에서 빈 게임 오브젝트를 생성하고, 이름을 WorldMultiplay로 설정한다.

  • 해당 오브젝트에 Zepeto World Multiplay 컴포넌트를 추가한다.

    • 이 컴포넌트는 클라이언트에서 사용하는 멀티플레이 매니저로, 앞서 추가한 Multiplay Package와 자동으로 연결된다.
  • 게임을 플레이하여, 클라이언트가 정상적으로 접속했다면 접속 로그를 확인할 수 있다.

2. 월드 로직 작성 1

멀티플레이 제작하기

ZEPETO MultiPlay Guide - 월드 로직 작성하기 1

  • 서버, 클라이언트 간 통신을 위해 필요한 Schema에 대해 알아보고, Schema Types와 Room State를 정의한다.
  • 서버에 데이터를 저장하는 방법을 알아본다.
  • 클라이언트에서 Room 생성 및 입장 시의 이벤트 처리 코드를 작성한다.
  • Room State를 통해 서버의 변경사항을 처리하는 코드를 작성한다.
  • Room message를 통해 클라이언트에서 서버로 State를 전송하고, 서버에서 message를 수신받아 State를 관리하는 방법을 알아본다.

(1) Schemas

  • schemas

    • Room State: Room에 접속 중인 플레이어와 관련 정보, 오브젝트 위치 등을 관리하기 위한 State Property
    • Schema Types: 서버-클라이언트 통신용 Data Structure
    • 인스펙터 창에서 편집할 수 있다.

(2) Schema Types 정의

  • number 타입의 x, y, z를 필드 값으로 갖는 Vector3를 추가한다.

  • 방금 전에 추가한 Vector3 타입의 position, rotation을 필드 값으로 갖는 Transform을 추가한다.

  • Player를 추가한다. string 타입의 sessionid, zepetoHash, zepetoUserId 필드와, Transform 타입의 transform 필드와, number 타입의 state 필드를 가지고 있다.

(3) Room State 정의

  • Room에 입장한 플레이어 정보를 관리하기 위해서, Player에 map 타입으로 players를 추가한다.

  • 그리고 맨 아래의 Apply를 클릭한다.

(4) Room 입장과 플레이어 초기화

  • Room Lifecycle Event
    • onCreate(options: SandboxOptions)
      • Room이 생성될 때 1회 호출된다.
      • Room에 대한 초기화 로직을 추가할 수 있다.
    • onJoin(client: SandboxPlayer)
      • 클라이언트가 Room에 입장할 때 호출된다.
      • 클라이언트의 ID 및 캐릭터 정보는 SandboxPlayer 객체에 포함되어 있다.
    • onLeave(client: SandboxPlayer, consented?: boolean)
      • 클라이언트가 Room에서 퇴장할 때 호출된다.
      • 클라이언트가 연결 해제를 요청한 경우, Consented 값이 true로, 그렇지 않은 경우 false로 호출된다.
    • onTick(deltaTime: number)
      • SandboxOptions에서 설정된 tickInterval마다 반복적으로 호출된다.
      • 각종 Interval 이벤트를 관리할 수 있다.

  • Room 입장 시 이벤트 처리하기
    • onJoin() 함수 작성
    • 클라이언트가 Room에 입장할 때 호출되기 때문에, 캐릭터 초기화 등 처음에 수행할 일을 처리한다.
  • 📑 index.ts 스크립트 수정

  • import

    import { Sandbox, SandboxOptions, SandboxPlayer } from "ZEPETO.Multiplay";
    import { DataStorage } from "ZEPETO.Multiplay.DataStorage";
    import { Player } from "ZEPETO.Multiplay.Schema";
  • onJoin() 함수

    // client가 Room에 입장 시 호출
    async onJoin(client: SandboxPlayer) {
        // 입장한 client의 정보
        console.log(`[OnJoin] sessionId: ${client.sessionId}, HashCode: ${client.hashCode}, userId: ${client.userId}`);
    
        const player = new Player();    // 스키마에서 정의한 Player 타입
        player.sessionId = client.sessionId;
    
        if (client.hashCode) {
            player.zepetoHash = client.hashCode;
        }
    
        if (client.userId) {
            player.zepetoUserId = client.userId;
        }
    
        // 플레이어 데이터를 서버에 저장
        const storage: DataStorage = client.loadDataStorage();
    
        // client의 방문 횟수 저장
        let visit_cnt = await storage.get("VisitCount") as number;
        if (visit_cnt == null) visit_cnt = 0;
    
        console.log(`[OnJoin] ${client.sessionId}'s visiting count: ${visit_cnt}`);
    
        await storage.set("VisitCount", ++visit_cnt);   // Player의 VisitCount를 갱신해서, Data Storage에 저장
    
        this.state.players.set(client.sessionId, player);   // 지금까지 설정한 Player의 정보를 Room State에 정의한 players에 저장
    }

  • 실행 결과

    • Unity 환경의 로컬 서버의 데이터는 메모리에만 저장되기 때문에, 서버를 종료한 후 다시 실행해도 데이터가 유지되지 않는다고 한다.
    • 월드 배포 후에는 ZEPETO DB에 저장되어서 데이터가 유지된다고 한다.

(5) 클라이언트에서 State 전송

클라이언트

  • ClientStarter.ts 스크립트를 생성하고, 비어있는 게임 오브젝트를 생성한 후 컴포넌트로 넣어 준다.

    • 이때 스크립트는 [World.multiplay] 폴더 바깥에 생성해 주어야 한다.
    • 이유는 모르겠지만 컴포넌트 적용이 되질 않았다.

  • 초반 Room 설정 & 캐릭터 초기화 코드 작성
  • 📑 ClientStarter.ts 스크립트 작성

  • import

    import { CharacterState, SpawnInfo, ZepetoPlayers } from 'ZEPETO.Character.Controller';
    import { Room, RoomData } from 'ZEPETO.Multiplay';
    import { Player, State } from 'ZEPETO.Multiplay.Schema';
    import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
    import { ZepetoWorldMultiplay } from 'ZEPETO.World';
    import * as UnityEngine from 'UnityEngine';
  • 변수

    public multiplay: ZepetoWorldMultiplay;
    private room: Room;
    
    // 클라이언트 고유 값인 sessionId로 player 객체 저장 → key: sessionId, value: player
    private currentPlayers: Map<string, Player> = new Map<string, Player>();
  • Start() 함수

    Start() {
        // Room Event Listener 등록
        this.multiplay.RoomCreated += (room: Room) => {
            this.room = room;
        };
    
        this.multiplay.RoomJoined += (room: Room) => {
            room.OnStateChange += this.OnStateChange;   // State가 변할 때마다 호출되는 함수
        };
    }
  • OnStateChange() 함수

    // 플레이어의 Join에 관련된 처리
    private OnStateChange(state: State, isFirst: boolean) {
        // ZepetoPlayers에 이벤트 리스너 등록
        if (isFirst) {
            // Local Player 인스턴스가 Scene에 완전히 로드되었을 때 호출된다.
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
                const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
    
                // character: 플레이에서 동적으로 생성된, 실제 Character Controller가 포함된 오브젝트
                myPlayer.character.OnChangedState.AddListener((cur, prev) => {
                    this.SendState(cur);    // 이 이벤트가 발생할 때마다 캐릭터 State(cur, prev)를 서버로 전송
                });
            });
        }
        
        let join = new Map<string, Player>();
    
        // 스키마의 Room State에 저장된 플레이어 정보를 하나씩 조회
        state.players.ForEach((sessionId: string, player: Player) => {
            // currentPlayers가 저장된 클라이언트의 sessionId를 가지고 있지 않으면
            // 방금 입장한 플레이어이니까, set으로 join에 등록
            if (!this.currentPlayers.has(sessionId)) {
                join.set(sessionId, player);
            }
        });
    
        // Room에 새 플레이어가 입장할 때 이벤트를 받을 수 있게, player 객체에 이벤트 연결
        join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
    }
    • 최초 접속 시 자동으로 호출되고, 서버에서 State가 변경되면 호출된다.
    • 첫 번째 인자 state: Room State에 정의된 스키마에 접근할 수 있다.
    • 두 번째 인자 isFirst: 첫 번째 호출을 판단한다. 두 번째 호출부터 false로 변경된다.
  • SendState() 함수

    private SendState(state: CharacterState) {
        const data = new RoomData();
    
        data.Add("state", state);   // CharacterState는 ZEPETO.Character.Controller에 정의된 enum 타입 (Invalid, Idle, Walk, Run 등)
        this.room.Send("onChangedState", data.GetObject());	// 클라에서 서버로 메시지 송신
    }
  • OnJoinPlayer() 함수

    // Room 입장 시 플레이어 이벤트 처리
    private OnJoinPlayer(sessionId: string, player: Player) {
        console.log(`[OnJoinPlayer] players - sessionId: ${sessionId}`);
        
        // 입장한 플레이어를 관리하기 위해, 지금 입장한 플레이어를 currentPlayers에 등록
        // 입장한 모든 플레이어는 currentPlayers에 등록되어, 지금 입장한 플레이어는 currentPlayers에 sessionId가 없는 경우로 판단
        this.currentPlayers.set(sessionId, player);
    
        // 플레이어 인스턴스의 초기 Transform 설정
        const spawnInfo = new SpawnInfo();
        const position = new UnityEngine.Vector3(0, 0, 0);
        const rotation = new UnityEngine.Vector3(0, 0, 0);
    
        spawnInfo.position = position;
        spawnInfo.rotation = UnityEngine.Quaternion.Euler(rotation);
    
        // Room과 Player의 sessionId가 같으면 Local Player이다.
        const isLocal = this.room.SessionId === player.sessionId;
    
        // 플레이어 인스턴스 생성
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
    }

  • ClientStarter 오브젝트에 ZepetoWorldMultiplay 컴포넌트가 추가된 오브젝트를 가져와서 할당한다.

    • 해당 컴포넌트는 client에 서버 Room 이벤트를 연동하는 인터페이스를 제공한다.
    • Room Event Listener
      • RoomCreated(Room)
        • Room이 생성되고, 접속 가능할 때 호출된다.
        • Room을 인자로 전달한다.
      • RoomJoined(Room)
        • 해당 Room에 접속되면 호출된다.
        • Room을 인자로 전달한다.
      • RoomLeave(RoomLeaveEvent)
        • 해당 Room에서 접속을 해제할 때 호출된다.
        • RoomLeaveEvent(상태 코드 정보)를 인자로 전달한다.
      • RoomReconnected(Room)
        • 해당 Room에 재연결되었을 때 호출된다.
        • Room을 인자로 전달한다.
      • RoomError(RoomErrorEvent)
        • 해당 Room에 에러가 발생했을 때 호출된다.
        • RoomErrorEvent(에러 코드 정보)를 인자로 전달한다.
      • RoomWeakConnection()
        • 해당 Room 객체와의 연결이 불안정할 때 호출된다.
  • 실행 결과

    • 유니티 플레이 버튼을 클릭하면, 방 입장 시점에 로그가 출력된다.

(6) 서버에서 State 전송받기

서버

  • 📑 index.ts 스크립트 수정

  • onCreate() 함수 작성

    onCreate(options: SandboxOptions) {
        // 클라이언트로부터 수신된 메시지 확인
        this.onMessage("onChangedState", (client, message) => {
            const player = this.state.players.get(client.sessionId);    // 메시지를 보낸 플레이어 정보 불러오기
            player!.state = message.state;
        });
    }

3. 월드 로직 작성 2

멀티플레이 제작하기

ZEPETO MultiPlay Guide - 월드 로직 작성하기 2

  • 플레이어의 위치 동기화부터 플레이어의 퇴장까지 진행한다.
  • 클라이언트의 현재 위치를 일정 시간 마다 서버로 전송하고, 서버에서 수신 받은 다른 플레이어의 위치를 동기화한다
  • 플레이어의 퇴장에 대한 처리도 진행해 월드 로직을 마무리한다.

(1) 내 위치 전송

클라이언트


  • 📑 ClientStarter.ts 스크립트 수정

  • Start() 함수 수정

    Start() {
        // ...
    
        // 서버에 내 위치 전송
        this.StartCoroutine(this.SendMessageLoop(0.1));
    }
    • 정기적으로 플레이어 위치를 전송하기 위해 코루틴을 사용
  • SendMessageLoop() 함수 작성

    private *SendMessageLoop(tick: number) {
        while (true) {
            yield new UnityEngine.WaitForSeconds(tick);
    
            // 룸이 없거나, 룸에 연결되지 않은 경우에 대한 예외 처리
            if (this.room != null && this.room.IsConnected) {
                const hasPlayer = ZepetoPlayers.instance.HasPlayer(this.room.SessionId);    // 로컬 플레이어의 인스턴스 존재 여부
    
                // 인스턴스가 있으면, myPlayer 객체에 해당 인스턴스 저장
                if (hasPlayer) {
                    const myPlayer = ZepetoPlayers.instance.GetPlayer(this.room.SessionId);
    
                    // 캐릭터가 움직이고 있는 경우
                    if (myPlayer.character.CurrentState != CharacterState.Idle) {
                        this.SendTransform(myPlayer.character.transform);
                    }
                }
            }
        }
    }
    • 인자로 메시지 전송 주기를 받는다.
    • 룸에 잘 연결된 경우, 내 캐릭터의 인스턴스를 받아 오고, 캐릭터가 움직이는 경우 내 위치를 받아 온다.
  • SendTransform() 함수 작성

    private SendTransform(transform: UnityEngine.Transform) {
            const data = new RoomData();
    
            const pos = new RoomData();
            pos.Add("x", transform.localPosition.x);
            pos.Add("y", transform.localPosition.y);
            pos.Add("z", transform.localPosition.z);
            data.Add("position", pos.GetObject());
    
            const rot = new RoomData();
            rot.Add("x", transform.localEulerAngles.x);
            rot.Add("y", transform.localEulerAngles.y);
            rot.Add("z", transform.localEulerAngles.z);
            data.Add("rotation", rot.GetObject());
    
            // onChangedTransform 타입으로, data를 메시지로 전송한다.
            this.room.Send("onChangedTransform", data.GetObject());
        }
    • 캐릭터의 위치, 회전 정보를 서버로 전송한다.
    • 이러한 방식으로, 직접 정의한 캐릭터 상태와 인벤토리 설정 등의 정보도 전달할 수 있다.

(2) Room 상태 동기화

서버


  • 📑 index.ts 스크립트 수정

  • onCreate() 함수 수정

    onCreate(options: SandboxOptions) {
        // ...
    
        // 개별 클라이언트의 위치 수신
        this.onMessage("onChangedTransform", (client, message) => {
            // transform이 변경된 player를 불러온다.
            const player = this.state.players.get(client.sessionId);
    
            // 메시지에서 값 받아와서 transform에 저장
            const transform = new Transform();
            transform.position = new Vector3();
            transform.position.x = message.position.x;
            transform.position.y = message.position.y;
            transform.position.z = message.position.z;
    
            transform.rotation = new Vector3();
            transform.rotation.x = message.rotation.x;
            transform.rotation.y = message.rotation.y;
            transform.rotation.z = message.rotation.z;
    
            player!.transform = transform;
        });
    }

(3) 다른 클라이언트 위치 수신

클라이언트


  • 📑 ClientStarter.ts 스크립트 수정

  • OnStateChange() 함수 수정

    private OnStateChange(state: State, isFirst: boolean) {
        // ZepetoPlayers에 이벤트 리스너 등록
        if (isFirst) {
            // ...
    
            // 다른 캐릭터 위치를 전송받는다.
            ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
                const isLocal = this.room.SessionId === sessionId;
    
                // 로컬 플레이어가 아닌 경우에만 업데이트
                if (!isLocal) {
                    const player: Player = this.currentPlayers.get(sessionId);
    
                    // OnChange 이벤트에, 플레이어 위치를 업데이트하는 OnUpdatePlayer 함수를 연결
                    player.OnChange += (ChangeValues) => this.OnUpdatePlayer(sessionId, player);
                }
            })
        }
    
        // ...
    }
  • ParseVector3() 함수 작성

    private ParseVector3(vector3: Vector3): UnityEngine.Vector3 {
        return new UnityEngine.Vector3(vector3.x, vector3.y, vector3.z);
    }
    • Vector3 타입을 변환해 주는 함수
    • Schema에서 정의된 Vector3 타입을, UnityEngine에서 정의된 Vector3 타입으로 변환한다.
  • OnUpdatePlayer() 함수 작성

    // 플레이어의 위치를 업데이트
    private OnUpdatePlayer(sessionId: string, player: Player) {
        const position = this.ParseVector3(player.transform.position);
    
        const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
        zepetoPlayer.character.MoveToPosition(position);
    
        // CharacterState가 Jump인 경우, 실제 캐릭터도 점프하도록
        if (player.state === CharacterState.JumpIdle || player.state === CharacterState.JumpMove) {
            zepetoPlayer.character.Jump();
        }
    }

(4) Room 퇴장 로직

서버 & 클라이언트


  • 📑 index.ts 스크립트 수정

  • onLeave() 함수 작성

    // 플레이어가 Room을 떠날 때 호출
    onLeave(client: SandboxPlayer, consented?: boolean) {
        // 퇴장한 player를 Room에서 관리하는 State의 players 목록에서 제거
        this.state.players.delete(client.sessionId);
    }
  • 📑 ClientStarter.ts 스크립트 수정

  • OnStateChange() 함수 수정

    // 플레이어의 Join, Update, Leave에 관련된 처리
    private OnStateChange(state: State, isFirst: boolean) {
        // ZepetoPlayers에 이벤트 리스너 등록
        if (isFirst) {
            // ...
        }
    
        let join = new Map<string, Player>();
        let leave = new Map<string, Player>(this.currentPlayers);   // currentPlayers로 초기화
    
        // 스키마의 Room State에 저장된 플레이어 정보를 하나씩 조회
        state.players.ForEach((sessionId: string, player: Player) => {
            // currentPlayers가 저장된 클라이언트의 sessionId를 가지고 있지 않으면
            // 방금 입장한 플레이어이니까, set으로 join에 등록
            if (!this.currentPlayers.has(sessionId)) {
                join.set(sessionId, player);
            }
    
            leave.delete(sessionId);    // 현재 room에 존재하는 플레이어는 모두 제거
        });
    
        // Room에 새 플레이어가 입장할 때 이벤트를 받을 수 있게, player 객체에 이벤트 연결
        join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
    
        // Room에서 플레이어가 퇴장할 때 이벤트를 받을 수 있게, player 객체에 이벤트 연결
        leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
    }
  • OnLeavePlayer() 함수 작성

    // Room 퇴장 시 플레이어 이벤트 처리
    private OnLeavePlayer(sessionId: string, player: Player) {
        console.log(`[OnLeavePlayer] players - sessionId: ${sessionId}`);
    
        this.currentPlayers.delete(sessionId);  // currentPlayers 목록에서 플레이어 제거
        ZepetoPlayers.instance.RemovePlayer(sessionId); // 플레이어 인스턴스 제거
    }

4. 서버 구동 및 멀티플레이 접속하기

멀티플레이 제작하기

ZEPETO MultiPlay Guide - 서버 구동 및 멀티플레이 접속하기

(1) 서버 구동 및 접속

  • [Start Multiplay Server] 버튼을 클릭해 초록불이 들어오면, 서버 로그로 작동을 확인한다.

  • 클라이언트 접속을 위해 플레이 버튼을 클릭하여, 콘솔과 서버 로그에서 SessionId를 확인한다.

  • 이전에 만들어 둔 Character Controller 오브젝트를 비활성화해야 한다.

    • 이 안의 코드 때문에 로컬 플레이어가 2명 생기는 오류가 발생한다.

(2) QR 코드로 접속

  • 툴바에서 서버에 초록불이 들어오는지(= 서버가 실행중인지) 확인한다.
  • [Play on Zepeto] 버튼을 클릭해, QR 코드를 생성한다.
  • 근데 QR 코드가 생성되지 않고 자꾸 UnhandledPromiseRejectionWarning이 뜬다. async-await에 try-catch 구문을 써야 한다는데...
    • 그래서 try-catch 구문도 추가해 보고 그냥 onJoin 함수에 async-await 지워봤는데도 똑같은 오류가 계속 뜬다..
    • 그리고 TS2532: Object is possibly 'undefined'. 오류도 뜨는데 왜 그런건지???
    • 코드를 처음부터 작성하여 함수를 작성할 때마다 [Play on Zepeto] 버튼을 눌러 QR이 생성되는지 확인해 본 결과, index.tsonCreate() 함수에서 작성한 메서드에서 메시지를 보낸 클라이언트 정보를 받아와 player에 저장하는데, 해당 player가 null인 경우도 있을 수 있어 해당 오류가 발생한 것 같았다.
    • 따라서 player!.state = massage.state;, player!.transform = transform;로 바꿔 주었다.
    • (오류 찾고 고치는 데 6시간 걸렸지만? 내가 해냄.)
  • 실행 결과

    • 접속도 잘 되고, 이동도 잘 되는 것 같다...
    • 근데 위치 동기화가 좀 이상하다 ㅠㅠㅠㅠㅠㅠ 점프도 잘 안되고

5. 애니메이션 모션 동기화

[Example] 멀티플레이 애니메이션 모션 V2 동기화 샘플


  • 📑 ClientStarter.ts 스크립트 수정

  • OnUpdatePlayer() 함수 수정

    // 플레이어의 위치를 업데이트
    private OnUpdatePlayer(sessionId: string, player: Player) {
        const position = this.ParseVector3(player.transform.position);
    
        const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
    
        // 캐릭터 이동
        zepetoPlayer.character.MoveToPosition(position);
    
        // 캐릭터 점프
        if (player.state === CharacterState.Jump) {
            if (zepetoPlayer.character.CurrentState !== CharacterState.Jump) {
                zepetoPlayer.character.Jump();
            }
        }
    }
  • 실행 결과

    • 처음에 애니메이션 동기화가 안 돼서, 다른 유저의 점프 애니메이션이 보이지 않았고, 따라서 이 정자 위로 캐릭터가 올라오지 못하는 현상이 발생하였었다.
    • 하지만 지금은 둘 모두 정자 위에 잘 올라와 있는 것을 볼 수 있다.

6. 다음에 할 일

  • 퀵채팅 기능 구현?
  • 앉는 모션도 동기화?
  • 영상 촬영 및 편집

7. 후기

  • 진짜... 역대급으로 어려웠다. 대충 로직은 이해가 가는데 세부적인 코드는 이해를 거의 못 한 상태라 응용을 못 하겠다. 제페토에서 제공하는 예제 코드를 활용하려 해도 못 할 것 같다...
  • 오타도 다 고치고 영상에서 나온 코드랑 다른 점이 있는지 10번은 넘게 봤는데도 계속 오류 나서 울고 싶었다 진짜로.......... 하지만 해냈다. 물론 결과가 엉성하긴 하다만... 나도 코딩 고수 되고 싶어요ㅠ_ㅠ
  • 멀티 구현이란 정말 어려운 거구나.. 정말 게임 만들다 보면, 이 세상의 모든 똥겜들이 대단하게 느껴진다.
  • 이젠 정말 선택과 집중이 필요할 때인 것 같다. 앉는 모션 동기화는 시간이 부족하기 때문에 현재 구현할 예정은 없지만, 그래도 뭔가 아쉬운 것은 어쩔 수 없는 것 같다. 퀵 채팅은 그나마 코드가 짧아서 도전해 볼 만한 것 같다. 그래서 이틀 정도 일정을 잡고 안 되면 바로 영상 찍어야지.

0개의 댓글