Урок 5
Главный цикл программы из четвёртого урока разросся, и стоит его сделать поаккуратнее. В главном цикле не нужно выполнять все вычисления непосредственно, главный цикл занимается тем, что вызывает действия, которые стоит выпольнять на каждом шаге работы программы. Выделим отдельные функции для отрисовки фона и вывода буфера изображения, и заодно добавим функцию для отрисовки главного персонажа - и тогда главный цикл будеть лишь вызывать эти три функции, вся же настоящая работа будет происходить уже в них. Играть предстоит за самолётик, который к концу урока станет лететь направо, к своему недостижимому горизонту, уворачиваясь от препятствий. Обычно в играх камеру стараются расположить так, чтобы хорошо обозревалось пространство перед актором (главным героем, аватаром игрока). В силу технических причин не всегда это удаётся, но нам пока ничто не мешает расположить самолётик с левого края экрана - летит он направо, и так препятствия на пути будут видны заранее, до того, как актор "долетит" до них. чтобы не рисовать самолётик впритык к краю (где он иначе бы визуально сливался с границей окна) введём горизонтальное смещение MAINCHAR_X - количество отступа в символах от левой грани экрана до самолётика. Сам самолёт пока что будем отрисовывать символом "V":
#include <stdio.h>
#include <signal.h>
#define WIDTH 80
#define HEIGHT 24
#define DAMPING 500
#define MAINCHAR_X 10
int main_cycle; /* stop program if 0 */
unsigned int t; /* background offset */
char screen[(WIDTH + 1) * HEIGHT]; /* One extra char for linebreak */
void handler(int dummy)
{
main_cycle = 0;
}
void init_screen()
{
for (int y = 0; y < HEIGHT; y++)
{
for (int x = 0; x < WIDTH; x++)
{
screen[(WIDTH + 1) * y + x] = ' ';
}
screen[(WIDTH + 1) * y + WIDTH] = '\n';
}
screen[(WIDTH + 1) * HEIGHT - 1] = '\0';
}
void draw_pixel(int x, int y, char p)
{
if (x < WIDTH && y < HEIGHT)
{
screen[(WIDTH + 1) * y + x] = p;
}
}
void init_terminal()
{
printf("\033[?1049h"); /* Enables buffer mode in terminal */
printf("\033[?47h"); /* Tells terminal to save screen */
printf("\033[?25l"); /* Hide cursor */
printf("\033[2J"); /* Erase console screen */
}
void restore_terminal()
{
printf("\033[?25h"); /* Restore cursor visibility */
printf("\033[?47h"); /* Restore pre-program screen */
printf("\033[?1049l"); /* Disables buffer mode in terminal */
}
void draw_background()
{
init_screen(); /* Erase old image from buffer */
for (int x = 1; x < WIDTH; x++)
{
int y = HEIGHT * x / WIDTH;
int x_tmp = (x + t / DAMPING) % WIDTH;
draw_pixel(x_tmp, y, '#');
}
}
void draw_player()
{
draw_pixel(MAINCHAR_X, HEIGHT / 2, 'V');
}
void render_scene()
{
printf("\033[H"); /* Set cursor to upper-left position */
printf("%s", screen); /* Draws "screen" */
}
int main()
{
signal(SIGINT, handler); /* Registers handler() for SIGINT */
main_cycle = 1; /* Initializing runprogram flag */
init_terminal();
t = 0;
/* Main cycle */
while(main_cycle)
{
draw_background();
draw_player();
render_scene();
t--;
}
restore_terminal();
return 0;
}
Стоит скомпилировать программу и убедиться, что работает она как и ранее - но теперь главный цикл только вызывает последовательно функции: отрисовки фона, отрисовки аватара, и вывода сцены на экран. Плюс пока что программа не избавлена от привязки к тактам процессора, так что счётчик циклов программы t инкрементируется здесь же, в главном цикле.
Перепишем функцию отрисовки фона так, чтобы она не расчитывала позиции для диагональных линий каждый раз заново, а расчитывала их позицию только при их появлении на правой грани экрана. Всю остальную же картинку стоит брать из уже готового предыдущего готового кадра, сдвигая его по необходимости влево:
Для сдвига изображения понадобилась так же функция check_pixel()
- она является парной функции draw_pixel()
, но не помещает символ на холст, а возвращает значение пикселя холста.
Рисование правой грани экрана доверено формуле (t / DAMPING) % (HEIGHT * GAP_WIDTH)
- если вы не понимаете её суть, попробуйте разобраться или реализовать свой механизм отрисовки колонн. Формула же расчитывает появление очередного символа колонны на соответствующей высоте в зависимости от переменной t - то есть от времени, и заодно здесь вводится константа GAP_WIDTH, регулирующая расстояние между колоннами.
...
#define GAP_WIDTH 3
...
char check_pixel(int x, int y)
{
char result = '\0';
if (x < WIDTH && y < HEIGHT)
{
result = screen[(WIDTH + 1) * y + x];
}
return result;
}
...
void draw_background()
{
if (t % DAMPING == 0) /* Every DAMPING'th frame */
{
/* scrolls all screen */
for (int x = 0; x < WIDTH - 1; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
char pixel = check_pixel(x + 1, y);
draw_pixel(x, y, pixel);
}
}
/* construct right edge */
int block_y = (t / DAMPING) % (HEIGHT * GAP_WIDTH);
if (block_y >= 0)
{
/* draw new column element */
draw_pixel(WIDTH - 1, block_y, '#');
/* and erase old one */
draw_pixel(WIDTH - 1, block_y - 1, ' ');
}
}
}
...
int main()
{
signal(SIGINT, handler); /* Registers handler() for SIGINT */
main_cycle = 1; /* Initializing runprogram flag */
init_terminal(); /* Prepairing output device */
init_screen(); /* Have to do it once at start */
t = 0; /* Initializing tick counter */
/* Main cycle */
while(main_cycle)
{
draw_background();
draw_player();
render_scene();
t++;
}
restore_terminal(); /* Free output device */
return 0;
}
Подкрутить скорость скроллинга экрана просто, для этого нужно подобрать соответствующее значение константы DAMPING, например для меня комфортным оказывается следующее значение:
Проблему же с "хвостом", остаточным изображением самолётика можно не просто решить, но и преобразовать в эдакую "систему частиц": оставлять за самолётом след из символов '.':
void draw_background()
{
if (t % DAMPING == 0) /* Every DAMPING'th frame */
{
/* scrolls all screen */
for (int x = 0; x < WIDTH - 1; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
char pixel = check_pixel(x + 1, y);
/* particle system */
if (pixel == 'V')
{
pixel = '.';
}
draw_pixel(x, y, pixel);
}
}
/* construct right edge */
int block_y = (t / DAMPING) % (HEIGHT * GAP_WIDTH);
if (block_y >= 0)
{
/* draw new column element */
draw_pixel(WIDTH - 1, block_y, '#');
/* and erase old one */
draw_pixel(WIDTH - 1, block_y - 1, ' ');
}
}
}
Самолёт будет подвержен гравитации - заведём под сохранение вектора вертикальной скорости переменную character_velocity, и гравитационную постоянную GRAVITY. Функция draw_player()
начинает включать в себя логику обработки персонажа - поэтому стоит переименовать её из draw_player()
в handle_player()
. Чтобы противостоять гравитации, самолёт обзаведётся двигателем - импульс двигателя будет применяться в противоположном гравитации направлении. Заодно применим импульс с самого старта программы - чтобы самолёт не начал падать с самого начала игры.
Будем проверять, не вышел ли самолёт за границы экрана сверху или снизу - при выходе за границы экрана сразу же закончим программу.
... #define GRAVITY 0.02f #define START_POSITION HEIGHT / 2; #define ENGINE_FORCE 0.6f float y_player; /* player current y coord */ float character_velocity; /* main character vertical velocity */ ... void handle_player() { if (t % DAMPING == 0) /* every damping cycle */ { /* apply velocity */ y_player += character_velocity; /* stop program if out of bounds */ if (y_player >= HEIGHT || y_player < 0) { main_cycle = 0; } else { /* drawing player */ draw_pixel(MAINCHAR_X, y_player, 'V'); /* physics */ character_velocity += GRAVITY; } } } ... int main() { signal(SIGINT, handler); /* Registers handler() for SIGINT */ main_cycle = 1; /* Initializing runprogram flag */ init_terminal(); init_screen(); t = 0; y_player = START_POSITION; character_velocity = -ENGINE_FORCE; /* start impulse */ /* Main cycle */ while(main_cycle) { draw_background(); handle_player(); render_scene(); t++; } restore_terminal(); return 0; }
Теперь для того, чтобы программа превратилась в игру - осталось осуществить возможность интерактивного ввода. Поменяем функцию-обработчик сигнала так, чтобы вызов обработчика увеличивал вертикальный импульс самолёта. Нужно помнить, что сигнал SIGINT не предназначен для пользовательского ввода, и переопределение его для целей, не связанных с выходом из программы является не более чем хаком. В общем случае так делать нельзя, мы же пользуемся этим хаком только до тех пор, как научимся пользоваться клавиатурным вводом в неблокирующем режиме.
Пока SIGINT переназначен, выход из программы будет осуществляться простым образом - как только аватар игрока потерпит крушение - вылетит за пределы экрана. Также можно пользоваться неперeопределяемым сигналом SIGQUIT - "Ctrl+\". При остановке процесса при передаче сигнала SIGQUIT создаётся core-файл - слепок ядра операционной системы на момент остановки программы.
В игру уже можно играть - цель игры не коснуться краёв экрана. Но чтобы добавить вариативности, нужно ввести, например, препятствия. Чтобы сделать колонны из # препятствие, нужно добавить проверку столкновений - систему коллизий. В простейшем случае можно проверять пиксель, в который будет рисоваться символ самолёта - нет ли там уже символа колонны. Тогда для проверки коллизий стоит выделить отдельную функцию - которая по заданным координатам проверяет, свободны ли они в игровом мире. Кроме пересечения с колоннами эта же функция (по смыслу!) может проверять и нахождение координат внутри вертикальных границ
Теперь функция handle_player()
может вызывать check_collision()
, а не заниматься проверкой самостоятельно.
(Заодно здесь я поменял символ самолёта, определив его константой)
#define PLAYER_SPRITE '>'
#define PARTICLE_SPRITE '.'
...
/** 0 - no collision, other value - collision **/
int check_collision(int x, int y)
{
if (y < 0 || y >= HEIGHT)
{
/* 1 - out of bounds */
return 1;
}
char dot = check_pixel(x, y);
if (dot == '#')
{
/* 2 - column collision */
return 2;
}
return 0;
}
...
void draw_background()
{
...
if (pixel == PLAYER_SPRITE)
{
pixel = PARTICLE_SPRITE;
}
...
}
...
void handle_player()
{
if (t % DAMPING == 0)
{
y_player += character_velocity;
if (check_collision(MAINCHAR_X, y_player) != 0)
{
main_cycle = 0;
}
else
{
draw_pixel(MAINCHAR_X, y_player, PLAYER_SPRITE);
character_velocity += GRAVITY;
}
}
}
/** 0 - no collision, other value - collision **/
int check_collision(int x, int y)
{
if (y < 0 || y >= HEIGHT)
{
/* 1 - out of bounds */
return 1;
}
char dot = check_pixel(x, y);
if (dot == '#')
{
/* 2 - column collision */
return 2;
}
char dot_behind = check_pixel(x - 1, y);
char dot_under = check_pixel(x - 1, y);
char dot_from = check_pixel(x - 1, y - 1);
if ( dot_behind == '#'
&& dot_under == '#'
&&(dot_from != PLAYER_SPRITE || dot_from != PARTICLE_SPRITE))
{
/* 3 - column fly-through */
return 3;
}
return 0;
}
Теперь, когда игра вроде бы стала вести себя как задумано, корректно - играть в неё стало не интересно. Первая же колонна становится непреодолимым препятствием, и игра не позволяющая продвинуться дальше - в не зависимости от усилий игрока - скучна. Для добавления разрывов в колоннах меняется код генерации колонн: добавляется условие, когда в колонне не рисовать разрыв, и условие это берётся из простейшего псевдо-генератора случайных значений, генерирующее высоту пикселя начала разрыва.
#define VOID_SIZE (HEIGHT / 3)
...
void draw_background()
{
if (t % DAMPING == 0) /* Every DAMPING'th frame */
{
/* scrolls all screen */
for (int x = 0; x < WIDTH - 1; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
char pixel = check_pixel(x + 1, y);
if (pixel == PLAYER_SPRITE)
{
pixel = PARTICLE_SPRITE;
}
draw_pixel(x, y, pixel);
}
}
/* construct right edge */
int block_y = (t / DAMPING) % (HEIGHT * GAP_WIDTH);
if (block_y >= 0 && block_y <= HEIGHT)
{
/* count column's number */
int void_y = ((t)/ DAMPING);
void_y /= (HEIGHT * GAP_WIDTH);
/* generate pseudo-random height from it */
void_y *= 31337;
void_y %= HEIGHT - VOID_SIZE;
/* draw new column element */
if (block_y < void_y
|| block_y > void_y + VOID_SIZE)
{
draw_pixel(WIDTH - 1, block_y, '#');
}
/* and erase old one */
draw_pixel(WIDTH - 1, block_y - 1, ' ');
}
}
}
Почти! Стоит добавить маленький штришок - счётчик очков:
int score; /* achieved player score */
...
void handle_player()
{
if (t % DAMPING == 0)
{
score++;
y_player += character_velocity;
if (check_collision(MAINCHAR_X, y_player) != 0)
{
main_cycle = 0;
}
else
{
draw_pixel(MAINCHAR_X, y_player, PLAYER_SPRITE);
character_velocity += GRAVITY;
}
}
}
...
int main()
{
signal(SIGINT, handler); /* Registers handler() for SIGINT */
main_cycle = 1; /* Initializing runprogram flag */
init_terminal();
init_screen();
t = 0;
score = 0;
character_velocity = -ENGINE_FORCE;
y_player = START_POSITION;
/* Main cycle */
while(main_cycle)
{
draw_background();
handle_player();
render_scene();
t++;
}
restore_terminal();
printf("Game Over! Your score: %d\n", score);
return 0;
}
Самостоятельная работа (необязательно, но стоит проработать код ради того, чтобы позже были понятны преимущества ООП и структуризации проекта, использования систем сборки):
- Следующий урок посвящён рефакторингу. Попробуйте переработать структуру программы по своему представлению - чтобы улучшить читаемость проекта, и облегчить возможность внесения доработок.
- Сделайте расстояние между колоннами и разрыв в колонне нефиксированными, зависящими от значений генератора случайных значений.
- Добавьте вывод полученных очков непосредственно в процессе игры, добавьте "жизни" - чтобы игра прекращалась не с первым же касанием препятствия.
- Добавьте анимацию "взрыва" - чтобы при касании препятствия в месте касания отображалась какая-нибудь динамическая система частиц.