
오늘 학습 키워드
최종 팀 프로젝트
오늘 학습 한 내용을 나만의 언어로 정리하기
튜터님의 피드백
- 점프 어색함
- 벽점은 더 어색함
- HandleInput 중복 너무 많음. 상위로 올리던지 하셈
- enum flag를 써보셈
피드백대로 고쳐보기
1. 점프 어색한 문제
- 좀 주관적이라서 어떻게 해야할 지 모르겠음.
- 그래서 기획이랑 좀 더 얘기를 해봤음
- 바닥이 너무 잡아 끄는 느낌이라고 했음. 그래서 그래비티를 조절해봄
- 원래 중력 배수 4였던거 3으로 줄임
- 점프 힘힘 조절함
2. 벽점 어색한 문제
- 이건 인정
- 지금 물리 처리 안하고 그냥 애니메이션 커브 써서 하고있어서 그런듯
- 일단 물리를 쓰던 이전 버전으로 돌리자
public override void LogicUpdate(PlayerController controller)
{
animRunningTime += Time.deltaTime;
t = animRunningTime / wallJumpAnimationLength;
newPos = Vector2.Lerp(startPos, targetPos, t);
curPos = controller.transform.position;
// 현재 위치에서 이동할 위치만큼 선 하나 그어서, 그게 벽에 닿으면 벽 끝에까지만 가고 상태 바뀌게함
Vector2 direction = (newPos - curPos).normalized;
float distance = Vector2.Distance(curPos, newPos);
RaycastHit2D hit =
Physics2D.Raycast(controller.transform.position, direction, distance, controller.Move.groundMask);
if (hit.collider != null)
{
controller.Move.rb.MovePosition(hit.point - direction * 0.01f);
if (controller.Move.isGrounded) controller.ChangeState<IdleState>();
else controller.ChangeState<FallState>();
return;
}
controller.Move.rb.MovePosition(newPos);
if (Vector2.Distance(newPos, targetPos) < 0.01f)
{
if (controller.Move.isGrounded)
{
controller.ChangeState<IdleState>();
return;
}
controller.ChangeState<FallState>();
}
if (controller.Move.isGrounded)
{
controller.ChangeState<IdleState>();
return;
}
}- 이랬더니 너무 갑자기 크게 움직임
- Lerp 대신 SmoothDamp를 써보자
- SmoothDamp : current 에서 target까지 부드럽게 움직임. smoothTime동안 목적지에 (거의?) 도착하도록 처리함. (완전히 정확한 시간은 아님)
- 참고 자료
- 해봤는데 무한하게 계속 이동함…
- 원점으로 돌아가서, rigidbody의 velocity를 조정하자
public void WallJump()
{
Vector2 jumpDir = new Vector2(lastWallIsLeft ? 1 : -1, 1).normalized;
float jumpPower = 20f;
float jumpSidePower = 12f;
rb.velocity = Vector2.zero;
// AddForce로 점프 적용
/*rb.AddForce(new Vector2(jumpDir.x * jumpSidePower, jumpDir.y * jumpPower), ForceMode2D.Impulse);*/
rb.velocity = new Vector2(
(lastWallIsLeft ? 1 : -1) * jumpSidePower,
jumpPower
);
}
- 현재까지 스테이지에서 벽을 활용한 기믹이 없었을 뿐더러 벽 점프에 필요 이상으로 시간이 소요되고 있었다는 점, 그리고 벽 점프 때문에 중력 관련 값을 조정하는데도 시간이 걸린다는 점 때문에 삭제하기로 함
- 아쉬우니까 아카이빙
// WallJumpState.cs
public class WallJumpState : AirSubState
{
private float wallJumpStartTime;
private float wallHoldAbleTime = 0.5f;
private float startFallTime = 0.2f;
private float animRunningTime = 0f;
private float wallJumpAnimationLength;
private float wallJumpSpeed = 5f;
private float wallJumpDistance = 5f;
private Vector2 wallJumpDirection;
private Vector2 targetPos;
private Vector2 newPos;
private Vector2 curPos;
private Vector2 startPos;
private Vector2 nextPos;
/*private float wallJumpDirection;*/
private float wallJumpMoveX = 8f;
private float wallJumpMoveY = 3f;
private Vector2 smoothSpeed;
private float smoothTime = 0.1f;
private float t;
public override void Enter(PlayerController controller)
{
if (!controller.Condition.TryUseStamina(controller.Data.wallJumpStamina))
{
if (controller.Move.isGrounded)
{
controller.ChangeState<IdleState>();
return;
}
else
{
controller.ChangeState<FallState>();
return;
}
}
base.Enter(controller);
// 벽점할 때에는 벽 반대방향 봐야됨
controller.Move.ForceLook(!controller.Move.lastWallIsLeft);
controller.isLookLocked = true;
// 벽점했으니까 강제로 벽 터치 취소
controller.Animator.ClearBool(); // WallHold 끄려고
controller.Move.rb.velocity = Vector2.zero;
controller.Move.isWallTouched = false;
controller.Animator.SetTriggerAnimation(AnimatorHash.PlayerAnimation.WallJump);
controller.Move.isWallJumped = true;
wallJumpStartTime = Time.time;
animRunningTime = 0f;
wallJumpAnimationLength =
controller.Animator.animator.runtimeAnimatorController
.animationClips.First(c => c.name == "StartWallJump").length
+ controller.Animator.animator.runtimeAnimatorController
.animationClips.First(c => c.name == "WhileWallJump").length * 2f;
wallJumpDirection = new Vector2((controller.Move.lastWallIsLeft ? 1.5f : -1.5f),1f).normalized;
startPos = controller.transform.position;
targetPos = startPos + (wallJumpDirection * 2f);
// 시도하려는 것 : 포물선 운동을 강제로 만들기
controller.Move.wallJumpStartY = startPos.y;
controller.Move.WallJump();
/*wallJumpDirection = controller.Move.lastWallIsLeft ? 1 : -1;*/
}
public override void HandleInput(PlayerController controller)
{
var moveInputs = controller.Inputs.Player.Move.ReadValue<Vector2>();
if (controller.Inputs.Player.Jump.triggered && !controller.Move.isDoubleJump)
{
controller.ChangeState<DoubleJumpState>();
return;
}
if(Time.time - wallJumpStartTime > wallHoldAbleTime && controller.Move.isWallTouched)
{
controller.ChangeState<WallHoldState>();
return;
}
else
{
controller.Move.isWallTouched = false;
}
if (controller.Inputs.Player.NormalAttack.triggered && moveInputs.y < 0)
{
controller.isLookLocked = true;
controller.ChangeState<DownAttackState>();
return;
}
if (controller.Inputs.Player.NormalAttack.triggered && !controller.Attack.HasJumpAttack)
{
controller.isLookLocked = true;
controller.ChangeState<NormalJumpAttackState>();
return;
}
if (controller.Inputs.Player.SpecialAttack.triggered)
{
controller.isLookLocked = false;
controller.ChangeState<SpecialAttackState>();
return;
}
if (controller.Inputs.Player.Dodge.triggered)
{
controller.ChangeState<DodgeState>();
return;
}
if (controller.Inputs.Player.AdditionalAttack.triggered)
{
controller.ChangeState<AdditionalAttackState>();
return;
}
}
public override void LogicUpdate(PlayerController controller)
{
curPos = controller.transform.position;
if (controller.Move.rb.velocity.y <= 0 || Vector2.Distance(startPos, curPos) >= wallJumpDistance)
{
Debug.Log("[Wall Jump] 플레이어가 낙하 중 or 초기 위치와 멀어짐");
if (controller.Move.isGrounded) controller.ChangeState<IdleState>();
else controller.ChangeState<FallState>();
return;
}
if (controller.Move.isWallTouched)
{
controller.ChangeState<FallState>();
return;
}
/*animRunningTime += Time.deltaTime;
t = animRunningTime / wallJumpAnimationLength;
/*newPos = Vector2.Lerp(startPos, targetPos, t);#1#
newPos = Vector2.SmoothDamp(
controller.transform.position, targetPos, ref smoothSpeed, smoothTime ); curPos = controller.transform.position;
// 현재 위치에서 이동할 위치만큼 선 하나 그어서, 그게 벽에 닿으면 벽 끝에까지만 가고 상태 바뀌게함
Vector2 direction = (newPos - curPos).normalized; float distance = Vector2.Distance(curPos, newPos); RaycastHit2D hit =
Physics2D.Raycast(controller.transform.position, direction, distance, controller.Move.groundMask); if (hit.collider != null)
{ controller.Move.rb.MovePosition(hit.point - direction * 0.01f); if (controller.Move.isGrounded) controller.ChangeState<IdleState>(); else controller.ChangeState<FallState>(); return; }
controller.Move.rb.MovePosition(newPos);
if (Vector2.Distance(newPos, targetPos) < 0.01f)
{ if (controller.Move.isGrounded) { controller.ChangeState<IdleState>(); return; } controller.ChangeState<FallState>(); }
if (controller.Move.isGrounded)
{ controller.ChangeState<IdleState>(); return; }*/ /*float x = startPos.x + controller.Move.jumpXCurve.Evaluate(t) * wallJumpDirection * wallJumpMoveX;
float y = startPos.y + controller.Move.jumpYCurve.Evaluate(t) * wallJumpMoveY;
nextPos = new Vector2(x, y);
Vector2 direction = (nextPos - (Vector2)controller.transform.position).normalized; float distance = Vector2.Distance(controller.transform.position, nextPos);
RaycastHit2D hit = Physics2D.Raycast(controller.transform.position, direction, distance, controller.Move.groundMask);
if (hit.collider != null) { if (!hit.collider.CompareTag("Platform")) { controller.Move.rb.MovePosition(hit.point - direction * 0.01f); if (controller.Move.isGrounded) controller.ChangeState<IdleState>(); else controller.ChangeState<FallState>(); return; } }
controller.transform.position = nextPos;
if (t >= 1f) { if (controller.Move.isGrounded) { controller.ChangeState<IdleState>(); return; } else { controller.ChangeState<FallState>(); return; } }*/ }
public override void Exit(PlayerController controller)
{
base.Exit(controller);
controller.isLookLocked = false;
controller.Move.rb.velocity = new Vector2(controller.Move.rb.velocity.x, 0);
}
}// WallHoldState.cs
public class WallHoldState : AirSubState
{
public override void Enter(PlayerController controller)
{
base.Enter(controller);
controller.Move.ForceLook(!controller.Move.lastWallIsLeft);
controller.Attack.ClearAttackCount();
controller.isLookLocked = true;
controller.Move.rb.velocity = Vector2.zero;
}
public override void Exit(PlayerController controller)
{
base.Exit(controller);
controller.isLookLocked = false;
controller.Condition.canStaminaRecovery.Value = true;
// player.PlayerAnimator.ClearBool();
}
public override void HandleInput(PlayerController controller)
{
var moveInput = controller.Inputs.Player.Move.ReadValue<Vector2>();
if (!controller.Move.isWallTouched)
{
controller.ChangeState<FallState>();
return;
}
// 벽이 있는 방향으로 입력이 들어왔을 때
if (((moveInput.x < 0 && controller.Move.lastWallIsLeft)
|| moveInput.x > 0 && !controller.Move.lastWallIsLeft) )
{
// 점프 키가 눌림 and 벽점 가능함
if(controller.Inputs.Player.Jump.triggered && controller.Move.CanWallJump())
{
// Debug.Log("벽점으로");
controller.ChangeState<WallJumpState>();
return;
}
if (controller.Move.isWallTouched)
{
// Debug.Log("중력 감소");
controller.Move.ChangeGravity(true);
return;
}
}
if (moveInput.x == 0)
{
controller.ChangeState<FallState>();
return;
}
if (controller.Move.isGrounded)
{
controller.ChangeState<IdleState>();
return;
}
if (controller.Inputs.Player.SpecialAttack.triggered)
{
controller.isLookLocked = false;
controller.ChangeState<SpecialAttackState>();
return;
}
if (controller.Inputs.Player.Dodge.triggered)
{
controller.ChangeState<DodgeState>();
return;
}
if (controller.Inputs.Player.AdditionalAttack.triggered)
{
controller.ChangeState<AdditionalAttackState>();
return;
}
}
public override void LogicUpdate(PlayerController controller)
{
controller.Move.ApplyWallSlideClamp();
controller.Move.ForceLook(!controller.Move.lastWallIsLeft);
if (controller.Move.rb.velocity.y < 0)
{
controller.Animator.SetBoolAnimation(AnimatorHash.PlayerAnimation.WallHold);
}
if (controller.Move.keyboardLeft != controller.Move.lastWallIsLeft)
{
controller.Move.Move();
return;
}
}
}// PlayerMove.cs
public void WallJump()
{
float jumpPower = 12f; float jumpSidePower = 20f; rb.velocity = Vector2.zero;
rb.AddForce(new Vector2((lastWallIsLeft ? 0.1f : -0.1f), 0), ForceMode2D.Impulse); // 벽에서 약간 밀어내기
// AddForce로 점프 적용
// rb.AddForce(new Vector2(jumpDir.x * jumpSidePower, jumpDir.y * jumpPower), ForceMode2D.Impulse); rb.velocity = new Vector2( (lastWallIsLeft ? 1 : -1) * jumpSidePower, jumpPower ); StartCoroutine(Controller.IgnoreInputInTime(0.5f));}
public bool CanWallJump()
{
// 벽점 당시에 왼쪽 벽인지 아닌지 확인한 다음에 벽 체크
rightWallCheckPos = (Vector2)transform.position + Vector2.right * checkDistance;
leftWallCheckPos = (Vector2)transform.position + Vector2.left * checkDistance;
Collider2D hit = Physics2D.OverlapBox(keyboardLeft? leftWallCheckPos : rightWallCheckPos, wallCheckBoxSize, 0f, groundMask); if(hit != curWall) { return true; } return false;}3. HandleInput 중복
- 해결할 예정임
- 근데 enum flag가 뭔지 궁금해서 찾아봄
- 참고 자료
- 원래는 enum이 값을 하나만 가질 수 있는데, flag 속성을 붙이면 여러 개를 담을 수 있음!
[Flags]
public enum eTransitionType
{
None = 0,
IdleState = 1 << 0,
MoveState = 1 << 1,
JumpState = 1 << 2,
DoubleJumpState = 1 << 3,
FallState = 1 << 4,
NormalAttackState = 1 << 5,
NormalJumpAttackState = 1 << 6,
DownAttackState = 1 << 7,
SpecialAttackState = 1 << 8,
DodgeState = 1 << 9,
StartParryState = 1 << 10,
SuccessParryState = 1 << 11,
DamagedState = 1 << 12,
DieState = 1 << 13,
PotionState = 1 << 14,
AdditionalAttackState = 1 << 15,
}- 원래 인터페이스였던 IPlayerState를 BasePlayerState라는 이름으로 추상클래스로 변경 (프로퍼티때매)
public abstract class BasePlayerState
{
public abstract eTransitionType ChangableStates { get; }
public abstract void Enter(PlayerController controller); // 상태 변화했을 때 돌아감
public abstract void HandleInput(PlayerController controller); // 입력에 따른 상태 전환 등을 처리
public abstract void LogicUpdate(PlayerController controller); // 실제 로직이 돌아가는 부분
public abstract void Exit(PlayerController controller); // 상태에서 나갈 때 돌아감
public bool CanChangeTo(eTransitionType next)
{
return ChangableStates.HasFlag(next);
}
public void TryChangeState(eTransitionType next, PlayerController controller)
{
if (ChangableStates.HasFlag(next))
{
System.Type type = controller.stringStateTypes[next.ToString()];
controller.ChangeState(type);
}
}
}- 각 상태들은 어떤 상태로 변화 가능한지를 ChangableState에 가지고 있을 거임.
플레이어 상호작용 만들기
public void TryInteract()
{
RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.up, 0.1f, interactableMask);
if (hit.collider != null)
{
if (hit.collider.TryGetComponent(out InteractableObject interactable))
{
interactable.Interact();
Controller.PlayerInputDisable();
}
}
}- 상호작용 가능한 오브젝트는 레이어가 Interactable로 되어있고, abstract class 인 InteractableObject를 상속받아 구성되어있음.
모의 면접 대비 공부
-
Q1. CSV/JSON 등 데이터 저장 포맷에 대해 설명하고, 활용에 적절한 상황을 설명해주세요.
-
A1. CSV는 행은 줄넘김, 열은 콤마로 나눠져있습니다. JSON은 딕셔너리 형태와 유사합니다. XML은 마크다운 언어인 HTML과 비슷하게 태그를 사용해서 데이터를 정의합니다. YAML은 공백으로 데이터를 구분합니다. CSV는 표 형식의 데이터를 작성하는데 용이합니다. Json은 복잡한 구조를 가진 데이터에 유용합니다. XML은 명확한 구조 정의가 필요한 문서에, YAML은 사람이 쉽게 읽을 수 있어야 하는 데이터에 적합합니다.
-
Q2. 월드 스페이스 (World Space) 와 로컬 스페이스 (Local Space)의 차이에 대해 설명해주세요.
-
A2. 월드 스페이스는 게임 전체의 절대 좌표계를 의미하고, 로컬 스페이스는 각각 개별적인 게임 오브젝트가 자기 자신을 기준으로 하는 상대 좌표계를 의미합니다.
-
Q3. 3D 공간에 있는 오브젝트들이 화면에 표현되는 픽셀로 표시되기까지의 과정을 설명해보세요.
-
A3. 3D 모델을 2차원에 투영하는 렌더링 과정을 렌더링 파이프라인이라고 합니다.
-
- Input Assembler (입력 조립) : Vertex Specification이라고도 하는데, CPU에서 렌더링을 수행할 도형의 정점(vertex) 정보를 정점 버퍼라는 자료구조에 담아서 GPU로 보냄.
- 정점 버퍼는 위치, 노말, 색상, uv 값들을 가지고 있고, 구조체가 아닌 직렬화된 형태로 담겨있음
- GPU는 정점 버퍼를 받아서 정점 데이터(구조체)로 변경함
- 조립하는 과정에서, 삼각형과 같은 기본적인 도형(프리미티브)으로 조립해줌.
- 조립된 프리미티브는 정점 쉐이더에 입력됨
-
- Vertex Shader : 1단계에서 받은 정점 데이터의 local space 좌표를 world space 좌표로 변겅하고, 카메라가 담는 view space로 변환한 뒤, 투영을 통해 투영 공간인 clip space로 바꿈
- 이 단계에서 행렬 연산이 진행되는데, Model - View - Projection 순서로 진행함.
- M, V, P는 순서 이름이기도 하지만 변환 행렬의 이름이기도 함.
- P (View space → Clip Space) 단계에서, 컬링 (카메라 절두체 외에 있는 부분을 지움)과 클리핑 (정규 좌표에 의해 계산된 절두체에 걸쳐있는 부분)을 진행함
-
- Tesselation : 모델의 정점을 쪼개서 디테일한 표현 가능 (LOD)
-
- Geometry Shader : 2단계에서 생성되지 않은 임의의 정점을 추가하거나 삭제해 모델을 수정하는 셰이더. Tesselation이나 그림자 효과, 큐브 맵을 한번의 처리로 렌더링하는데 사용
-
- Rasterization : 정점 정보가 확정된 도형을 2D로 표현하기 위해 픽셀 데이터로 변환하는 단계.
- Viewport Transformation : 3차원 정규좌표 (NDC공간) 상의 좌표를 2D 스크린 좌표로 변환
- Scan Transformation : 프리미티브를 통해 픽셀 데이터(프래그먼트)를 생성하고, 여기를 채우는 픽셀을 찾아냄. 각 픽셀마다 정점 데이터를 보간해 할당
-
- Pixel Shader : (Fragment Shader) 렌더링 될 각각의 픽셀들의 색을 계산. Rasterizer가 전달한 픽셀 개수만큼 실행되며, 투명도 처리, 조명 처리, 그림자 처리, 텍스쳐에 색상 입히기 모두 Pixel Shader의 역할. 각 픽셀의 색상과 깊이를 출력으로 전달하는데, 색상은 컬러버퍼, 깊이는 Z버퍼에 저장됨. 이 버퍼를 통칭 스크린 버퍼라고 부름.
-
- Output Merge : 투명도와 깊이 값 등을 통해 픽셀끼리 경쟁해 색을 정하거나 합쳐서 최종적으로 화면에 그려질 픽셀을 정함. Z-Test, Stencil Test, Alpha Blending 등의 연산을 수행함.
-
-
축약본 :
- 렌더링 파이프라인은 크게 Input Assembler, Vertex Shader, Rasterization, Pixel Shader, Output Merge의 과정을 거칩니다.
- Input Assembler에서는 CPU가 GPU에게 정점 데이터를 보내줍니다.
- Vertex Shader에서는 GPU가 받은 정점 데이터를 토대로 local space에서 world space > view space > clip space로 좌표변환을 합니다.
- Rasterization에서는 정점 데이터가 확정된 도형을 2D로 바꾸기 위해 3D 공간상의 좌표를 2D 스크린상의 좌표로 바꾸고, 픽셀 데이터를 생성합니다.
- Pixel Shader에서는 렌더링 될 픽셀의 색을 결정합니다.
- Output merge에서는 픽셀들의 투명도, 깊이 값 등을 보고 화면에 어떻게 그려질지 정해서 출력합니다.
-
Q4. Tree의 순회(Traversal) 방법에 대해 설명해주세요.
-
A4. 트리 순회는 노드를 어떤 순서로 방문할지를 정하는 방식입니다. 전위는 루트를 먼저, 중위는 루트를 중간에, 후위는 루트를 마지막에 방문하는 차이가 있습니다.
-
Q4-2. 순회를 구현하는 방법들
-
A4-2. 트리의 순회는 재귀로 구현할 수 있음. 예를 들어 전위 순회의 경우 현재 노드를 방문하고 왼쪽 자식을, 그 후에 오른쪽 자식을 재귀적으로 순회하면 됨. 다만 깊은 트리의 경우 스택 오버플로우 위험이 있을 수 있어서, 스택으로 구현하는 방식을 고려해 보아야 함.
-
Q5. 각 길찾기 알고리즘의 차이점은 무엇인가요?
-
A5. DFS, BFS, 다익스트라, A*, 벨만-포드, 플로이드-워셜 등이 있음.
- DFS : 깊이 우선 탐색. 그래프를 최대한 깊게 탐색한 후에 다른 경로를 찾음
- BFS : 너비 우선 탐색. 시작지점부터 가까운 순서대로 탐색함. 가중치가 없는 그래프에서 최단경로를 찾음.
- 다익스트라 : BFS와 유사, 음의 가중치를 허용하지 않는 가중 그래프에서 최단경로를 찾음.
- A* : 다익스트라의 확장판. 휴리스틱 함수를 사용해 경로의 예상 비용을 고려함.
- 벨만-포드 : 음의 가중치가 있는 그래프에서도 최단경로를 찾음.
- 플로이드-워셜 : 모든 노드 쌍 간의 최단 거리를 한 번에 계산함.
-
Q6. A* 알고리즘에 대해 설명해주세요.
-
A6. 시작 지점과 목적지를 알고 있을 때, 최단 거리를 효율적으로 찾는 방법임. 다음에 방문할 정점을 선택할 때 최단 거리 경로를 유지할 수 있는 정점과, 목적지에 얼마나 가까운 정점인지 두 개를 동시에 고려해서 선정함.
-
Q7. 해당 알고리즘들을 프로젝트에 적용해본 경험이 있나요?
-
A7. 로봇 원격 제어 시뮬레이션을 제작할 때 다익스트라 알고리즘을 사용한 경험이 있습니다.
내일 학습 할 것은 무엇인지
- 다른 서브스테이트도 다 변경해줘야됨