오늘의 학습 키워드

ToString("D2"), string.PadLeft(int num, char character), 인터페이스

 

공부한 내용

문자열 공백 0출력

저번에 배웠던 ToString("N2")는 소숫점을 나타낸다면 ToString("D2")를 하면 공백이 0으로 채워진다는 것을 배웠다.

void ShowState()
{
    string input = null;
    while (input != "0")
    {
        Console.Write
        (
            "상태 보기\n" +
            "캐릭터의 정보가 표시됩니다\n\n" +
            $"Lv. {character.Level.ToString("D2")}\n" +
            $"Chad ( {character.Name} )\n" +
            $"공격력 : {character.Attack}\n" +
            $"방어력 : {character.Defense}\n" +
            $"체력 : {character.Health}\n" +
            $"Gold : {character.Gold} G\n\n" +
            "0. 나가기\n\n" +
            "원하시는 행동을 입력해주세요.\n" +
            ">> "
        );

        input = Console.ReadLine();
    }

    Console.WriteLine();
}

아이템 인터페이스

인터페이스에 관해 궁금한 점이 있었는데 어떨 때 인터페이스를 사용하는지 궁금했다.

검색도 해보고 고민을 해봤지만 이건 내 영역을 넘어선 것 같아서 튜터님께 언제 사용하면 좋을지에 대해 질문을 드렸다.

인터페이스 사용과 관련하여 다음 내용을 참고하면 좋다.

1. 인터페이스는 공통적인 행위를 묶을 때 사용하면 좋다.

2. 로직이 조금이라도 들어간다면 클래스로 빼는 것이 덜 복잡할 수 있다.(인터페이스 안에서는 구현이 불가능하므로)

3. 최소한의 단위로 만들어야지 코드가 보기 좋다.

interface IItem
{
    void Equip(bool isEquip);
}

보기 안 좋은 코드일 수 있지만 일단 구현해보는 것으로 하고 나중에 리팩토링을 해봐야겠다고 생각했다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TextRPG_Single
{
    class Item : IItem
    {
        public enum ItemType
        {
            Weapon,
            Armor,
        }

        public struct ItemInfo
        {
            public ItemType Type { get; set; }
            public string Name { get; set; }
            public int AttackModifier { get; set; }
            public int DefenseModifier { get; set; }
            public bool IsEquiped { get; set; }
        }

        public ItemInfo Info = new ItemInfo();
        

        protected Item(ItemType type, string name, int attack, int defense, bool isEquiped)
        {
            Info.Type = type;
            Info.Name = name;
            Info.AttackModifier = attack;
            Info.DefenseModifier = defense;
            Info.IsEquiped = isEquiped;
        }

        public void Equip(bool isEquip)
        {
            Info.IsEquiped = isEquip;
        }
    }
}

 

오늘의 회고

 튜터님께 질문드리기 전에 내가 고민했던 것들을 정리 했었는데 막상 앞에서 질문하려니 부끄럽고 말도 잘 못드렸던 것 같다. 하지만 친절하게 잘 설명해주려고 하시고 동기부여도 해주시니 공부할 맛이 났다. 사실 엄청나게 깨달음을 주신 건 아니지만 코드엔 정답은 없고 상황마다 다르며 직접 구현해보고 내가 경험으로 쌓아가는 것이라 먼저 해보라는 말씀이 제일 기억에 남았던 것 같다. 무작정 짜보고 왜 이게 안 좋은지 판단하고 고치는 작업을 통해 내가 더 성장할 수 있는 기회를 주신 것 같아서 더 감사하다는 생각이 들었다.

 오늘 질문을 하면서 느낀 점은 튜터님이 있고 질문을 편하게 드릴 수 있다는 점이 얼마나 좋은 환경인지 조금이라도 깨닫게 되는 계기가 된 것 같다. 항상 열심히 해주시는 튜터님들, 매니저님들, 동료들을 생각하며 나도 또한 열심히 해야겠다. 내일도 파이팅!

 

 

참고 : 

https://acpi.tistory.com/122

 

