Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

기억 저장소

콘솔 화면에 움직이는 애니메이션 그리기(2차원)(Ascii Art) 본문

Voxel Engine 제작루틴/c#프로그래밍

콘솔 화면에 움직이는 애니메이션 그리기(2차원)(Ascii Art)

NaTure77 2021. 9. 7. 17:56

일반적으로 코딩 입문 시기 때 많이 하는 것이 '별찍기' 이다.

별 찍기란 간단히 말해서

출력문과 알고리즘으로 콘솔 화면에 일종의 그림을 그리는 것이다.

뭐 대충 이런 식으로..

 

비슷하게 화면을 그리는 코드를 만들어 보면,

using System;
class Viewer
{
	const int VIEW_SIZE = 40;
	public static void Main(String[] args)
	{
		new Viewer();
	}
	
	public Viewer()
	{
		int center = VIEW_SIZE / 2;
		//화면 출력
		Console.SetCursorPosition(0,0); // 출력 시작 위치를 0,0으로.
		for(int i = 0; i < VIEW_SIZE; i++)
		{
			for(int j = 0; j < VIEW_SIZE; j++)
			{
 				//범위 10 ~ 30 이내, 즉 길이 20인 정사각형 그리기
				if(Math.Abs(i - center) < 10 && Math.Abs(j - center) < 10)
					Console.Write("*");
 				else 
					Console.Write(" ");
			}
			Console.WriteLine();
		}
		
		Console.ReadLine();
	}
}

(ㅋㅋㅋㅋ 조금 다르긴 하지만 설명하기엔 충분하다.)

 


이때 콘솔 화면에 계산하면서 화면에 글자 하나씩 출력하기보다는

2차원 배열에 데이터를 저장해 뒀다가 한 번에 출력하는 것이 좋다.

이유는 코드 관리가 깔끔해지기도 하고,

콘솔 창에 출력 시 속도도 훨씬 빠르다.(중요)

 

해당 내용을 반영해서 고친 Viewer 생성자 코드. 출력 결과물은 동일하다.

public Viewer()
{
	display = new char[VIEW_SIZE,VIEW_SIZE];
	
	//디스플레이 초기화
	for(int i = 0; i < VIEW_SIZE; i++)
	for(int j = 0; j < VIEW_SIZE; j++)
		display[i,j] = ' ';
	
	int center = VIEW_SIZE / 2;
	int square_start = center - 10;
	int square_end = center + 10;
	
	//정사각형 그리기
	for(int i = square_start; i < square_end; i++)
	for(int j = square_start; j < square_end; j++)
	{
		display[i,j] = '*';
	}
	
	//화면 출력
	StringBuilder sb = new StringBuilder();
	for(int i = 0; i < VIEW_SIZE; i++)
	{
		for(int j = 0; j < VIEW_SIZE; j++)
		{
			sb.Append(display[i,j]);
		}
		sb.Append('\n');
	}
	Console.SetCursorPosition(0,0);
	Console.WriteLine(sb.ToString());
	
	Console.ReadLine();
}

 


그렇다면 포스트 제목처럼 움직이는 애니메이션이 되려면 어떻게 해야 하는가??

바로 콘솔 화면을 계속 다른 모양으로 업데이트해 주면 된다.

 

위에서 말했듯이 2차원 배열이 있다고 가정하면,

화면을 업데이트할 때마다

<2차원 배열 초기화 → 배열에 별 찍기. → 화면에 출력.>

이 과정을 계속 반복하면 된다.

 

배열에 별을 찍을 때 이전 화면과는 다른 모양을 보여 줘야 움직이는 애니메이션이 된다.

이전 포스트에서 소개했던 회전 변환을 활용하면 회전하는 애니메이션을 만들 수 있다.

https://nature77s.tistory.com/2

 

주의해야 할 점은, 화면에 출력되는 배열 인덱스는 정수 값이고, 회전 변환 후의 좌표는 실수 값이다.

이 좌표를 화면에 뿌리기 위해서는 내림 연산하여 정수로 만들어야 하므로 향상 오차가 생기게 되는데,

기존 회전이 된 배열에 계속 조금씩 회전을 시킬 경우 그 오차가 점점 쌓여서 모양이 뭉개져 버리게 된다.

 

