C# 첫 게임! SpaceInvader

주환서·2026년 2월 22일
post-thumbnail

개요

콘솔 창을 기준으로 만들어본 SpaceInvader입니다. 플레이어는 좌우로만 움직일 수 있고 적 인베이더들을 총알로 전부 격추 시켜 클리어하는 게임입니다.

구현 과정

클래스들의 역할
1. 전체 게임의 흐름을 제어하는 Game, PlayScene
2. 게임 내에 객체들을 관리하는 World, InvaderManager
3. 게임 내 물체들의 개별 행동들을 관리하는 Entity, Player, Invader, Bullet
4. 키보드 입력과, 충돌 판정, 화면 출력 등 독립적으로 수행하는 기능들인 InputSystem, CollisionSystem, RendererSystem

구현 결과

// game.cs
using System.Diagnostics; // 시간을 재는 '스톱워치' 기능을 쓰려고 가져옴
using System.Threading;   // 잠깐 대기하는 'Sleep' 기능을 쓰려고 가져옴

namespace SpaceInvader
{
    public sealed class Game
    {
        // 실제 게임 로직(플레이어, 적, 화면 등)
        private PlayScene _scene = new PlayScene();

        // 프레임
        private const int TargetFps = 30;

        // 1프레임당 걸려야 하는 시간 계산. 1000ms(1초) 나누기 30 = 약 33ms
        private const int FrameMs = 1000 / TargetFps;

        public void Run()
        {
            // 시간을 잴 스톱워치 생성
            var stopWatch = new Stopwatch();

            _scene.Enter();

            // _scene에서 나가기까지 계속 반복
            // 게임이 돌아가는 메인 루프
            while (!_scene.IsExitRequest)
            {
                // 시간 측정 시작!
                stopWatch.Restart();

                // 1. 키 입력 받고, 캐릭터 위치 옮기고, 죽었나 살았나 체크하고
                _scene.Update();

                // 2. 화면 출력
                _scene.Render();

                // 3. 속도 조절
                // 이번 프레임을 처리하는 데 걸린 시간을 잰 다음, 33ms보다 빨리 끝났으면 남은 시간만큼 쉼.
                // 이걸 안 하면 컴퓨터 성능에 따라 게임 속도가 빨라짐.
                int remainMs = FrameMs - (int)stopWatch.ElapsedMilliseconds;
                if (remainMs > 0)
                {
                    Thread.Sleep(remainMs);
                }
            }

            // 게임 종료
            _scene.Exit();
        }
    }
}
// playscene.cs
using SpaceInvader.System;
using SpaceInvader.Util;
using SpaceInvader.World;
using System;

namespace SpaceInvader
{
    public sealed class PlayScene
    {
        private World.World _world = new World.World();          
        private InputSystem _input = new InputSystem();          // 키보드 입력 
        private CollisionSystem _collision = new CollisionSystem(); // 충돌 판정
        private ConsoleRenderer _renderer = new ConsoleRenderer();  // 화면 출력

        // 게임을 끄라는 신호인지 확인하는 변수
        public bool IsExitRequest { get; private set; } = false;

        // 게임 시작할 때 딱 한 번 실행되는 함수
        public void Enter()
        {
            _world.Initialize();     // 초기화 (플레이어 만들고, 적 배치하고)
            _renderer.Initialize();  // 초기화 (화면 깨끗이 지우기)
        }

        // 매 프레임마다 반복 실행되는 함수
        public void Update()
        {
            // 1. 키보드 입력을 받아옴
            InputFrame input = _input.ReadInput();

            // 만약 ESC키를 눌러서 종료 요청이 들어왔다면
            if (input.Exit)
            {
                IsExitRequest = true; // 종료
                return;
            }

            // 2. 입력을 전달 (플레이어 움직임 처리)
            _world.ApplyInput(input);

            // 3. 진행 (적들 움직이고, 총알 날아가고)
            _world.Update();

            // 4. 충돌 검사 (총알이 적을 맞췄는지 확인)
            _collision.CheckCollision(_world);

            // 5. 시체 치우기 (죽은 적, 화면 밖으로 나간 총알 삭제)
            _world.Cleanup();

            // 6. 게임 오버 됐는지 확인
            CheckGameOver();
        }