[C#] 숫자앞에 0붙이기

1. PadLeft 함수 사용 int value = 100; string result = Convert.ToString(value).PadLeft(5, '0'); MessageBox.Show(result); 2. string.Format 함수 사용 int value = 100; string result = string.Format("{0:D5}", value); MessageBox.Show(result); 3. ToString

acpi.tistory.com

 

오늘의 학습 키워드

재귀

 

공부한 내용

Flood_Fill 문제

구현 방법 : 알고리즘 해결을 위해 생각했던 것이 좌표를 리스트에 넣어놓고 상하좌우를 값이 같은지 체크하면서 반복하면서 뻗어가는 방식이었다.

문제 : 해당 과제에서의 문제가 발생한 부분은 리스트 안에 다음 타일의 중복체크를 안 해서 스택 오버 플로우가 발생했다. 해해결 : FloodFillNext 메서드에 중복체크하는 구문을 통해 이를 해결하였다.

* HashSet이 List보다 더 런타임 속도가 빨라서 이후에 수정하였다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class Flood_Fill
    {
        // 2차원 배열이 주어지고 특정 좌표와 색이 주어진다.
        // 특정 좌표의 값을 바꾸는데 특정 좌표로부터 4차원(위,아래,왼,오른쪽)과
        // 연결되어있는 같은 값을 모두 color의 숫자로 바꾼다.
        static void Main(string[] args)
        {
            FloodFill(new int[][] { new int[] { 1, 1, 1 }, new int[] { 1, 1, 0 }, new int[] { 1, 0, 1 } }, 1, 1, 2);

        }

        private static void FloodFill(int[][] image, int sr, int sc, int color)
        {
            HashSet<(int, int)> posSet = new HashSet<(int, int)>();
            posSet.Add((sr, sc));
            FloodFillNext(image, posSet, sr, sc);

            foreach (var item in posSet)
            {
                image[item.Item1][item.Item2] = color;
            }

            //return image;
        }

        private static void FloodFillNext(int[][] image, HashSet<(int, int)> posSet, int sr, int sc)
        {
            int next = -1;

            if (sr > 0 && image[sr][sc] == image[sr - 1][sc])
            {
                next = sr - 1;
                if (!posSet.Contains((next, sc)))
                {
                    posSet.Add((next, sc));
                    FloodFillNext(image, posSet, next, sc);
                }

            }
            if (sr + 1 < image.Length && image[sr][sc] == image[sr + 1][sc])
            {
                next = sr + 1;
                if (!posSet.Contains((next, sc)))
                {
                    posSet.Add((next, sc));
                    FloodFillNext(image, posSet, next, sc);
                }
            }
            if (sc > 0 && image[sr][sc] == image[sr][sc - 1])
            {
                next = sc - 1;
                if (!posSet.Contains((sr, next)))
                {
                    posSet.Add((sr, next));
                    FloodFillNext(image, posSet, sr, next);
                }
            }
            if (sc + 1 < image[0].Length && image[sr][sc] == image[sr][sc + 1])
            {
                next = sc + 1;
                if (!posSet.Contains((sr, next)))
                {
                    posSet.Add((sr, next));
                    FloodFillNext(image, posSet, sr, next);
                }
            }
        }
    }
}

https://leetcode.com/problems/flood-fill/description/

 

Flood Fill - LeetCode

Can you solve this real interview question? Flood Fill - An image is represented by an m x n integer grid image where image[i][j] represents the pixel value of the image. You are also given three integers sr, sc, and color. You should perform a flood fill

leetcode.com

 

 

오늘의 회고

 오늘은 알고리즘 강의와 여러 알고리즘 문제들을 풀면서 공부하는 시간을 가졌다. 아직 많이 모자라고 부족한 점도 많지만 문제를 분석하고 정리해둔 후 코드로 바꾼다는 접근법을 알게된 것만으로 많이 배웠다고 생각한다. 알고리즘 풀 때는 비슷한 유형의 문제들을 연속적으로 풀어서 그 유형에 익숙해지도록 해야겠다.

 강의 과제로 문제들을 풀면서 힌트도 보고 했는데 내일은 힌트를 보지 않고 강의에 나온 문제들을 다시 복습하는 시간을 가져야겠다. 내일도 파이팅!

최빈값 구하기

주어진 int 배열 값 중에서 자주 나오는 값을 반환하면 되는데 이때 최빈값이 여러 개라면 -1을 반환하면 되는 문제였다.

 

나의 풀이

값들을 담는 frequentDict라는 딕셔너리를 만든 뒤

첫 for문에서 값들을 중복체크하여 딕셔너리에 담고

첫 번째 foreach문에서 max키와 value를 구한다.

두 번째 foreach문에서 최빈값이 여러 개인지 확인해 맞다면 -1을 answer에 대입한다.

Dictionary<int, int> frequentDict = new Dictionary<int, int>();
int answer = 0;

for (int i = 0; i < array.Length; i++)
{
    if (!frequentDict.ContainsKey(array[i]))
    {
        frequentDict.Add(array[i], 1);
    }
    else
    {
        frequentDict[array[i]]++;
    }
}
int max = 0;
foreach (var item in frequentDict)
{
    if (item.Value > max)
    {
        max = item.Value;
        answer = item.Key;
    }
}


foreach (var item in frequentDict)
{
    if (answer != item.Key && max == item.Value)
    {
        answer = -1;
        break;
    }
}

return answer;

 

다른 사람의 풀이

array를 n으로 그룹화하여 g.Count()를 사용해 대응되는 개수를 지정하였다.

이후 list의 cnt 기준으로 Max()를 이용하여 max인 것을 찾아 max에 넣는다.

max가 2개 이상이면 answer에 -1을 대입하고 아니면 첫번째의 n(키값)을 대입한다.

짧은 구문이지만 꽤나 이해하기 어려운 것 같다. 다시 복습해보자.

var list = array.GroupBy(x => x, g => g, (x, g) => new { n = x, cnt = g.Count() });
var max = list.Where(x => x.cnt == list.Max(o => o.cnt));
int answer = max.Count() == 1 ? max.First().n : -1;

 

