통일성이 부족하다고 하여 보스 씬과 복셀 등을 깔끔하게 날려내고 메인 씬에 집중하기로 했다.
한 주 후기
이번 주에는 멘토링 중 피드백을 반영하여 기획을 다시 해보는 시간을 가졌다. 멘토님의 촌철살인과도 같은 피드백을 들으면서 우울하기도 했지만 정말 우리에게 필요한 내용들이라는 것을 안 뒤 바로 프로젝트에 적용하기로 했다. 그런데 적용하면서 보니 내가 만든 부분들이 많았고 그에 대한 허무감이 좀 들었다. 나는 이전에 여러 작업들을 도와주다가 보스씬을 메인으로 같이 작업하고 있었는데 보스씬 자체를 날리기로 결정했으니 '지금까지 뭐 했지?' 라는 생각이 든 것이다. 그렇게 멘탈적인 문제로 하루동안 떠돌며 작업 효율이 안 나오다가 당일 저녁에 팀원들과의 밥 약속에서 솔직하게 털어놓으니 팀원들이 오히려 격려해주며 같이 잘 해내보자고 했다. 팀원들과의 소통이 없었다면 더 안 좋은 상황이 길어졌을 것 같다. 꾸준하게 팀원들과 소통하는 것이 답인 것 같았다.
다음 주에는 확실히 정해진 기획으로 배경 이동과 리소스 교체 부분을 맡기로 하였다. 다시 열심히 해보도록 하자. 파이팅!
나는 보스 게임 씬을 담당하게 되었는데 그 중에서 노트UI를 생성하고 노트를 이동시키는 부분을 담당했다.
아래는 노트를 이동시키는 코드이다.
// 목표한 지점까지 현재 노트 시간에서 시작 노트 시간을 빼고
// 비트의 sample rate로 나누어 단위를 1로 만들어 준 뒤 UI의 width만큼 더해서 구현하였다.
protected virtual void MovePosition()
{
samplePerUnit = noteCreator.SampleRate;
// 목표 위치
Vector3 pos = noteCreator.transform.localPosition;
pos.x += noteCreatorTransform.rect.width / 2f;
Debug.Log(pos);
pos.x -= (noteCreator.CurrentSampleTime - myEvent.StartSample) / samplePerUnit * Screen.width;
transform.localPosition = pos;
}
아래는 씬을 합친 사진이다.
한 주 후기
이번 주는 정해진 기획 안에서 작업하는 것이라 굉장히 시간이 빨리 갔던 것 같다. 작업하면서 RectTransform에 대해서도 다시 찾아보게 되었고 노트 생성에서 오브젝트 풀링을 이용하기도 하였다. 제일 어려웠던 일이 다른 사람들의 작업과 내 작업을 합치는 것이었는데 합치면 오류나고 오류 고치면 다음 오류나고 이런 것이 반복되었다. 또한 연결하면서 생기는 새로운 작업들이 추가적으로 발생하여 일이 더 많아졌다. 계속 대면으로 소통하면서 진행하고 모르는 것은 물어보고 하니까 이후엔 꽤나 수월하게 진행한 것 같았다.
다음 주에는 추가적인 컨텐츠를 생각해보기로 했다. 정말 어려운 일이지만 팀원들과 의견을 조율하여 좋은 컨텐츠를 만들어봤으면 좋겠다. 다음 주도 파이팅!
기능 명세서를 작성하기 전에 어떤 것부터 작성해야할지 생각이 안 나서 먼저 큰 기능별로 나누어 어떤 요소들이 있나 조원과 이야기 하면서 정리하는 시간을 가졌다.
그 후에 그 작업물을 바탕으로 기능명세서를 작성하였다.
함수명, 기능 설명, 우선순위 등등 먼저 예상되는 기능들에 대한 정보를 작성해보았다.
이후에 기능 명세서를 바탕으로 관련된 것끼리 묶어 클래스 다이어그램을 작성했다.
팀 프로젝트 작업을 관리하기 위해 지라라는 툴도 사용하였다.
이를 통해 팀원이 무엇을 작업하고 있는지 전체적인 작업 진행도가 어떻게 되는지 알 수 있었다.
프로젝트원이 만들어 본 타이틀 페이지이다.
보스 리소스와 애니메이션을 만들어봤다. (테스트 중)
한 주 후기
이번 주에는 본격적으로 스프린트에 들어가기 전 리소스와 기능과 관련된 기술적인 코드들을 찾아보는 시간을 가졌다. 리듬게임에 필수적인 노트찍는 작업을 어떻게 할 지 고민이 많았다. 여러 에셋을 찾아가며 다른 사람은 어떻게 했는지도 찾아보고 시간과 공을 많이 들였다. 처음엔 유튜브(케이디)에 나와있는 대로 BPM을 자동 계산해주는 사이트로 곡의 BPM을 가져온 다음 정해진 공식을 사용하여 진행하려 했으나 우리가 찾던 기능인 유니티 에디터와 바로 연결할 수 있는 것이 아니어서 다른 것을 찾아보게 되었다. 그 다음으로는 깃허브에 참고될만한 작업물이 있어서 그 사람 것을 연구해보았다. 어떻게 사용하는지 까지는 알게 되었으나 이것도 우리가 사용하기엔 편하지 않아서 보류하게 되었다. 마지막으로 찾아본 것이 Koreographer라는 에셋인데 곡을 분석하여 노트를 찍어주고 변수와도 연동할 수 있다는 점이 좋아서 이걸로 채택하게 되었다. 아직 진행중이라 끝까지 이것으로 할지는 모르겠지만 매우 좋은 툴임에는 분명한 것 같다.
다음 주부터 진짜 본격적인 프로젝트의 시작이다. 분명 어려운 작업들이 많을 것이고 조원과도 의견 조율을 해야할 때가 올 것이다. 그래도 같은 목표를 가지고 있으니 열심히 소통하면 잘 되지 않을까 싶다. 다음 주도 파이팅!
우리 조는 플래포머 리듬게임으로 리듬에 맞춰서 키 입력을 하여 장애물을 피하는 게임을 만들기로 했다.
여러 툴들을 사용하여 기획 단계를 구체화했다.
피그마
플로우 차트
컨셉 기획서
한 주 후기
팀 빌딩과 기획을 하면서 팀원들과 소통 부분에서 어려운 점이 하나씩 생겼던 것 같다. 먼저 내가 생각하고 있는 것을 말로 표현하는 게 어려웠던 것 같다. 처음엔 어색하고 너무 많이 말하는 거 아닌가 했지만 그림을 그리거나 질의응답을 통하는 방법으로 내 의견을 전할 수 있게 되었다. 다음으로는 기획을 진행하면서 세세한 규칙이나 산출물의 이미지를 일치시키는 것이 어려웠던 것 같다. 사람마다 생각하고 이해하는 게 달라서 나중에 또 다시 질문하는 경우가 자주 있었다. 이건 어쩔 수 없이 계속 질문하고 답변해서 최대한 같은 그림을 그려가도록 하는 게 최선의 방법이었던 것 같다. 여기서 중요한 점은 이 일치된 내용을 기록하는 것이라고 생각한다. 문제도 좀 있었지만 팀원들이 서로 소통하려고 노력하고 즐거운 분위기에서 대화를 나눌 수 있어서 기획 시간이 하나도 지루하지 않고 재밌었던 것 같다.
다음 주에는 프로토타입을 만들어보면서 방법을 알아보기로 했다. 지금 BPM 관련해서 기술적으로 우리가 할 수 있는 방법을 찾고 있는데 빨리 찾아서 프로젝트를 진행하고싶다. 다음주도 파이팅!
이번 주에 주강사님이 편찮으신 관계로 알고리즘 수업으로 대체가 되었는데 정말 유익한 수업이었다. 알고리즘을 푸는데 그냥 막 푸는 것이 아니라 알고리즘 문제가 어떤 문제인지(어떤 시간 복잡도를 가지고 있는지 -> 어떤 알고리즘으로 풀 수 있는 문제인지)부터 파악하는 접근에서 시작하는 것이 중요하다는 것을 배웠다. 그리고 알고리즘 로드맵을 따라가면서 익숙해지면 이후의 것들은 이 로드맵 안의 것들을 활용하는 부분이라고 하셔서 이 로드맵부터 공부할 것이다. 이후에 알고리즘마다 나만의 템플릿을 만들어서 이를 적용해보도록 해야겠다.
또한 마무리 테스트 중 개발 테스트가 있었는데 알까기 어플을 역기획하여 따라 만들어보는 시간을 가졌다. 5일이라는 기간동안 핵심 기능들을 만들어 똑같이 동작하게 만드는 것이었는데 처음엔 어디서부터 시작해야할지 막막했던 것 같다. 이번엔 UI를 한 게임오브젝트로 묶어 Don'tDestroyOnLoad하는 방법을 사용했는데 코드 양이 550줄에 가까워져서 다음날 보면 내가 설정한 변수명이 뭐가 뭔지 헷갈리고 코드가 보기 어려운 경우가 자주 생기게 되었다. 이는 사람들이 왜 클래스를 기능별로 구분하는지를 깨닫는 경험이 되었고 다음에는 UI를 팝업UI 기본UI 등으로 분류하여 나누어 구현해야겠다는 생각이 들었다.
이번 주로써 베이직코스가 마무리 되었다. 다들 열심히 했고 그에 따른 결과가 있었다고 생각한다. 또한 남은 프로젝트 기간을 준비하면서 협업과 객체지향 부분을 공부할 생각이다. 이제 내가 스스로 공부하고 만들어 가야할 때가 왔으니 더 힘들 수도 있겠지만 꾸준히 노력하여 결실을 맺었으면 좋겠다. 다음주도 파이팅!!
스타 UML의 Extension을 사용하면 다이어그램을 코드로 코드를 다이어그램으로 만들 수 있다.
개체 관계 다이어그램
ERD - Entity RelationShip Diagram (DB)
열쇠 모양은 Primary Key
마름모 색 칠해진 것은 외래키 Foreign Key
DB 명령어
INSERT, DELETE, SELECT, UPDATE
CREATE, DROP, TRUNCATE
테이블 생성 / 파괴 / 내용 삭제(열 빼고 다 지움)
한 주 후기
이번 주에는 다이어그램에 대해 배웠다. 다이어그램을 이용하여 개발 단계를 미리 설계할 수 있다. StarUML을 사용하면서 시각적으로 보이는 다이어그램을 보면서 클래스들의 관계를 설계할 수 있다는 점이 좋은 것 같다. 그리고 서로 변환하는 extension은 정말 유용한 툴인 것 같다. 개체 관계 다이어그램에 대해서도 배웠는데 데이터베이스에 사용하는 다이어그램으로 나중에 데이터 관리를 할 때 이용하게 될 것으로 현업에서도 사용한다고 한다.
이번 주에는 마지막 주에 있을 시험공부를 하느라 굉장히 애를 썼던 것 같다. 다음 주가 베이직 코스 마지막 주차니 파이팅하고 프로젝트 기간도 열심히 해보자!
ex) Class Instance();, Instantiate(Resource.Load, new GameObject) → Start나 Awake 등에서 초기화 해주어야 함(미리 메모리에 올려놓지 않으면 파일 시스템에서 메모리에 올리는 작업을 반복하게 된다.)
ex) AddComponent, GetComponent 등 지양 → 전역변수로 만들어서 Start나 Awake 등에서 초기화
Resources.Load, SceneManager.Load의 Async (비동기)
LoadAsync, LoadAsync → 빨라지지 않는다
Enumerator로 떨어짐 (isDone 등 프로퍼티)
ex) 로딩 창 - 로드 작업은 파일 시스템에서 메모리에올리는 경우가 대부분(여기서 렉이 발생 할 수밖에 없음)
yield return (job);
WaitForSeconds();
AsyncOperation
빠르기 - 유저편의성
SceneLoadAsync Mode Additive (다음 씬 미리 추가) but 그냥 씬 로드시 유저가 끊기는 게 보임
requestedFrameRate (최대 상한선 FPS)
평상시에 유지하려는 경향
Cinematic
정지화면 → SRPG - 프레임이 높을 필요가 없음(10 ~ 15)
어차피 낮을건데 왜해요? ⇒ CPU의 Optimizations
CPU 온도 - CPU 안정성 (회로가 탐)
쓰로틀링(제조사) - CPU의 속도를 낮춤
superUser나 탈옥 등을 통해 풀면 오버클럭을 할 수 있음
안 쓸 때는 최대한 온도를 낮춰놓자
GPU 최적화
SOC 칩 있으면 CPU GPU 이동 효율 좋음 (대신 연산능력이 안 좋음 - 크기가 작음)
대신 영상편집 등은 많이 따라왔음
Iphone - Mali (GPU칩)
Android - Snapdragon, Mali … (GPU 칩)
GPU에 따라 프로파일러가 따로 있음(GPU 제조사마다 프로그램이 다름)
메모리에 텍스처가 올라간 방식이 나옴 - 제조사 쪽 직접 가서 볼 때
가끔 봐야할 경우가 생김 (유니티에선 SystemInfo - 간단한 그래프)
메모리 최적화
UI, 2D 최적화
Atlas Packing, Font Paking
메모리 낭비를 막기 위해 (들어갈 때 패딩 붙이는 연산이 들고, 뺄 때 패딩 뗴는 연산이 듬)
texture 가 POT로 떨어짐(Power of two - 2의 n승 * NPOT는 NotPOT인 것)
texture inspector 옵션 강제로 낮출 때 down 올려야할 때 Up
Sprite Packer
Full Rect : 마진 값이 들어간 이미지
Tight : 정방형 이미지일 때 씀
압축 POT
Sprite Packer
한 번에 관리하려는 용도
RGB, RGBA를 같이 압축하면 RGB기준으로 압축되어 A가 없어질 수 있어서 따로 압축해줘야 됨
UI는 Camera Culling(카메라 시야에 안들어 올 때 안 그리는 것)이 적용이 안 됨
Graphic RayCaster - Touch가 필요 없으면 꺼주기
- 모바일 환경에서의 리소스 최적화
한 주 후기
이번 주에는 최적화에 대해서 배웠다. 요즘에 PC는 다들 잘 나와서 크게 최적화가 필요 없을 수 있겠지만 모바일 환경이라면 최적화가 거의 필수적이다. 그래서 이번에 강의를 더 열심히 들었던 것 같다. 이번 강의를 듣는데 좀 아쉬웠던 부분이 이론적인 내용과 예시가 많았기 때문에 직접적으로 해보면서 느끼는 것은 덜해서 전체적으로 습득하지는 못했던 것 같다. 프로젝트를 마무리하는 부분에 있어서 그 때마다 최적화 기법이 다르니 실습을 하기도 쉽지 않았기 때문인 것 같긴 하다. 지금 하고있는 프로젝트의 마무리 단계에서 적용해보고 다시 이런 내용들을 실습해보는 과정을 겪어야 확실하게 이해할 수 있을 것 같다.
그래도 이번 최적화에 대한 내용은 어디서 배우기 힘든 내용들이라는 것에 되게 만족했다. 다음 주도 파이팅!
1. 플레이어 간에 게임 플레이 상호작용이 아닌 바로 없어져 버리는 것들의 전송 - RPC 사용O
2. 시기가 중요하지 않은 상점 내 구매(호출한 플레이어에게만 중요한 구매 - 자금감소, 인벤토리 추가) - RPC 사용 X
3. 이름, 색상, 스킨과 같은 초기 설정 - RPC 사용O
4. 게임 모드, 준비상태 - RPC 사용O
RPC 호출해보기
RPC는 틱 정렬이 되지 않으므로 FixedUpdateNetwork() 등 Fusion 입력 처리를 사용할 필요가 없다.
void Update()
{
// Object.HasInputAuthority는 NetworkObject.Spawn()에서 생성할 때 파라미터로 Runner를 정해주는데 결국 생성이 되었냐에 대한 것이다.
if (Object.HasInputAuthority && Input.GetKeyDown(KeyCode.R))
{
RPC_SendMessage("Hey Mate!");
}
}
RPC 속성 사용
- string을 받아와서 rpc의 info가 로컬이면 You said~ 아니면 Some other said ~ 로 텍스트를 출력한다.(누가 호출했는지)
// RpcSources : 전송할 수 있는 피어
// RpcTargets : 피어가 실행되는 피어
// 1. All : 모두에게 전송 / 세션 내의 모든 피어에 의해서 실행됨(서버 포함)
// 2. Proxies : 나 말고 전송 / 객체에 대하여 입력 권한 또는 상태 권한을 갖고 있지 않는 피어에 의해 실행됨
// 3. InputAuthority : 입력 권한 있는 피어만 전송 / 객체에 대한 입력 권한이 있는 피어에 의해 실행됨
// 4. StateAuthority : 상태 권한 있는 피어만 전송 / 객체에 대한 상태 권한이 있는 피어에 의해 실행됨
// RpcInfo
// - Tick : 어떤 곳에서 틱이 전송되었는지
// - Source : 어떤 플레이어(PlayerRef)가 보냈는지
// - Channel : Unrealiable 또는 Reliable RPC로 보냈는지 여부
// - IsInvokeLocal : 이 RPC를 원래 호출한 로컬 플레이어인지의 여부
// * 공식 문서엔 HostMode를 설정하지 않았지만 이걸 쓰지 않으면 계속 원격 플레이어가 된다. (기본이 서버 모드여서 그런 듯)
[Rpc(RpcSources.All, RpcTargets.InputAuthority, HostMode = RpcHostMode.SourceIsHostPlayer)]
public void RPC_SendMessage(string message, RpcInfo info = default)
{
if (!_messages)
{
_messages = FindObjectOfType<Text>();
}
Debug.Log(info.Channel);
Debug.Log(info.Source.RawEncoded);
Debug.Log(info.Source.PlayerId);
Debug.Log(Runner.Simulation.LocalPlayer.PlayerId);
if (info.Source == Runner.Simulation.LocalPlayer)
{
message = $"You said: {message}\n";
}
else
{
message = $"Some other player said: {message}\n";
}
_messages.text += message;
}
public class Player : NetworkBehaviour
{
// Networked 속성의 파라미터로 OnChanged라는 string 프로퍼티가 있는데
// OnChanged는 'static void OnChanged(Changed<MyClass> changed){}' 형식의 콜백이다.
[Networked(OnChanged = nameof(OnBallSpawned))]
// NetworkBool처럼 true false 값이 두 가지만 있는 경우 변경사항이 감지되지 않을 수가 있으므로
// byte/int 로 대체하고 호출할 때마다 값을 변경하는 방식으로 할수도 있다. (시각적 효과와 대역폭의 소모 중요도 차이)
public NetworkBool spawned { get; set; }
여기선 자식의 Meshrenderer에서 Material을 찾아 따로 Material프로퍼티를 만들어주는데 이 프로젝트의 경우 Player의 Cube 메시를 Player스크립트가 있는 부모와 구분해 자식으로 가게 했으므로 자식에서 찾는 GetComponentInChildren으로 찾아오는 것 같다. -> 매번 가져오지 않고 한 번만 세팅하기 위함
Material _material;
Material material
{
get
{
if (!_material)
{
_material = GetComponentInChildren<MeshRenderer>().material;
}
return _material;
}
}
위의 Networked속성의 파라미터 형식대로 함수를 만들었다.
여기서 spawned 값이 바뀐 Player의 material의 color를 흰색으로 바꿔준다.
이전에 가져온 material의 Color를 Color.Lerp를 통해 현재 컬러에서 목표 컬러까지 Time.deltaTime 비율만큼 부드럽게 변하게 만들었다.
// NetworkBehaviour를 상속하는 SimulationBehaviour에 있는 메서드이다.
// 색 변화 등을 Update가 아니라 Render에서 수행하는 이유는
// Render()가 퓨전의 FixedUpdateNetwork 이후에 호출되는 것이 보장되기 때문이다.
// 또한 Runner.DeltaTime이 아니라 Time.deltaTime을 사용하는 이유는
// 렌더링 자체는 Fusion 시뮬레이션 단위인 Runner.DeltaTime이 아니고
// 유니티의 render loop에서 실행되어서 그렇다.
public override void Render()
{
material.color = Color.Lerp(material.color, Color.blue, Time.deltaTime);
}
이후에 FixedUpdateNetwork의 마지막 부분에 spawned = !spawned를 추가하여 토글로 공을 생성할 때마다 값이 바뀌게 했다.
Fusion - Network Project Config - Server Physics Mode를 Client Prediction으로 하면 더 많이 연산이 들지만 더 정확하게 물리를 계산한다.
물리 객체는 NetworkRigidbody 컴포넌트로 동기화한다.
이전에 transform 이동과 비슷하게 구현했는데 여기선 Rigidbody의 Velocity를 forward로 하여 초기 속도 값을 정해줬다.
public class PhysxBall : NetworkBehaviour
{
[Networked] TickTimer life { get; set; }
public void Init(Vector3 forward)
{
life = TickTimer.CreateFromSeconds(Runner, 5.0f);
GetComponent<Rigidbody>().velocity = forward;
}
public override void FixedUpdateNetwork()
{
if (life.Expired(Runner))
{
Runner.Despawn(Object);
}
}
}
마우스 버튼을 하나 더 매핑해주고
public struct NetworkInputData : INetworkInput
{
// const로 선언한 상수는 내부에서 자동으로 static 가 된다.
public const byte MOUSEBUTTON1 = 0x01;
public const byte MOUSEBUTTON2 = 0x02;
public byte buttons;
public Vector3 direction;
}
조건 또한 비슷하게 만들어주었다.
public class PhotonInstantiate : MonoBehaviour, INetworkRunnerCallbacks
{
// NetworkObject의 요소로 GUID(고유ID)를 가지고 있다.
[SerializeField] NetworkPrefabRef _playerPrefab;
// PlayerRef default 0 : None, 1 : index 0, 2 : index 1
Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();
NetworkRunner _runner;
bool _mouseButton0;
bool _mouseButton1;
void Update()
{
// 입력은 FixedUpdateNetwork에서 틱 단위로 호출되고
// bool 체크는 Update에서 하니까 frame마다 호출되어 단위가 달라
// OnInput에서 실행하고 false로 만들어주는 식이다.
_mouseButton0 |= Input.GetMouseButton(0);
_mouseButton1 |= Input.GetMouseButton(1);
}
async void StartGame(GameMode mode)
{
// GameMode
// 1. 싱글 2. 쉐어드 - 포톤 클라우드 이용(사용자는 클라이언트) 3. 서버(게임 서버를 직접 지원하고 원격 플레이어만 허용)
// 4. 호스트(로컬 플레이어를 허용하는 게임서버) 5. 클라이언트(호스트나 게임 모드에 클라이언트로 시작)
// 6. 자동(첫 번째 접속시 호스트 모드, 나중 접속시 클라이언트 모드)
// NetworkRunner : 플레이어와 관련된 변수들을 가지고있는 클래스
_runner = gameObject.AddComponent<NetworkRunner>();
// NetworkRunner가 클라이언트로부터 Input을 받을지 여부
_runner.ProvideInput = true;
// NetworkRunner.StartGame(StartGameArgs args) : 매치메이킹 세팅을 해주는 메서드
// 비동기로 NetworkRunner.StartGame(StartGameArgs args) 실행
await _runner.StartGame(new StartGameArgs
{
GameMode = mode,
SessionName = "TestRoom", // 세션 이름(클라이언트와 서버 세션 이름)
Scene = SceneManager.GetActiveScene().buildIndex, // Scene은 구조체인 SceneRef?타입
// NetworkSceneManagerDefault : 비동기 씬 관련 메서드가 포함된 클래스
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>(), // SceneManager INetworkSceneManager 타입이다.
});
}
void OnGUI()
{
if (!_runner)
{
if (GUI.Button(new Rect(0, 0, 400, 100), "Host"))
{
StartGame(GameMode.Host);
}
if (GUI.Button(new Rect(0, 150, 400 , 100), "Join"))
{
StartGame(GameMode.Client);
}
}
}
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
// 연결중이면
if (runner.IsServer)
{
// 특정한 위치 저장
Vector3 spawnPosition =
new Vector3((player.RawEncoded % runner.Config.Simulation.DefaultPlayers) * 3, 1, 0);
// NetworkRunner.Spawn이 네트워크 Instantitate와 비슷한 역할인데 추가로 PlayerRef를 받는다.
NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
// 딕셔너리에 추가
_spawnedCharacters.Add(player, networkPlayerObject);
}
}
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
{
// 파괴 후 반환
runner.Despawn(networkObject);
_spawnedCharacters.Remove(player);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var data = new NetworkInputData();
if (Input.GetKey(KeyCode.W))
data.direction += Vector3.forward;
if (Input.GetKey(KeyCode.S))
data.direction += Vector3.back;
if (Input.GetKey(KeyCode.A))
data.direction += Vector3.left;
if (Input.GetKey(KeyCode.D))
data.direction += Vector3.right;
if (_mouseButton0)
{
data.buttons |= NetworkInputData.MOUSEBUTTON1;
}
if (_mouseButton1)
{
data.buttons |= NetworkInputData.MOUSEBUTTON2;
}
_mouseButton0 = false;
_mouseButton1 = false;
input.Set(data);
}
}
발사하는 부분도 프리펩과 Init()의 인자만 다를 뿐 다 같게 설정해주었다.
public class Player : NetworkBehaviour
{
[Networked] TickTimer delay { get; set; }
[SerializeField] Ball _prefabBall;
[SerializeField] PhysxBall _prefabPhysxBall;
NetworkCharacterControllerPrototype _cc;
Vector3 _forward;
void Awake()
{
_cc = GetComponent<NetworkCharacterControllerPrototype>();
_forward = transform.forward;
}
public override void FixedUpdateNetwork()
{
if (GetInput(out NetworkInputData data))
{
data.direction.Normalize();
_cc.Move(5 * data.direction * Runner.DeltaTime);
// OnInput에서 Set된 NetworkInputData.direction 값이 0보다 클 때
if (data.direction.sqrMagnitude > 0)
{
_forward = data.direction;
}
// NetworkRunner의 TickTimer가 도달했거나 세팅되지 않았을 때 -> 생성 빈도의 제한을 거는 것
if (delay.ExpiredOrNotRunning(Runner))
{
// NetworkInputData.MOUSEBUTTON1이 0x01이니 byte인 data.button이 1이고 &연산(둘 다 1일 때)으로 1이 될 때
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
// NetworkRunner의 바라보는 forward만큼 앞으로, 회전은 입력의 방향으로(data.direction의 방향)
// Object.InputAuthority는 누구의 입력에서 나왔는지에 대한 PlayerRef이다(PlayerID를 포함한 정보).
// Spawn하기 전에 파괴될 틱을 설정해주어야 하기 때문에(Life.CreateFromSeconds(NetworkRunner, int delayInSeconds))
// 마지막의 람다식 delegate void OnBeforeSpawned(NetworkRunner runner, NetworkObject obj)에서 Ball의 Init을 호출한다.
Runner.Spawn(_prefabBall, transform.position + _forward, Quaternion.LookRotation(_forward),
Object.InputAuthority,
(runner, o) =>
{
o.GetComponent<Ball>().Init();
});
}
else if ((data.buttons & NetworkInputData.MOUSEBUTTON2) != 0)
{
delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
// NetworkRunner의 바라보는 forward만큼 앞으로, 회전은 입력의 방향으로(data.direction의 방향)
// Object.InputAuthority는 누구의 입력에서 나왔는지에 대한 PlayerRef이다(PlayerID를 포함한 정보).
// Spawn하기 전에 파괴될 틱을 설정해주어야 하기 때문에(Life.CreateFromSeconds(NetworkRunner, int delayInSeconds))
// 마지막의 람다식 delegate void OnBeforeSpawned(NetworkRunner runner, NetworkObject obj)에서 Ball의 Init을 호출한다.
Runner.Spawn(_prefabPhysxBall, transform.position + _forward, Quaternion.LookRotation(_forward),
Object.InputAuthority,
(runner, o) =>
{
o.GetComponent<PhysxBall>().Init(10 * _forward);
});
}
}
}
}
}