        // 게임 오버 조건을 검사하는 함수
        private void CheckGameOver()
        {
            // 조건 1: 인베이더가 바닥까지 내려왔거나 (_world.IsGameOver가 true)
            // 조건 2: 플레이어가 죽어서 삭제됐을 경우 (Player 변수가 null이 됨)
            if (_world.IsGameOver || _world.Player == null)
            {
                IsExitRequest = true; // 게임 루프를 끝내라고 신호 보냄

                // 화면을 지우고 GAME OVER 글자를 띄움
                Console.Clear();
                Console.SetCursorPosition(0, 0);
                Console.WriteLine("GAME OVER");
            }
        }

        // 화면 출력 함수
        public void Render()
        {
            // 게임 오버 상태가 아닐 때만
            if (!IsExitRequest)
            {
                _renderer.Draw(_world); // 현재 상태를 버퍼에 그림
                _renderer.Present();    // 실제 모니터에 보여줌
            }
        }

        // 게임 끝날 때 뒷정리하는 함수
        public void Exit()
        {
            _renderer.Shutdown();
        }
    }
}
//world.cs
using System.Collections.Generic;
using SpaceInvader.Util;

namespace SpaceInvader.World
{
    public sealed class World
    {
        // 게임에 존재하는 모든 물체를 담는 리스트
        private readonly List<Entity> _entities = new List<Entity>();

        // 추가/삭제할 물체를 잠깐 담아두는 임시 리스트
        // for문 돌면서 리스트를 직접 건드리면 에러가 나기 때문에, 나중에 한꺼번에 처리하려고
        private List<Entity> _toAdd = new List<Entity>();
        private List<Entity> _toRemove = new List<Entity>();

        private Player _player;
        private InvaderManager _invaderManager;

        // 게임 화면 크기\
        public int Width { get; private set; } = 80;
        public int Height { get; private set; } = 30;

        // 게임 오버 상태인지 확인하는 변수\
        public bool IsGameOver { get; set; } = false;

        public World()
        {
            // 월드가 만들어질 때 적 관리자도 같이 고용함
            _invaderManager = new InvaderManager(this);
        }

        // 물체를 추가 예약하는 함수
        public void Add(Entity entity)
        {
            _toAdd.Add(entity); // 대기열에 넣음
            // 만약 추가된 게 플레이어라면 따로 변수에 저장해둠 (나중에 찾기 쉽게)
            if (entity is Player) _player = (Player)entity;
        }

        // 물체를 삭제 예약하는 함수
        public void Remove(Entity entity)
        {
            _toRemove.Add(entity); // 대기열에 넣음
            // 플레이어가 삭제된다면 변수도 비워둠
            if (entity is Player) _player = null;
        }

        // 외부에서 _entities 리스트를 읽을 수 있게 해주는
        public List<Entity> Entities => _entities;
        public Player Player => _player;

        // 게임 시작 시 초기화
        public void Initialize()
        {
            // 기존에 있던 거 싹 비움
            _entities.Clear();
            _toAdd.Clear();
            _toRemove.Clear();
            _player = null;
            IsGameOver = false; // 게임오버 상태도 초기화

            // 플레이어를 화면 중앙 하단(Width / 2, Height - 2)에 생성해서 추가함
            Add(new Player(new Vec2Int(Width / 2, Height - 2)));

            // 적을 배치함
            _invaderManager.Initialize();
        }

        // 예약된 추가/삭제 작업을 실제로 수행하는 함수
        private void CommitList()
        {
            if (_toAdd.Count > 0)
            {
                _entities.AddRange(_toAdd); // 추가 대기열에 있던 걸 진짜 리스트에 합침
                _toAdd.Clear(); // 대기열 비움
            }

            if (_toRemove.Count > 0)
            {
                foreach (Entity e in _toRemove)
                {
                    _entities.Remove(e); // 삭제 대기열에 있던 걸 진짜 리스트에서 지움
                }
                _toRemove.Clear(); // 대기열 비움
            }
        }

        // 매 프레임 호출되는 업데이트 함수
        public void Update()
        {
            // 적 관리자에게 전달 (적 이동, 공격 시키기)
            _invaderManager.Update();

            // 물체 이동
            foreach (var e in _entities)
            {
                e.Update(this);
            }

            // 위에서 추가/삭제 예약된 게 있으면 실제 리스트에 반영
            CommitList();
        }

        // 죽은 물체들을 골라내서 삭제 예약하는 함수
        public void Cleanup()
        {
            foreach (var e in _entities)
            {
                if (e.IsAlive == false)
                {
                    Remove(e); // 죽었으면 삭제 리스트로 보냄
                }
            }
            CommitList(); // 실제 삭제 수행
        }