https://school.programmers.co.kr/learn/courses/30/lessons/120812

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

몫 구하기

두 수를 나눠 몫을 구하면 되는 문제였다.

using System;

public class Solution {
    public int solution(int num1, int num2) {
        return num1 / num2;
    }
}

 

https://school.programmers.co.kr/learn/courses/30/lessons/120805

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

오늘의 학습 키워드

Task

 

공부한 내용

스네이크 게임

스네이크 게임 구현

스파르타 동료분의 코드를 참고하여 진행하였습니다.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SnakeGame
{
    class Program
    {
        static int score = 0;
        static int highScore = 0;
        static int foodCount = -1;

        static Map map;
        static SnakeHead player;
        static ConsoleKeyInfo input;
        static Vector2Int direction = new Vector2Int(1, 0);

        static bool applicationQuit = false;
        static bool isEnterPressed = false;

        static void Main(string[] args)
        {
            Init();
            Route();
            while (!applicationQuit);
        }

        private static async void Route()
        {
            bool isCrashed = false;

            Task task = Task.Run(() =>
            {
                while (true)
                {
                    input = Console.ReadKey(true);
                    if (input.Key == ConsoleKey.Escape)
                    {
                        applicationQuit = true;
                        break;
                    }
                    else if (input.Key == ConsoleKey.Enter)
                    {
                        isEnterPressed = true;
                    }
                }
            });

            while (!applicationQuit)
            {
                await Task.Delay(100);

                if (isCrashed)
                {
                    Console.Clear();
                    map.DrawMap();
                    Console.WriteLine("===============================================================");
                    Console.WriteLine($"점수 : {score}  최고 점수 : {highScore}  먹은 수 : {foodCount}");
                    Console.WriteLine("===============================================================");
                    Console.WriteLine("죽었습니다. 다시 시작 : Enter");

                    if (isEnterPressed)
                    {
                        Reset();
                        Init();
                        isEnterPressed = false;
                        isCrashed = false;
                    }
                }
                else
                {
                    switch (input.Key)
                    {
                        case ConsoleKey.RightArrow:
                            direction = new Vector2Int(1, 0);
                            break;
                        case ConsoleKey.LeftArrow:
                            direction = new Vector2Int(-1, 0);
                            break;
                        case ConsoleKey.UpArrow:
                            direction = new Vector2Int(0, -1);
                            break;
                        case ConsoleKey.DownArrow:
                            direction = new Vector2Int(0, 1);
                            break;
                    }

                    isCrashed = Update();
                }
            }
        }

        private static void Reset()
        {
            score = 0;
            foodCount = -1;

            map = null;
            player = null;
            direction = new Vector2Int(1, 0);
        }

        static bool Update()
        {
            Console.SetCursorPosition(0,0);
            if (!map.IsFoodExist())
            {
                foodCount++;
                map.SpawnRandomFood();
            }

            bool isCrashed = player.TryMove(map, direction);
            map.DrawMap();
            Console.WriteLine("===============================================================");
            Console.WriteLine($"점수 : {score}  최고 점수 : {highScore}  먹은 수 : {foodCount}");
            Console.WriteLine("===============================================================");
            Console.WriteLine("ESC를 누르면 종료");
            score++;
            if (score > highScore)
            {
                highScore = score;
            }

            isEnterPressed = false;
            return isCrashed;
        }

        private static void Init()
        {
            int mapX = 50, mapY = 25;
            map = new Map(mapX, mapY);
            player = new SnakeHead(new Vector2(mapX / 2, mapY / 2));
        }

        public enum ObjectType
        {
            BLANK,
            WALL,
            FOOD,
            SNAKE_BODY,
            SNAKE_HEAD,
        }

        public struct Vector2
        {
            public int X { get; set; }
            public int Y { get; set; }

            public Vector2(int x, int y)
            {
                X = x;
                Y = y;
            }
        }

        public struct Vector2Int
        {
            private int x, y;

            public int X 
            {   
                get => x; 
                set
                {
                    if (value > 1)
                    {
                        x = 1;
                    }
                    else if (value < -1)
                    {
                        x = -1;
                    }
                    else
                    {
                        x = value;
                    }
                } 
            }

            public int Y
            {
                get => y;
                set
                {
                    if (value > 1)
                    {
                        y = 1;
                    }
                    else if (value < -1)
                    {
                        y = -1;
                    }
                    else
                    {
                        y = value;
                    }
                }
            }
            
            public Vector2Int(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        }

        public class Transform
        {
            public Vector2 position = new Vector2(0, 0);
            public Vector2Int rotation = new Vector2Int(0, 0);
        }

        class Object
        {
            public Transform trans = new Transform();
            public ObjectType objType;

            public Object(Vector2 pos, Vector2Int rot, ObjectType type)
            {
                trans.position = pos;
                trans.rotation = rot;

                objType = type;
            }
        }

        class Map
        {
            public int X { get; private set; }
            public int Y { get; private set; }

            public Object[][] tiles { get; private set; }

            public Map(int x, int y)
            {
                X = x;
                Y = y;

                // 세로줄이 y개인 2차원 배열
                tiles = new Object[Y][];

                for (int i = 0; i < Y; i++)
                {
                    // 안에 가로줄의 요소가 x개인 배열
                    tiles[i] = new Object[X];
                }

                for (int i = 0; i < Y; i++)
                {
                    for (int j = 0; j < X; j++)
                    {
                        Vector2 pos = new Vector2(X, Y);
                        Vector2Int rot = new Vector2Int(0, 0);
                        Object tile;

                        // 벽 만들기
                        if (i == 0 || i + 1 == Y || j == 0 || j + 1 == X) // 경계 부분이라면
                        {
                            if (i == 0)
                            {
                                if (j == 0)
                                {
                                    rot = new Vector2Int(-1, 1);
                                }
                                else if (j + 1 == X)
                                {
                                    rot = new Vector2Int(1, 1);
                                }
                                else
                                {
                                    rot = new Vector2Int(0, 1);
                                }
                            }
                            // -1
                            else if (i + 1 == Y)
                            {
                                if (j == 0)
                                {
                                    rot = new Vector2Int(-1, -1);
                                }
                                else if (j + 1 == X)
                                {
                                    rot = new Vector2Int(1, -1);
                                }
                                else
                                {
                                    rot = new Vector2Int(0, -1);
                                }
                            }
                            else
                            {
                                if (j == 0)
                                {
                                    rot = new Vector2Int(-1, 0);
                                }
                                else if (j + 1 == X)
                                {
                                    rot = new Vector2Int(1, 0);
                                }
                            }

                            tile = new Object(pos, rot, ObjectType.WALL);
                        }
                        else // 경계 부분이 아니라면 기본 Vector2Int(0,0);
                        {
                            tile = new Object(pos, rot, ObjectType.BLANK);
                        }

                        tiles[i][j] = tile;
                    }
                }
            }

            public void DrawMap()
            {
                for (int i = 0; i < Y; i++)
                {
                    for (int j = 0; j < X; j++)
                    {
                        if (tiles[i][j].objType == ObjectType.BLANK)
                        {
                            Console.Write(' ');
                        }
                        else if (tiles[i][j].objType == ObjectType.FOOD)
                        {
                            Console.Write('$');
                        }
                        else if (tiles[i][j].objType == ObjectType.SNAKE_HEAD)
                        {
                            Console.Write('@');
                        }
                        else if (tiles[i][j].objType == ObjectType.SNAKE_BODY)
                        {
                            Console.Write('#');
                        }
                        else if (tiles[i][j].objType == ObjectType.WALL)
                        {
                            if (tiles[i][j].trans.rotation.X == -1 && tiles[i][j].trans.rotation.Y == 0
                                || tiles[i][j].trans.rotation.X == 1 && tiles[i][j].trans.rotation.Y == 0)
                            {
                                Console.Write('|');
                            }
                            else if (tiles[i][j].trans.rotation.Y == -1 && tiles[i][j].trans.rotation.X == 0
                                || tiles[i][j].trans.rotation.Y == 1 && tiles[i][j].trans.rotation.X == 0)
                            {
                                Console.Write('-');
                            }
                            else
                            {
                                Console.Write('+');
                            }
                        }
                    }
                    Console.WriteLine();
                }
            }

            public void SpawnRandomFood()
            {
                Random rand = new Random();

                while (true)
                {
                    int ranX = rand.Next(0, X);
                    int ranY = rand.Next(0, Y);

                    if (tiles[ranY][ranX].objType == ObjectType.BLANK)
                    {
                        tiles[ranY][ranX].objType = ObjectType.FOOD;
                        break;
                    }
                }
            }

            public bool IsFoodExist()
            {
                for (int i = 0; i < Y; i++)
                {
                    for (int j = 0; j < X; j++)
                    {
                        if (tiles[i][j].objType == ObjectType.FOOD)
                        {
                            return true;
                        }
                    }
                }

                return false;
            }
        }

        class SnakeHead : Object
        {
            List<Object> body = new List<Object>();

            public SnakeHead(Vector2 startPos) : base(startPos, new Vector2Int(1, 0), ObjectType.SNAKE_HEAD)
            {
                body.Add(this);
                for (int i = 1; i < 4; i++)
                {
                    body.Add(new Object(new Vector2(trans.position.X - i, trans.position.Y), 
                        trans.rotation, ObjectType.SNAKE_BODY));
                }
            }

            public bool TryMove(Map map, Vector2Int dir)
            {
                bool isCrashed = false;

                Vector2 lastBodyPosition = body[body.Count - 1].trans.position;

                // 머리 다음 부분을 제외한 몸통 이동
                for (int i = body.Count - 1; i > 1; i--)
                {
                    body[i].trans.rotation = body[i - 1].trans.rotation;

                    body[i].trans.position = 
                        new Vector2(body[i].trans.position.X + body[i].trans.rotation.X,
                        body[i].trans.position.Y + body[i].trans.rotation.Y);

                    map.tiles[body[i].trans.position.Y][body[i].trans.position.X].objType = 
                        body[i].objType;
                }

                body[1].trans.rotation = trans.rotation;
                body[1].trans.position =
                    new Vector2(body[1].trans.position.X + body[1].trans.rotation.X,
                    body[1].trans.position.Y + body[1].trans.rotation.Y);

                map.tiles[body[1].trans.position.Y][body[1].trans.position.X].objType = 
                    body[1].objType;

                trans.rotation = dir;
                trans.position.X += trans.rotation.X;
                trans.position.Y += trans.rotation.Y;

                if (map.tiles[trans.position.Y + dir.Y][trans.position.X + dir.X].objType == 
                    ObjectType.WALL)
                {
                    isCrashed = true;
                }
                else if (map.tiles[trans.position.Y + dir.Y][trans.position.X + dir.X].objType ==
                    ObjectType.SNAKE_BODY)
                {
                    isCrashed = true;
                }
                else if (map.tiles[trans.position.Y + dir.Y][trans.position.X + dir.X].objType ==
                    ObjectType.FOOD)
                {
                    Object tail = body[body.Count - 1];
                    body.Add(new Object(new Vector2(tail.trans.position.X - tail.trans.rotation.X, tail.trans.position.Y - tail.trans.rotation.Y),
                        body[body.Count - 1].trans.rotation, ObjectType.SNAKE_BODY));
                    lastBodyPosition = body[body.Count - 1].trans.position;
                }

                map.tiles[trans.position.Y][trans.position.X].objType = objType;

                map.tiles[lastBodyPosition.Y][lastBodyPosition.X].objType = ObjectType.BLANK;

                return isCrashed;
            }
        }
    }
}

블랙잭

딜러와 겨루는 블랙잭 게임을 구현했습니다.

나의 조건 : 21이 목표, 21을 넘어가면 짐, 1~13의 카드뭉치 4개 => 52장

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class 블랙잭
    {
        static int cardCount = 3;
        static int targetSum = 21;

        static void Main(string[] args)
        {
            List<int> cardSet = new List<int>();
            int[] cards = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
            for (int i = 0; i < 4; i++)
            {
                cardSet.AddRange(cards);
            }

            // 1 ~ 13
            List<int> playerCards = new List<int>();
            int playerSum = 0;

            List<int> dealerCards = new List<int>();
            int dealerSum = 0;

            Random rand = new Random();

            for (int i = 0; i < cardCount; i++)
            {
                int randomIndex = rand.Next(0, cardSet.Count);
                playerCards.Add(cardSet[randomIndex]);
                cardSet.RemoveAt(randomIndex);
                WriteCards(playerCards);

                randomIndex = rand.Next(0, cardSet.Count);
                dealerCards.Add(cardSet[randomIndex]);
                cardSet.RemoveAt(randomIndex);
                WriteCards(dealerCards);

                playerSum = CalculateSum(playerCards);
                dealerSum = CalculateSum(dealerCards);

                if (playerSum > targetSum && dealerSum > targetSum)
                {
                    Console.WriteLine("둘 다 합이 목표 숫자를 넘었습니다. 무승부입니다.");
                    break;
                }
                else if (dealerSum > targetSum)
                {
                    Console.WriteLine("플레이어의 승입니다.");
                    break;
                }
                else if (playerSum > targetSum)
                {
                    Console.WriteLine("딜러의 승입니다.");
                    break;
                }

                if (i == cardCount - 1)
                {
                    if (playerSum == dealerSum)
                    {
                        Console.WriteLine("합이 같습니다. 무승부입니다.");
                    }
                    else if (playerSum > dealerSum)
                    {
                        Console.WriteLine("플레이어의 승입니다.");
                    }
                    else
                    {
                        Console.WriteLine("딜러의 승입니다.");
                    }
                }
            }
        }

        private static void WriteCards(List<int> cards)
        {
            foreach (var card in cards)
            {
                Console.Write($"{card} ");
            }
            Console.WriteLine();
        }

        private static int CalculateSum(List<int> cards)
        {
            int sum = 0;
            foreach (var card in cards)
            {
                sum += card;
            }

            return sum;
        }
    }
}

 

