|
http://www.hackerschool.org/HS_Boards/zboard.php?AllArticle=true&no=8011 [복사]
14-1-다.시분할
동시에 여러 개의 신호를 이동시키는 루프를 어떻게 설계할 수 있을지 생각해보고 가능한 여러 가지 방법들을 비교해 보자. 세 개의 신호 M1, M2, M3를 동시에 움직이고 싶을 때 다음과 같은 루프를 작성할 수 있다.
for (;;) {
M1 이동
M2 이동
M3 이동
}
매 루프를 돌 때마다 M1, M2, M3를 한 칸씩 이동시키는 루프인데 이렇게 하면 세 개의 신호가 동시에 이동하기는 하지만 속도가 일정할 것이다. 속도의 차이를 주려면 각 신호가 한 번에 얼마만큼 이동할 것인지를 지정하는 값이 있어야 한다. 이 값을 speed 멤버로 정의하고 각각 1,2,3으로 준다면 M1이 한 칸 움직일 때 M2는 두 칸, M3는 세 칸 움직여 속도의 차이가 발생할 것이다.
그러나 이렇게 되면 속도가 빠른 신호는 한 번에 여러 칸을 움직이기 때문에 매끄러워 보이지 않는다. 그래서 이동 속도를 정수로 줄 수는 없으며 실수를 사용해야 한다. 1 미만의 실수로 이동 속도를 정의하면 한 번에 두 칸씩 이동하지 않으면서도 부드럽게 이동 처리된다. 하지만 실수를 쓰게 되면 속도가 느려지는 큰 문제점이 있고 또한 실제로 이동하지 않는 신호에 대해서도 지속적으로 좌표를 갱신하고 출력까지 해야 한다는 문제가 있다. 좌표가 바뀌지도 않았는데 지웠다 그림으로써 속도는 더 느려지고 화면은 계속 깜박거려 품질도 떨어진다.
좀 더 좋은 방법은 속도를 이동 거리로 통제하는 것이 아니라 주기로 통제하는 방식이다. 이 개념을 시분할(Time Sharing) 방식이라고 하는데 시간을 아주 잘게 조각내어 시간을 필요로 하는 대상에게 나누어 주는 것이다. 운영체제가 멀티 태스킹을 하는 기본 방식이 바로 시분할이다. 이때 분할된 시간을 시간 조각(Time Slice, 퀀텀이라고 한다.)이라고 한다. 각 신호가 몇 번의 시간 조각에 대해 한 번 움직일 것인가를 기억하는데 Signal 구조체에서 nFrame이 바로 이 정보이다. 가령 M1, M2, M3에 대해 nFrame이 3, 4, 6으로 정의되어 있다면 이 신호들은 다음과 같이 움직일 것이다.
이동 주기가 다름으로 해서 M1이 네 번 움직일 때 M2는 세 번밖에 움직이지 못하며 M3는 두 번만 움직일 것이다. nFrame이 클수록 신호의 이동 주기는 길어지고 이동 속도는 느려진다. 이동 주기는 정수이므로 느리지 않으며 또한 주기가 되지 않은 신호는 좌표를 갱신할 필요도 없고 다시 그릴 필요도 없어 실수를 쓰는 방법보다 훨씬 더 좋다.
main 함수는 몇 가지 간단한 초기화를 한 후 바로 무한 루프로 진입하는데 이 무한 루프의 전체 구조는 다음과 같이 되어 있다.
for (;;) {
....
delay(1000/frame)
}
이 루프의 제일 끝에 있는 delay 문이 시간을 일정 크기로 분할하는 역할을 한다. 만약 루프를 한 번 돌 때마다 0.1초씩 시간을 지연시킨다면 1초가 10개의 시간 조각으로 분할되므로 이 루프는 초당 10번 실행될 것이다. frame 변수가 200으로 초기화되었으므로 delay는 0.005초씩 시간을 지연시키며 초당 루프 반복 회수는 200이 된다. frame 변수는 지연 시간과 반비례 관계에 있으며 그래서 frame과 루프 반복 속도는 비례적이다.
이 루프는 시간 지연 함수인 delay로 시간을 조각내는 가장 기초적인 방법을 사용하는데 frame이 지정하는 회수만큼 정확하게 반복되지는 않는다. 왜냐하면 루프 내부에는 delay외에도 신호를 생성하고 이동시키는 다른 코드들이 있으며 이 코드들도 시간을 소모하기 때문에 약간의 오차가 있다. 시간 조각에서 이 코드들의 실행 시간을 뺀 만큼만 지연시키면 정확해질 것이며 정확한 시간 조각을 만드는 정교한 알고리즘들이 많이 개발되어 있다.
이 예제의 frame 변수는 루프의 속도를 조절하는 대충의 값이며 약간의 오차만 무시하면 거의 정확한 시간 조각을 생성해 낸다. 어쨌든 frame이 루프의 반복 속도와 비례적이므로 이 값을 조절함으로써 프로그램의 속도를 조정할 수 있다. 단, 지연 시간 계산에 정수 나눗셈을 사용했으므로 frame의 일정 구간별로 속도 차가 나는데 167~200까지는 모두 200프레임, 210~249까지는 250 프레임으로 계산될 것이다. 약간의 오차가 있기는 하지만 일반적으로 초당 n프레임을 얻고 싶으면 delay(1000/n) 지연문을 사용하면 된다.
Signal 구조체의 nFrame 멤버는 몇 개의 프레임에 대해 한 번 움직일 것인가를 지정하는데 난수로 5~24까지의 값을 가진다. 5가 가장 빨라서 초당 40번 움직이고 24가 가장 느려서 초당 8번 정도 움직인다. Signal 구조체에는 nFrame값을 카운트하는 nStay라는 별도의 멤버가 선언되어 있는데 이 값은 최초 nFrame과 같은 값으로 초기화된다. 매 프레임마다 1씩 감소하여 이 값이 0이 되면 신호를 한 칸 아래로 이동시키며 다시 nFrame값을 대입받는다. nFrame을 직접 카운트로 사용하면 한 번 이동한 후 원래 속도를 알 수 없어지므로 nStay라는 별도의 카운트가 필요하다. nFrame은 개체의 속성값이며 nStay는 런타임 데이터이다.
메인 루프의 신호 이동 코드를 보자. 일단 0~MAX까지 루프를 돌되 존재하지 않는 신호에 대해서는 관심을 가질 필요가 없다. exist가 FALSE인 신호는 즉시 continue하여 루프의 처음으로 돌려 보낸다. 존재하는 신호는 nStay를 1감소시키고 이 값이 0일 때만 이동 처리를 하는데 이전 위치의 신호를 지우고 y를 증가시킨 후 다시 출력하면 된다. 만약 한 칸 이동 후 distance만큼 내려 왔다면 이 신호는 수명이 다했으므로 삭제된다. nStay가 0이 되면 이동 처리뿐만 아니라 nStay를 다시 nFrame으로 초기화하여 다음 주기까지 카운트하도록 해야 한다.
main 함수에는 이 외에도 키 입력을 받아 프로그램의 속도인 frame을 증감시키는 코드와 신호의 발생 빈도인 freq를 증감시키는 코드가 작성되어 있는데 아주 쉽게 분석될 것이다. 시분할 개념은 게임처럼 동시에 여러 개의 물체가 움직이는 프로그램에서 반드시 필요하다. 움직임이 많고 정교하다면 프레임 수를 높여야 하고 그렇지 않다면 조금 낮아도 상관없다.
이 예제의 동작 과정을 이해하려면 약간의 상상력이 동원되어야 한다. 내부에서 각 개체들이 카운트를 어떻게 관리하며 언제 왜 이동하는지, 어떤 조건에 의해 생성, 파괴되는지를 머리속으로 잘 그려 봐야 한다. 그러나 아무래도 이런 과정이 시각적으로 보이지 않기 때문에 얼른 이해되지는 않을 것이다. 그렇다면 코드를 다음과 같이 수정하여 동작 과정을 슬로우 모션으로 확인해 보자.
// 주기가 다 된 신호 이동 및 제거 처리
for (i=0;i<MAX;i++) {
if (S[i].exist == FALSE)
continue;
gotoxy(S[i].x,S[i].y);puts(" ");
if (--S[i].nStay == 0) {
S[i].nStay=S[i].nFrame;
if (++S[i].y >= S[i].distance) {
S[i].exist=FALSE;
}
}
if (S[i].exist) {
gotoxy(S[i].x,S[i].y);
printf("%c-%d:%d ",S[i].ch,S[i].nFrame,S[i].nStay);
}
}
delay(100);
프레임 수를 초당 10회로 낮추어 느리게 동작하도록 했으며 각 신호의 nFrame과 nStay 정보를 글자 옆에 문자열로 출력해 보았다. 너무 많은 신호들이 동시에 나타나 결과를 보기 어렵다면 MAX는 3으로 잠시 낮춰 놓는 것이 좋고 10프레임이 너무 빠르다면 delay(1000)으로 수정하여 아주 천천히 중간 과정을 살펴볼 수도 있다.
============================================================================
#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <conio.h>
#define MAX 1024
#define LEFT 75
#define RIGHT 77
#define UP 72
#define DOWN 80
#define ESC 27
void gotoxy(int x, int y);
struct Signal
{
BOOL exist; // 신호의 존재 여부
char ch; // 출력할 문자
int x, y; // 현재 좌표
int distance; // 이동할 거리
int nFrame; // 속도
int nStay; // 속도에 대한 카운트
};
struct Signal S[MAX];
void main()
{
int i;
int ch;
int freq = 15;
int frame = 200;
//clrscr();
gotoxy(1, 23);//gotoxy(0,23);
printf("매트릭스 흉내내기. 상하:속도 증감, 좌우:빈도 증감, ESC:종료");
//randomize();
for (;;) {
gotoxy(60, 23);
printf("속도:%d, 빈도:%d ", frame, freq);
// 키 입력 처리
if (kbhit()) {
ch = getch();
if (ch == 0xE0) {
ch = getch();
switch (ch) {
case UP:
frame = min(frame + 10, 1000);
break;
case DOWN:
frame = max(frame - 10, 10);
break;
case RIGHT:
freq = max(freq - 1, 1);
break;
case LEFT:
freq = min(freq + 1, 30);
break;
}
}
else {
if (ch == ESC) {
return;
}
}
}
// 새로운 신호 생성
if (rand() % (freq + 1) == 0) //(random(freq) == 0)
{
for (i = 0; i < MAX; i++) {
if (S[i].exist == false) {
S[i].exist = TRUE;
S[i].ch = rand() % 26 + 65; //random('Z'-'A'+1)+'A';
S[i].x = rand() % 81; //random(80);
S[i].y = 0;
S[i].distance = rand() % 15 + 9; //random(14)+9;
S[i].nFrame = S[i].nStay = rand() % 16 + 5;//random(15)+5;
break;
}
}
// 주기가 다 된 신호 이동 및 제거 처리
for (i = 0; i < MAX; i++) {
if (S[i].exist == FALSE)
continue;
if (--S[i].nStay == 0) {
S[i].nStay = S[i].nFrame;
gotoxy(S[i].x, S[i].y); putch(' ');
if (++S[i].y < S[i].distance) {
gotoxy(S[i].x, S[i].y); putch(S[i].ch);
}
else {
S[i].exist = FALSE;
}
}
}
Sleep(1000 / frame);
}
}
}
void gotoxy(int x, int y)
{
COORD Pos = { x - 1, y - 1 };
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), Pos);
}
|
Hit : 9636 Date : 2014/02/15 05:35
|