-
생각해보니 어제 했던 코드카타 정리는 가뿐히 넘기고 개인과제에 대해서만 적었네요.. [머쓱]
없는 숫자 더하기
주어진 수가 무조건 1부터 9 사이의 값이기 때문에 저는 1~9까지 모두 더하고 받은 배열에 있는 숫자들을 더해서 빼줬습니다.
var numberArray = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numberArray.Except(numbers).Sum();
배열 A.Except(비교할 배열 B) : A에 있는 값들 중 B에 있는 값과 동일한 값이 있다면 그 값을 제외한 나머지 값을 반환합니다.
따라서 1~9 중에 B에 없는 값들만 A에 남습니다. 남은 값들을 .Sum을 통해서 더해줍니다.
제일 작은 수 제거하기
for문을 이용해서 배열 요소들을 하나씩 비교하면서 가장 작은 수를 찾아 foreach를 이용해서 가장 작은 수 일 경우 저장하지 않고 index도 증가하지 않도록 하였습니다.
int v = arr.Min(); int[] answer = arr.Where(x => x != v).ToArray();
배열 A.Min() : A에 있는 값들 중 가장 작은 값을 반환합니다.
배열 A.Where(조건) : 조건에 맞는 A의 요소를 반환합니다.
Where의 경우, Python에서만 사용하는 예제들이 나와서 정확하지 않습니다.
배열의 값이 가장 작은 값이 아닐 경우 반환을 하는 데 그게 int 형식이므로 ToArray를 통해 배열 형식으로 바꿔주는 것 같습니다.
내적
두 배열을 서로 곱해주는 것이기 때문에 간단하게 넘어갔습니다.
return a.Zip(b, (t1, t2) => t1 * t2).Sum();
배열 A.Zip(배열 B) : A와 B를 묶어줍니다.
Zip에 대해서 많이 찾아보려고 했는데 위와 마찬가지로 Python에서 사용되는 함수이다 보니깐 Python에 관해서 적힌게 많더라구요.
그래서 출력해볼려고 노력해봤는데 출력을 하기 위해서는 람다를 사용해서 A와 B의 요소를 계산하여 foreach문을 이용해서 출력하는 방법밖에 없는 것 같습니다.
foreach (int i in a.Zip(b, (t1, t2) => t1 * t2)) { Console.WriteLine(i); }
문자열 내림차순으로 배치하기
Array.Sort()하면 대문자가 소문자보다 큰 것으로 처리되게 때문에 알맞지 않습니다.
배열 A.OrderByDescending으로 내림차순으로 정리해야지 됩니다.
public string solution(string s) { string answer = ""; char[] a = s.ToCharArray(); System.Array.Sort(a); System.Array.Reverse(a); answer = new string(a); return answer; }
근데 이분꺼는 왜 되는거죠?
public string solution(string s) { string answer = ""; string[] str = new string[s.Length]; for (int i = 0; i<s.Length; i++){ str[i] = s[i].ToString(); } Array.Sort(str); Array.Reverse(str); foreach (string st in str){ answer+=st; } return answer; }
배열을 만들 때 string 타입으로 만들어서 올바르게 작동되지 않은 것 같습니다.
이유는 저도 모르겠습니다... ㅠㅠ
오늘은 개인 프로젝트보다는 강의를 따라 연습하는 경우가 많았습니다.
강의 내용에서 개인프로젝트의 요구사항과 비슷하거나 일치하는 내용을 다룰 경우에는 강의를 듣고 그것을 가지고 개인프로젝트를 진행했는데요.
필수 요구사항
1. 캐릭터 만들기
2. 캐릭터 이동
3. 방 만들기
4. 카메라 따라가기
5. 캐릭터 애니메이션 추가
6. 이름 입력 시스템
7. 캐릭터 선택 시스템
필수 요구사항에서 1,2번은 어제 완성했고 3번까지 강의내용을 확인했습니다. 4번부터는 아직 강의내용에 나오지 않아서 하지 않았습니다. 그래서 3. 방 만들기 정리하고 이후에 강의내용을 정리해보려고 합니다.
3. 방 만들기 : 타일맵을 이용해서 맵을 만듭니다. 콜라이더를 이용해서 벽을 넘어가지 못하게 만듭니다.
create - 2D Object - Tilemap - Rectangular
생성하게 되면 부모 요소로 Grid가 생겨나게 됩니다. UI를 만들 때 Canvas가 필요한 것처럼 Tilemap을 만들려면 Grid가 필요한가 봅니다.
Window - 2D - Tile Palette
맵을 만들 때 바닥이나 벽에 들어갈 이미지를 받아둡니다. Tilemap이기 때문에 격자모양에 맞춰서 맵을 그릴 수 있도록 이미지도 격자에 맞춰서 있습니다.
가운데 Tilemap은 Hierarchy에 만들어둔 Tilemap을 선택하면 되는 것이며, Scene 오른쪽 아래에 Focus On - Tilemap으로 하게되면 지정한 게임 요소에 어떻게 그려뒀는지 볼 수 있습니다.
Tile Palette를 사용하기 전에 Prefab을 설정해줘야합니다. 맵을 그릴 때 사용되는 이미지를 저장하는 Prefab입니다.
원하는 이미지를 선택해서 Scene에 있는 격자를 선택해주면 이미지가 추가되면서 그려집니다.
이미지를 지우고 싶으면 위에 지우개 모양(오른쪽에서 두번째)를 선택해주면 지워집니다.
맵을 완성하지는 않았습니다. 아직 어떤걸 넣을지 구상을 하지 않은 상태이기 때문입니다. 아마 뒤에 필수 요구사항과 선택 요구사항을 만들면서 추가된 것을 보고 그리지 않을까 싶습니다.
강의 내용 들어가겠습니다.
개인 프로젝트는 단순히 움직이고 npc와 대화한다고 한다면 강의 내용은 던전을 만드는 것으로 플레이어가 공격도 하고 몬스터도 있는 게임을 만드는 내용입니다.
강의 내용을 바탕으로 개인 프로젝트 1,2번을 했기 때문에 캐릭터 움직임에 대한 정리는 생략하고 공격에 대한 내용을 적어보도록 하겠습니다.
몬스터를 공격하기 위해서는 캐릭터가 몬스터에게 조준을 해야하는데 이것을 에임 시스템이라 하겠습니다.
에임 시스템을 만들기 위해서 캐릭터가 바라보고 있는 방향이 중요합니다. 캐릭터가 바라보는 방향은 마우스의 방향으로 설정하고 그에 맞춰서 조준을 하도록 설정해줍니다.
//TopDownAimRotation.cs private void RotateArm(Vector2 direction) { float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; armRenderer.flipY = Mathf.Abs(rotZ) > 90f; characterRenderer.flipX = armRenderer.flipY; armPivot.rotation = Quaternion.Euler(0, 0, rotZ); }
몬스터가 캐릭터의 양 옆에 항상 있을 수 없기 때문에 조준을 하기 위해서 무기를 회전시켜줍니다.
Atan2(y, x) : x와 y의 값을 이용해서 x축과 벡터 사이의 각도를 라디안(파이) 값으로 계산해준다.
Rad2Deg : 라디안 값을 디그리 값(도)로 바꿔준다.
flip : true일 경우 이미지는 그대로, false일 경우 이미지를 반대로 바뀐다.
flipX는 좌우로, flipY는 상하로 이미지를 바꿔줍니다.
개인프로젝트에서는 좌우로 바꾸기 위해서 Scale 값을 음수와 양수로 바꿔서 사용해줬습니다.
armPivot의 하위에는 무기의 이미지와 무기가 공격하는 위치가 있습니다. 따라서 armPivot을 회전하게 되면 이미지와 공격 위치도 같이 회전하게 됩니다.
Quaternion.Euler(x, y, z) : x, y, z에 각도를 주게되면 그에 맞게 회전시켜준다.
Quaternion은 4차원의 값을 이용하여 회전값을 줘 회전시켜줍니다. Euler는 오일러를 이용하여 3차원 값을 4차원으로 계산해서 Quaternion에 넣어줍니다. 오일러를 사용하기 위해서는 x, y, z 값은 디그리여야합니다. 그렇기 때문에 라디안을 디그리로 바꿔주었습니다.
이러면 캐릭터는 마우스 방향에 따라 좌우가 바뀌게 되며, 무기는 마우스 방향으로 회전하게 됩니다.
이때, 무기의 회전축이 조금 이상한데요. 왜 회전축은 무기 이미지의 중심이 아닌가를 고민하게 되었습니다. 그래서 두 가지의 이유를 생각하게 되었는데요.
1. 메인 캐릭터 아래에 무기가 종속되어있기 때문에 무기를 rotation한다 해도 무기의 중심이 아닌 캐릭터의 중심을 기준으로 돌고 있음 -> 아님
2. Euler가 vecter3를 4차원 값으로 변환시키는데 어케 변환시키는지는 모르겠지만 변환시키는 과정에서 중심이 바뀌고 그것에 따라서 돌아가고 있음1은 3D로 보면서 아님을 확인했고 2는 함수를 뜯어보려했지만 정말 무슨 말인지 몰라서 확인하지 못했습니다. 아마 이유 2 때문에 그렇지 않을까 싶습니다.
조준에 성공을 하게 되었다면 공격을 해야합니다. 현재 플레이어가 들고 있는 무기는 활이기 때문에 화살을 만들어보도록 하겠습니다.
화살은 플레이어가 누를 때마다 생겨야하므로 Prefab으로 만들어줍니다.
그리고 개인프로젝트에서는 공격기능이 필요없기 때문에 만들지 않았지만 여기는 필요하기 때문에 Input Action에 Fire을 추가해줬습니다. Fire은 좌클릭을 할 때마다 실행되도록 설정해줬고 따라서 OnMove와 OnLook을 생성했던 곳에 OnFire을 생성해줍니다.
//PlayerInputController.cs public void OnFire(InputValue value) { IsAttacking = value.isPressed; }
//TopDownCharacterController.cs protected bool IsAttacking { get; set; } private void HandleAttackDelay() { if(Stats.CurrentStates.attackSO == null) return; if(IsAttacking && _timeSinceLastAttack > Stats.CurrentStates.attackSO.delay) { _timeSinceLastAttack = 0; CallAttackEvent(Stats.CurrentStates.attackSO); } } public void CallAttackEvent(AttackSO attackSO) { OnAttackEvent?.Invoke(attackSO); }
플레이어가 좌클릭을 하면, IsAttacking == true 가 되고 if문이 실행이 되면서 CallAttackEvent 함수가 실행됩니다.
그렇게되면 OnAttackEvent를 구독하고 있는 함수들에게 안내가 가게 됩니다.
//TopDownShooting.cs void Start() { _controller.OnAttackEvent += OnShoot; _controller.OnLookEvent += OnAim; }
구독하고 있는 함수가 실행됩니다. OnShoot은 화살이 플레이어에 있는 공격 포인트에서 생성된다 라는 함수이구요...
사실 강의 따라가다가 적기만하고 설명을 하나도 못들었습니다..... ㅠㅠㅠㅠ 다시 듣고 다시 정리할께요 ㅠㅠㅠ
공격을 할 줄 안다면 이제 공격력 등 캐릭터의 스탯을 만들어야합니다.
//AttackSO.cs [CreateAssetMenu(fileName = "DefaultAttackData", menuName = "TopDownController/Attacks/Default", order = 0)] public class AttackSO : ScriptableObject { [Header("Attack Info")] public float size; public float delay; public float power; public float speed; public LayerMask target; [Header("Knock Back Info")] public bool isOnKnockback; public float knockbackPower; public float knockbackTime; }
CreateAssetMenu : Asset에서 create를 할 때 나오게 만들도록 한다.
fileName은 create할 때 무엇을 만들 것인지 결정해주고 menuName은 create를 누르고 나오는 이름입니다. TopDownController를 선택하면 Attacks가 나오고 Attacks를 선택하면 Default가 나오는 순서를 /로 정해줍니다.
order은 같은 위치에서 정렬되는 순서입니다. 클 수록 아래에 생성됩니다.
Scriptable Object : 한 개의 데이터 컨테이너를 만들고 공유해준다.(Prefabs랑 비슷한 느낌)
[Header] : Inspector에서 확인할 때 아래의 변수들을 설명해주는 부제목 느낌.
사용해주면 변수들에 대해서 한눈에 알아 볼 수 있어서 좋다.
마찬가지로 공격에 대한 데이터를 생성해줍니다.
//RangedAttackData.cs : AttackSO [CreateAssetMenu(fileName = "RangedAttackData", menuName = "TopDownController/Attacks/Ranged", order = 1)]
공격에 대한 데이터를 생성해주었으면, 캐릭터의 기본 체력이나 스피드 등 스탯 데이터를 만들어줍니다.
//CharacterStats.cs public enum StatsChangeType; [Serializable] public class CharacterStats{}
[Serializable] : 클래스의 변수를 Inspector에서 나타나게 한다. enum은 [Serializable]이 없어도 된다.
class가 MonoBehaviour를 상속받지 않으면 유니티에서 제공하는 것들을 사용할 수 없다.
정리는 여기까지! 사실 투사체 구현하기랑 애니메이션 만드는 것까지 강의를 들었지만... 아직 정리하기에는 머릿 속에서 이전 것도 정리가 덜 됐는데 이것마저 정리하려니 아득하네요..
화요일까지 개인과제 제출이라서 주말에는 강의 들으면서 다시 보고 다시 정리해서 더 잘 써보도록 하겠습니다..
할게 너무 많아..