오늘의 회고

 오늘은 강의 듣느랴 기능 구현 해보느랴 코드 이해하느랴 정신이 없었다. 하지만 하나하나 문제가 풀려가는 것을 보고 꽤나 보람을 느낀 하루였던 것 같다. 이렇게만 꾸준히 노력하면 나도 언젠가 코딩 고수가 되지 않을까 하는 기대를 품으며 내일도 달려보도록 하겠다.

 내일은 알고리즘 강의 듣는 것이 목표인데 먼저 알고리즘 분야 별로 접근법이나 빅오 표기법을 위주로 공부하도록 해야겠다. 내일도 파이팅!

 

 

참고 :

https://potatopotatopotato.tistory.com/15

 

C# 콘솔로 스네이크 게임 찍어내기

숙제라서 만들었다 만들고 보니까 껌뻑껌뻑 거리는데 예전에 WINAPI 할때 더블 버퍼링 생각이 났다 어렵진 않았는데 시간 박치기 구현 노가다라서 피곤했다.. 저녁까지 밖에 나갔다 왔는데 집에

potatopotatopotato.tistory.com

 

한 주 회고

깃 데스크탑만 쓰다가 깃 배쉬도 써보고

카드 게임 리소스를 위해 팀원들과 사진 공유도 해보고