        // 키 입력을 받아서 처리하는 함수
        public void ApplyInput(InputFrame input)
        {
            if (Player == null) return; // 플레이어가 죽어서 없으면 아무것도 안 함

            // 왼쪽/오른쪽 키 눌렀으면 플레이어 이동
            if (input.Left) Player.RequestMove(-1);
            if (input.Right) Player.RequestMove(+1);

            // 발사 키 눌렀으면 플레이어가 총알을 쏨
            if (input.Fire)
            {
                var bullet = Player.Attack(); // 플레이어가 총알을 쏨
                if (bullet != null)
                {
                    Add(bullet); // 그 총알을 추가
                }
            }
        }
    }
}
// invadermanager.cs
using SpaceInvader.Util;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SpaceInvader.World
{
    public sealed class InvaderManager
    {
        private Random _random = new Random(); // 랜덤 뽑기
        private World _world;

        // 적 배치 설정값들
        private int _row = 20; // 가로로 몇 마리
        private int _col = 3;  // 세로로 몇 줄
        private int _rowSpacing = 3; // 가로 간격
        private int _colSpacing = 2; // 세로 간격
        private int _rowStart = 10;  // 시작 위치 X
        private int _colStart = 3;   // 시작 위치 Y

        // 현재 살아있는 적들 목록
        private List<Invader> _aliveInvaders = new List<Invader>();

        // 공격 쿨타임
        private int _attackInterval = 30; // 30프레임마다 한 번 공격 명령 약 1초인듯
        private int _attackCooldown = 0;

        public InvaderManager(World world)
        {
            _world = world;
        }

        public void Initialize()
        {
            SpawnFormation(); // 적들을 대형에 맞춰 생성
        }

        // 적 생성 함수
        public void SpawnFormation()
        {
            for (int j = 0; j < _col; j++) // 세로 줄 반복
            {
                for (int i = 0; i < _row; i++) // 가로 줄 반복
                {
                    // 위치 계산해서 인베이더 생성 후 월드에 추가
                    _world.Add(new Invader(new Vec2Int(_rowStart + i * _rowSpacing, _colStart + j * _colSpacing)));
                }
            }
        }

        // 월드에 있는 애들 중 살아있는 인베이더만 추려냄
        public void GetAliveInvaders()
        {
            _aliveInvaders.Clear();
            foreach (var entity in _world.Entities)
            {
                if (entity.IsAlive && entity is Invader inv)
                {
                    _aliveInvaders.Add(inv);
                }
            }
        }

        // 매 턴 행동
        public void Update()
        {
            GetAliveInvaders(); // 살아있는 애들 파악

            // 1. 단체 이동 관리 (벽 닿으면 내려가기 등)
            ProcessMovement();

            // 2. 공격 명령 (랜덤하게 총 쏘기)
            ProcessAttack();
        }

        private void ProcessMovement()
        {
            bool isHitWall = false;

            // 누구 하나라도 벽에 닿았는지 검사
            foreach (var inv in _aliveInvaders)
            {
                if (inv.IsHitWall(_world))
                {
                    isHitWall = true;
                    break;
                }
            }

            // 벽에 닿았다면? 전원 방향 반대로 바꾸고 한 칸 전진
            if (isHitWall)
            {
                foreach (var inv in _aliveInvaders)
                {
                    inv.DirX *= -1; // 방향 반전
                    inv.MoveToDown(); // 아래로 한 칸

                    // 게임 오버 체크
                    // 만약 내려왔는데 거기가 바닥이면
                    if (inv.Position.Y >= _world.Height - 2)
                    {
                        _world.IsGameOver = true; // 게임 오버
                    }
                }
            }
        }

        private void ProcessAttack()
        {
            if (_aliveInvaders.Count == 0) return; // 적이 다 죽었으면 공격 못함

            _attackCooldown--;
            if (_attackCooldown <= 0) // 공격할 타이밍이 되면
            {
                _attackCooldown = _attackInterval; // 쿨타임 리셋

                // 가장 아래쪽에 있는 애들 중 하나를 뽑음
                Invader shooter = GetRandomBottomInvader();
                if (shooter != null)
                {
                    // 걔한테 총알 받아냄
                    Bullet bullet = shooter.Attack();
                    // 그 총알을 월드에 등록 (그래야 날아감)
                    _world.Add(bullet);
                }
            }
        }

        // 맨 밑에 있는 녀석들 중에서 하나 랜덤으로 고르는 함수
        public Invader GetRandomBottomInvader()
        {
            // 1. X축끼리 그룹을 지음
            // 2. 각 그룹에서 Y값이 제일 큰 것만 뽑음
            var bottoms =
            _aliveInvaders.GroupBy(i => i.Position.X).Select(g => g.OrderByDescending(i => i.Position.Y).First()).ToList();

            if (bottoms.Count == 0)
                return null;

            // 추려낸 맨 앞줄 중 랜덤으로 한 명 선택
            return bottoms[_random.Next(bottoms.Count)];
        }
    }
}
// entitiy.cs
using SpaceInvader.Util;
using System;