그래서 원본 배열과 회전된 배열을 따로 두고,

매 프레임마다 degree 변수를 1도씩 증가시켜서

원본 배열을 degree만큼 회전시킨 결과를 회전된 배열에 저장 후

회전된 배열을 출력하는 식으로 구현해 보았다.

(아래 Vector2 클래스는 회전 변환에 쓰임)

using System;
using System.Text;
using System.Threading;

class Viewer
{
	const int VIEW_SIZE = 40;
	char[,] display;
	char[,] display_rotated;
	public static void Main(String[] args)
	{
		Console.CursorVisible = false;
		new Viewer();
	}
	public Viewer()
	{
		display = new char[VIEW_SIZE,VIEW_SIZE];
		display_rotated = new char[VIEW_SIZE,VIEW_SIZE];
		
		//원본 배열 초기화
		for(int i = 0; i < VIEW_SIZE; i++)
		for(int j = 0; j < VIEW_SIZE; j++)
			display[i,j] = ' ';
	
		int center = VIEW_SIZE / 2;
		int square_start = center - 10;
		int square_end = center + 10;
		
		//정사각형 그리기
		for(int i = square_start; i < square_end; i++)
		for(int j = square_start; j < square_end; j++)
			display[i,j] = '*';
		
		//회전에 필요한 변수들
		Vector2 pivot = new Vector2(center,center);
		Vector2 p = new Vector2();
		int degree = 0;
		
		while(true)
		{
			ResetDisplay();//회전 배열 초기화
			degree++; //프레임마다 1도씩 증가
            
			//회전 배열에 별 찍기
			for(int i = 0; i < VIEW_SIZE; i++)
			for(int j = 0; j < VIEW_SIZE; j++)
			{
				p.x = i; p.y = j;
				Vector2 r = Rotate2D(p,pivot,degree);//회전
				if(OutofRange(r)) continue;//회전된 좌표 배열 범위 검사
				display_rotated[(int)r.x,(int)r.y] = display[i,j];//별 찍기
			}
			Print();//화면 출력
			Thread.Sleep(10);//프레임 딜레이.
		}
	}
	
	bool OutofRange(Vector2 v)
	{
		return v.x < 0 || v.y < 0 || v.x >= VIEW_SIZE || v.y >= VIEW_SIZE;
	}
	void ResetDisplay()
	{
		for(int i = 0; i < VIEW_SIZE; i++)
		for(int j = 0; j < VIEW_SIZE; j++)
			display_rotated[i,j] = ' ';
	}
	
	StringBuilder sb = new StringBuilder();
	void Print()
	{
		sb.Clear();
		for(int i = 0; i < VIEW_SIZE; i++)
		{
			for(int j = 0; j < VIEW_SIZE; j++)
			{
				sb.Append(display_rotated[i,j]);
			}
			sb.Append('\n');
		}
		Console.SetCursorPosition(0,0);
		Console.WriteLine(sb.ToString());
	}
	
    //회전행렬 함수
	Vector2 Rotate2D(Vector2 p, Vector2 pivot, float degree)
	{
		float radian = (float)(degree * Math.PI / 180); //삼각함수 쓸 때 라디안 값으로 변환.
		p -= pivot; // 중심을 원점으로 이동
		float x = (float)(p.x * Math.Cos(radian) - p.y * Math.Sin(radian));
		float y = (float)(p.x * Math.Sin(radian) + p.y * Math.Cos(radian));
		return new Vector2(x, y) + pivot; // 중심을 pivot으로 복구
	}
}
//회전 행렬에 필요한 Vector 클래스
class Vector2
{
	public float x, y;
	public Vector2()
	{
		this.x = 0;
		this.y = 0;
	}
	public Vector2(float x, float y)
	{
		this.x = x;
		this.y = y;
	}
	public static Vector2 operator +(Vector2 a, Vector2 b)
	{
		float x = a.x + b.x;
		float y = a.y + b.y;
		return new Vector2(x,y);
	}
	public static Vector2 operator -(Vector2 a, Vector2 b)
	{
		float x = a.x - b.x;
		float y = a.y - b.y;
		return new Vector2(x,y);
	}
}

 

코드 컴파일 후 실행하면 다음과 같이 보인다.

컴파일 방법: https://nature77s.tistory.com/3

 

Comments