연출적인 부분을 맡아 여러가지 시도를 해보았다. 씬 페이딩이나 원의 방정식을 이용한 카드 세팅이라던지 처음엔 이해하기 어려웠지만 구현하고 나서는 다음에 또 써먹어야지 하는 생각이 들었다.

ppt 작업은 정민님이 해주셔서 부담감 없이 발표를 할 수 있게 되었다. 압도적 감사!

 

다음 주 목표

C# 과제들이 많던데 최대한 이해하면서 구현하는 방향으로 진행해야겠다. C# 과제 구현이 끝난다면 개인 공부(최적화, 쉐이더, 게임 수학)도 간간히 진행할 생각이다.  

오늘의 학습 키워드

문자열

 

공부한 내용

문자열 형식 지정

string.format(string format, string args[]);

문자열 대소 비교

string str1 = "Apple";
string str2 = "Banana";
// compare < 0 일 땐 str1 < str2, compare == 0 일 땐 str1 == str2, compare > 0 일 땐 str1 > str2
// A가 B보다 작으므로 음수 나옴
int compare = string.Compare(str1, str2);

문자열 갱신

boardUI가 갱신이 되지 않는 버그 수정

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class 틱택토_게임
    {
        static void Main(string[] args)
        {
            int[] board = new int[9];
            int player = 1;
            int enemy = 2;
            Random random = new Random();

            string boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";

            Console.WriteLine(boardUI);
            int user = 0;
            bool result = false;

            while (true)
            {
                int tempNum = -1;
                tempNum = ChooseNum(board);
                // 1 : 012 036 048
                // 2 : 012 147
                // 3 : 012 258 246
                // 4 : 345 036
                // 5 : 345 147 048 246
                // 6 : 345 258
                // 7 : 678 036 246
                // 8 : 678 147
                // 9 : 678 258 048

                // 012 345 678 036 147 258 048 246
                board[tempNum] = player;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = player;
                    break;
                }

                tempNum = SelectRandomNum(random, board);
                board[tempNum] = enemy;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = enemy;
                    break;
                }
            }

            WriteResult(user);
        }
    }
}