namespace SpaceInvader.World
{
    // abstract class(추상 클래스): 
    public abstract class Entity
    {
        // 위치 정보 (X, Y 좌표)
        public Vec2Int Position { get; protected set; }
        // 살아있는지 여부 (true면 생존, false면 사망)
        public bool IsAlive { get; protected set; } = true;

        // abstract: 자식들이 무조건 자기만의 Shape을 정해야 함.
        public abstract char Shape { get; }

        // 생성자: 태어날 때 위치를 정해줌
        protected Entity(Vec2Int position)
        {
            Position = position;
        }

        // virtual: 자식들이 원하면 이 기능을 자기 입맛대로 바꿔서(override) 써도 됨
        // 기본적으로는 아무것도 안 함
        public virtual void Update(World world)
        {
        }

        public virtual void Move(World world)
        {
        }

        public virtual void OnHit(Entity other)
        {
        }
    }
}
// player.cs
using SpaceInvader.Util;

namespace SpaceInvader.World
{
    public sealed class Player : Entity
    {
        public override char Shape => 'A'; // 플레이어 모양은 'A'
        public int Hp { get; private set; }
        public int Damage { get; private set; }

        private int _pendingMove;       // 이동하려는 방향 (-1, 0, 1) 임시 저장
        private int _fireCooldownTicks; // 쿨타임 체크 변수
        private int _fireInterval = 6;  // 총 쏘고 나서 6프레임동안은 못 쏨

        // 생성자
        public Player(Vec2Int position) : base(position) { }

        // 입력을 받아서 저장해둠
        public void RequestMove(int dir)
        {
            _pendingMove = dir;
        }

        // 진짜 움직이는 함수
        public override void Move(World world)
        {
            if (_pendingMove != 0) // 움직일 계획이 있다면
            {
                int newX = Position.X + _pendingMove;

                // 화면 밖으로 못 나가게 가둠
                newX = Clamp(newX, 0, world.Width - 1);

                // 위치 확정
                Position = new Vec2Int(newX, Position.Y);
                _pendingMove = 0; // 이동했으니 계획 초기화
            }
        }

        public override void Update(World world)
        {
            // 총알 쿨타임 줄이기
            _fireCooldownTicks--;
            if (_fireCooldownTicks < 0)
            {
                _fireCooldownTicks = 0;
            }

            // 움직임 처리
            Move(world);
        }

        // 총 쏘는 함수
        public Bullet Attack()
        {
            // 쿨타임이 다 됐을 때만 발사 가능
            if (_fireCooldownTicks <= 0)
            {
                _fireCooldownTicks = _fireInterval; // 쿨타임 다시 채움

                // 플레이어 바로 위(Y-1)에서 생성
                var spawnPos = new Vec2Int(Position.X, Position.Y - 1);

                // 주인은 Player라고 하고 총알 생성
                return new Bullet(spawnPos, -1, BulletOwner.Player);
            }

            return null; // 쿨타임 중이면 총알 안 나감
        }

        // 숫자가 최소값과 최대값 사이를 벗어나지 않게 잡아주는 함수
        private int Clamp(int value, int min, int max)
        {
            if (value < min) return min;
            if (value > max) return max;
            return value;
        }

        // 누가 나를 때렸을 때 실행됨
        public override void OnHit(Entity other)
        {
            // 부딪힌 게 총알이고, 그 총알 주인이 인베이더라면
            if (other is Bullet b && b.BulletOwner == BulletOwner.Invader)
            {
                IsAlive = false; // 죽음
            }
        }
    }
}
// invader.cs
using SpaceInvader.Util;