바보 같이 string을 갱신해주지 않아서 생긴 문제였다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace sparta
{
    class 틱택토_게임
    {
        static void Main(string[] args)
        {
            int[] board = new int[9];
            int player = 1;
            int enemy = 2;
            Random random = new Random();

            string boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";

            Console.WriteLine(boardUI);
            int user = 0;
            bool result = false;

            while (true)
            {
                int tempNum = -1;
                tempNum = ChooseNum(board);
                // 1 : 012 036 048
                // 2 : 012 147
                // 3 : 012 258 246
                // 4 : 345 036
                // 5 : 345 147 048 246
                // 6 : 345 258
                // 7 : 678 036 246
                // 8 : 678 147
                // 9 : 678 258 048

                // 012 345 678 036 147 258 048 246
                board[tempNum] = player;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = player;
                    break;
                }

                tempNum = SelectRandomNum(random, board);
                board[tempNum] = enemy;
                boardUI = $"-------\n-{board[0]}-{board[1]}-{board[2]}-\n-{board[3]}-{board[4]}-{board[5]}-\n-{board[6]}-{board[7]}-{board[8]}-\n-------";
                Console.WriteLine(boardUI);
                result = CheckBingo(board, tempNum);

                if (result)
                {
                    user = enemy;
                    break;
                }
            }

            WriteResult(user);
        }
    }
}

 

오늘의 회고

 오늘은 12시간 중 10~11시간 정도는 몰입한 것 같다. 구현이 오래 걸리는 문제나 내가 모르는 부분이 있어 정보를 찾는데 시간이 오래 걸렸는데 그 과정이 순탄치는 않았지만 새로 알게된 내용이 많아 재밌었던 것 같다.

 내일은 과제로 있는 스네이크 게임과 블랙잭 구현을 마무리 할 것이다.

FPS vs Frame Time

FPS : 1초당 몇 프레임을 렌더링 하는지

- FPS = 1000/ms

Frame Time : 한 프레임을 처리하는데 걸리는 시간

- 일반적으로 1/1000단위로 측정하고 ms(밀리 세컨드)로 표기한다.

- ms = 1000 / FPS

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DisplayFPS : MonoBehaviour
{
    [SerializeField]
    Text text;

    float frames = 0f;
    float timeElap = 0f;
    float frameTime = 0;

    void Start()
    {
        
    }

    void Update()
    {
        frames++;
        timeElap += Time.unscaledDeltaTime;
        // 1초가 넘었을 때 FPS 갱신하고 timeElap 초기화
        // FPS는 1초에 몇 프레임을 그리는가
        // FPS = ms / 1000
        // frameTime은 1프레임 당 몇 초가 걸리는가
        // frameTime = 1000 / FPS
        if (timeElap > 1f)
        {
            frameTime = timeElap / (float)frames;
            timeElap -= 1f;
            UpdateText();
            frames = 0;
        }
    }

    private void UpdateText()
    {
        text.text = string.Format("FPS = {0} frameTime = {1}", frames, frameTime * 1000.0f);
    }
}

 

왜 Frame Time으로 프로파일링 해야하는가?

1. 결과가 아니라 각 렌더링 순간의 지표가 필요

a구간이 1ms, b구간이 2ms, c구간이 10ms 가 걸린다고 하면 c구간을 최적화 해야 좋을 것이다.FPS는 1초의 평균 값이기 때문에 한 프레임을 렌더링 할 때 걸리는 시간인 Frame Time을 중요시 해야한다.

 

2. FPS는 비선형적 지표이다.ex1)오브젝트가 추가 될 때 성능 변화량이 90FPS, 45FPS, 30FPS일 때 비선형적으로 떨어지는 것으로 보이지만 계산해보면 - 1개 : 1000/90.0 = 11.1ms- 2개 : 1000/45.0 = 22.2ms- 3개 : 1000/30.0 = 33.3ms이처럼 선형적으로 떨어지는 것이다.ex2)씬에서 보면 더 큰 차이가 있다.A씬 : 900FPS -> 450FPS900FPS = 1000 / 900 => 1.1ms450FPS = 1000 / 450 => 2.2msB씬 : 60FPS -> 56.5FPS60FPS = 1000 / 60 => 16.6ms56.5FPS = 1000 / 56.5 => 17.7ms

두 씬에서 모두 동일하게 1.1ms의 영향을 주고 있음을 보인다.

프로파일링 할 때는 FPS보다 Frame Time으로 프로파일링을 하는 것이 좋다.

 

데이터를 측정할 때는 여러 번 측정해 평균 값을 데이터로 사용하자.

매번 잴 때마다 값이 조금씩 다를 수 있기 때문이다.

또한 각 시나리오마다 다를 수 있는데 평균적으로 NPC가 몇 없는 마을보다 몬스터가 많이 나오는 사냥터가 FPS가 더 낮게 나오겠지만 NPC의 모델의 텍스처가 매우 크다면 마을이 더 낮을 수도 있다.

ex1)

아래의 Rendering Path가 Deffered로 되어있다면 기본적으로 대역폭에 요구되는 성능 비용이 커서 FPS가 낮게 나올 수 있다.

ex2) 

포스트 프로세싱 등 후처리 효과도 비용이 크다.

 

Target Frame Rate

모바일 디바이스에서 쓰로틀링 상태로 진입하는 상태를 막기 위해 강제로 최고 FPS를 낮추는 방법이다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TargetFrameRate : MonoBehaviour
{
    void Start()
    {
        Application.targetFrameRate = 40;
    }
}

프로파일링 할 때는 FPS가 60까지 충분히 나오는지 확인하기 위해서 Application.TargetFrameRate를 60으로 설정하는 것이 좋다.

60프레임으로 표현할 수 있을 만큼 성능이 충분함에도 불구하고 40으로 제한을 두는 것은 그만큼 프로세서가 쉴 수 있는 시간을 충분히 확보해 둔다는 의미이다.

 

* Target Device : 갤럭시 노트 8

WaitForTargetFPS는 디바이스가 작업 후 쉴 시간이다. 쓰로틀링 방지를 위해 여유분이 있으면 좋다.

Target Frame = 30으로 설정 시 WaitForTargetFPS 10ms정도로 넉넉히 돌아간다.

Target Frame = 60으로 설정 후 시간이 좀 지났을 때 프로파일링 시  WaitForTargetFPS가 간신히 적용되는 것을 볼 수 있었다.

추가로 모바일 기기에서 TargetFrame = 40; 이 동작하지 않는 경우가 있었는데 이는 60Frames를 integer 값으로 나눌 수 있는 값이어야 한다고 한다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TargetFrameRate : MonoBehaviour
{
    void Start()
    {
        QualitySettings.vSyncCount = 0;
        Application.targetFrameRate = 40;
    }
}

 

VSync(수직 동기화)

targetFrameRate를 높게 설정해도 프레임이 60FPS를 넘지 않을 때 VSync를 의심해보자.

VSync(Vertical Synchronization = 수직동기화) : 모니터에서 렌더링 할 때 더블 버퍼링을 사용하는데 이 때 모니터의 주파수에 맞게 렌더링 퍼포먼스를 조절하는 것

더블 버퍼링 : 백 버퍼와 프론트 버퍼를 사용해서 다음에 출력될 화면을 백 버퍼에 집어 넣고 화면이 갱신될 때 프론트 버퍼와 백 버퍼를 바꿔치게 해 백 버퍼의 내용을 보여 줌으로써 자연스러운 화면 전환이 일어나게끔 하는 것