namespace SpaceInvader.World
{
    public sealed class Invader : Entity
    {
        public override char Shape => 'W'; // 적 모양은 'W'
        public int DirX { get; set; } = +1; // 현재 이동 방향 (+1이면 오른쪽, -1이면 왼쪽)

        // 이동 속도 조절용 변수
        private int _moveInterval = 5; // 5프레임마다 한 번 움직임
        private int _moveCooldownTicks;

        public Invader(Vec2Int position) : base(position) { }

        // 총알을 만들어서 뱉어냄
        public Bullet Attack()
        {
            // 내 바로 아래(Y+1)에서 생성, 아래쪽(+1)으로 날아감, 주인은 Invader
            return new Bullet(new Vec2Int(Position.X, Position.Y + 1), 1, BulletOwner.Invader);
        }

        public override void Update(World world)
        {
            _moveCooldownTicks--;
            if (_moveCooldownTicks <= 0) // 움직일 시간이 되면
            {
                _moveCooldownTicks = _moveInterval; // 쿨타임 리셋하고
                Move(world); // 움직임
            }
        }

        public override void Move(World world)
        {
            MoveHorizontal(); // 기본적으론 옆으로만 감
        }

        // 벽에 부딪혔는지 검사하는 함수
        public bool IsHitWall(World world)
        {
            int nextX = Position.X + DirX;
            // 화면 왼쪽 끝보다 작거나, 오른쪽 끝보다 크면 벽에 닿은 거임
            return nextX < 0 || nextX >= world.Width;
        }

        // 옆으로 한 칸 이동
        private void MoveHorizontal()
        {
            Position = new Vec2Int(Position.X + DirX, Position.Y);
        }

        // 아래로 한 칸 이동 (벽에 닿았을 때 매니저가 시킴)
        public void MoveToDown()
        {
            Position = new Vec2Int(Position.X, Position.Y + 1);
        }

        // 맞았을 때
        public override void OnHit(Entity other)
        {
            // 플레이어의 총알에 맞았으면 사망
            if (other is Bullet b && b.BulletOwner == BulletOwner.Player)
            {
                IsAlive = false;
            }
        }
    }
}
// bullet.cs
using SpaceInvader.Util;

namespace SpaceInvader.World
{
    // 총알 주인이 누군지 표시하기 위한 이름표
    public enum BulletOwner
    {
        Player, Invader
    }

    public sealed class Bullet : Entity
    {
        public override char Shape => 'I'; // 총알 모양
        private readonly int _dirY = 1;    // 위로 갈지 아래로 갈지 방향
        public BulletOwner BulletOwner { get; private set; } // 누가 쐈는지

        // _moveTick: 이동하기 위해 기다리는 카운트
        // _moveInterval: 몇 번 기다렸다가 움직일지 정하는 값 (2면 2번 쉴 때 1번 움직임)
        private int _moveTick = 0;
        private int _moveInterval = 2; // 클수록 총알이 느려짐

        public Bullet(Vec2Int position, int dirY, BulletOwner bulletOwner) : base(position)
        {
            _dirY = dirY;
            BulletOwner = bulletOwner;
            // 다 똑같이 느리게 만듦
        }

        public override void Update(World world)
        {
            // 총알 속도 늦추기
            _moveTick--; // 카운트를 하나 깜
            if (_moveTick > 0) return; // 아직 움직일 때가 아니면 여기서 함수 끝냄

            _moveTick = _moveInterval; // 카운트 다시 충전

            // 현재 위치에서 Y방향으로 한 칸 이동
            Position = new Vec2Int(Position.X, Position.Y + _dirY);

            // 화면 위나 아래로 벗어나면
            if (Position.Y < 0 || Position.Y > world.Height)
            {
                IsAlive = false; // 죽은 걸로 처리 (Cleanup때 사라짐)
            }
        }

        public override void OnHit(Entity other)
        {
            IsAlive = false; // 누구랑 부딪히면 총알은 사라짐
        }
    }
}
// input.cs
using System;
using SpaceInvader.Util;