티어링 : 더블 버퍼링으로 각 버퍼 간의 전환이 이루어질 때 화면이 섞여 출력되어서 화면이 찢어지는 것처럼 보이는 것

 

=> 성능 및 병목을 측정할 떄는 VSync를 끄는 것이 좋다.

Vsync 끄기 : Edit - Project Settings - Quality - Don't Sync

퀄리티 세팅

런타임 중에 설정된 레벨을 변경하려면 QualitySettings.SetQualityLevel 메소드를 통해 현재 적용되어 있는 레벨을 변경할 수 있다.

 

쓰로틀링 고려하기

- 타깃 디바이스가 얼마나 빠르게 쓰로틀링 상태로 진입하고 어느 정도의 성능 하락이 일어나는지 등을 먼저 체크한다.

- 측정 중인 모바일 디바이스를 노트북이나 데스크톱에서 최대한 멀리 떨어뜨려놓는다.

- 항상 기기가 100% 충전된 상태에서 측정한다.

- 측정 후 다음 측정까지 5분 정도의 쿨타임을 가지고 진행한다.

- 스마트폰 전용 쿨링팬이나 아이스팩으로 쓰로틀링 상태 진입을 억제해 줄 수도 있다.

- 최신 기종일수록 성능이 좋아 쓰로틀링 상태에 더 빨리 진입하고 그 격차가 크다.

- 기기마다 쓰로틀링 단계가 다르다. 성능 하락이 한 단계가 아니라 시간이 지남에 따라 여러 단계로 성능 하락이 일어나기도 한다.

 

참고 :

https://www.yes24.com/Product/Goods/67305196

 

유니티 그래픽스 최적화 스타트업 - 예스24

게임개발의 최대 난적, 그래픽스 최적화를 다루는 책이 책은 게임개발의 최대 난적이라 할 수 있는 게임개발의 최적화에 대해서 다루는 책이다. 특히 유니티 엔진을 기반으로 게임을 가볍게 만

www.yes24.com

https://forum.unity.com/threads/application-targetframerate-not-working.169299/

 

Application.targetFrameRate not working

Hi guys been doing on this issue for hours now, Application.targetFrameRate is not working no matter what I do. I've tried changing it to 20, 25,...

forum.unity.com

 

'유니티 그래픽스 최적화 스타트업' 카테고리의 다른 글

드로우 콜과 배칭  (1) 2023.12.20

오늘 한 것

오늘은 4일동안 진행한 미니 프로젝트를 발표하는 시간을 가졌다.

다른 사람들의 작업물들도 보게 되었는데 기발한 생각들도 많고 잘 구현한 것 같았다.

간단한 프로젝트였지만 까먹었던 내용도 있고 새로 배웠던 내용들도 많아서 재밌었던 것 같다.

또한 팀원들도 좋은 사람들이라 재미있게 개발할 수 있었다.

https://github.com/j-miiin/WhereIsMyTeam

 

GitHub - j-miiin/WhereIsMyTeam: 뿔뿔이 흩어진 팀원을 모아서 팀을 완성시켜라!

뿔뿔이 흩어진 팀원을 모아서 팀을 완성시켜라! Contribute to j-miiin/WhereIsMyTeam development by creating an account on GitHub.

github.com

https://www.youtube.com/watch?v=Om0pOJaK8OU 

 

배운 것들

깃 허브 데스크탑

Git 작업을 GUI상에서 할 수 있는 깃 데스크탑 사용법을 배웠다.

Readme 파일에 이미지 업로드하기

깃 허브의 Issues - New Issue의 Write창에 이미지를 드래그 드랍을 해서 url을 얻어온 뒤 readme 파일에 복사하면 된다.

#if를 통한 전처리

전처리를 통해 안드로이드와 데스크탑 분기를 나눌 수 있다.

void Update()
{
// 안드로이드
#if UNITY_ANDROID
        if (Input.touchCount > 0)
        {
            touch = Input.GetTouch(0);
            if (touch.phase == TouchPhase.Began)
            {
                TouchParticle particle = ObjectPooler.I.touchPool.GetObject();
                Vector3 touchPos = mainCam.ScreenToWorldPoint(touch.position);
                touchPos.z = 0;
                particle.transform.position = touchPos;
            }
        }
#endif
// 데스크탑
#if UNITY_STANDALONE
        if (Input.GetMouseButtonDown(0))
        {
            TouchParticle particle = ObjectPooler.I.touchPool.GetObject();
            Vector3 mousePos = mainCam.ScreenToWorldPoint(Input.mousePosition);
            mousePos.z = 0;
            particle.transform.position = mousePos;
        }
#endif

    }

 

 

참고 : 

https://worthpreading.tistory.com/83

 

Github Readme에 이미지 올리기

약간의 트릭을 이용해 편리하게 Github Readme에 이미지를 삽입할 수 있다. 1. Github 프로젝트의 Issue 탭으로 들어간다. 2. New Issue 버튼을 눌러 이슈 추가 화면으로 들어간다. 3. 업로드하고자 하는 이

worthpreading.tistory.com

 

+ Recent posts