namespace SpaceInvader.System
{
    public sealed class InputSystem
    {
        // 키보드 입력을 읽어서 'InputFrame'이라는 박스에 담아 리턴함
        public Util.InputFrame ReadInput()
        {
            bool left = false, right = false, fire = false, exit = false;

            // 키보드 버퍼에 입력이 남아있는 동안 계속 읽음
            // (빠르게 여러 키를 눌렀을 때 씹히지 않게 하기 위함)
            while (Console.KeyAvailable)
            {
                var key = Console.ReadKey(true).Key; // 키를 읽음 (화면엔 안 보이게 true 옵션)
                switch (key)
                {
                    case ConsoleKey.A:
                    case ConsoleKey.LeftArrow:
                        left = true; // 왼쪽 키 
                        break;
                    case ConsoleKey.D:
                    case ConsoleKey.RightArrow:
                        right = true; // 오른쪽 키
                        break;
                    case ConsoleKey.Spacebar:
                        fire = true; // 스페이스바
                        break;
                    case ConsoleKey.Escape:
                        exit = true; // ESC 
                        break;
                }
            }

            // 확인된 키 상태를 포장해서 보냄
            return new InputFrame(left, right, fire, exit);
        }
    }
}
// collision.cs
namespace SpaceInvader.System
{
    public sealed class CollisionSystem
    {
        public void CheckCollision(World.World world)
        {
            var entities = world.Entities;

            // A랑 B랑 비교하고, A랑 C랑 비교
            for (int i = 0; i < entities.Count; i++)
            {
                for (int j = i + 1; j < entities.Count; j++)
                {
                    var a = entities[i];
                    var b = entities[j];

                    // 두 물체의 위치가 똑같다면? 충돌
                    if (a.Position.Equals(b.Position))
                    {
                        // 서로에게 맞았다고 알려줌
                        a.OnHit(b);
                        b.OnHit(a);
                    }
                }
            }
        }
    }
}
// renderer.cs
using SpaceInvader.World;
using System;
using System.Text;

namespace SpaceInvader.System
{
    public sealed class ConsoleRenderer
    {
        private int _width;
        private int _height;
        private char[,] _buffer;    // 2차원 배열

        // 화면을 한 번에 출력하기 위해 글자를 모아두는 긴 문자열 빌더
        private readonly StringBuilder _sb = new StringBuilder();

        public void Initialize()
        {
            Console.Clear(); // 콘솔창 깨끗하게 지움
        }

        public void Shutdown()
        {
        }

        public void Draw(World.World world)
        {
            _width = world.Width;
            _height = world.Height;

            // _buffer가 없거나 크기가 바뀌었으면 새로 만듦
            if (_buffer == null ||
                _buffer.GetLength(0) != _height ||
                _buffer.GetLength(1) != _width)
            {
                _buffer = new char[_height, _width];
            }

            // 1. 도화지 전체를 공백으로 칠해서 지움
            for (int y = 0; y < _height; y++)
            {
                for (int x = 0; x < _width; x++)
                {
                    _buffer[y, x] = ' ';
                }
            }

            // 2. 월드에 있는 모든 물체를 순서대로 그림
            foreach (var e in world.Entities)
            {
                if (e.IsAlive == false) continue; // 죽은 놈은 안 그림

                int x = e.Position.X;
                int y = e.Position.Y;

                // 화면 밖으로 나간 놈은 그리지 않음
                if (x < 0 || x >= _width ||
                    y < 0 || y >= _height) continue;

                // 해당 위치에 그 물체의 Shape을 찍음
                _buffer[y, x] = e.Shape;
            }
        }

        // 다 그린걸 모니터에 출력하는 함수
        public void Present()
        {
            if (_buffer == null) return;

            Console.SetCursorPosition(0, 0); // 커서를 맨 위로 보냄
            _sb.Clear(); // 문자열 빌더 비움

            // 도화지 내용을 문자열 빌더에 한 줄씩 옮겨 담음
            for (int y = 0; y < _height; y++)
            {
                for (int x = 0; x < _width; x++)
                {
                    _sb.Append(_buffer[y, x]);
                }
                _sb.AppendLine(); // 줄바꿈
            }

            // 한 방에 출력
            Console.Write(_sb.ToString());
        }
    }
}

문제 해결 과정

  1. 콘솔 느낌이 나게 하고 싶은데 어떻게 할까 고민하다가 Console.Clear()를 사용해서 화면이 지워지는 찰나에 깜빡임이 발생하게 구현 해보았습니다.
  2. 좌표 계산이 헷갈렸는데 그냥 시작점 + (순서 * 간격) 이처럼 숫자가 아니라 명칭으로 해보니까 쉽게 생각 할 수 있었습니다.

0개의 댓글