Grabduck

Шутер с псевдо-3D графикой на… bash

:

Здравствуй, Хабрачеловек!

Решил я как-то, что неплохо бы научиться писать «Hello world!» на bash. Как-никак уже полгода работаю на убунте, стыдно не уметь такого. Поискал на Хабре и понял, что просто почитать мануалы нынче не модно, надо написать свою игру. Осталось выбрать какую. Шахматы, Xonix, Sokoban, Морской бой уже написали, Тетрис вроде тоже (хотя ссылки не нашел), что же выбрать? Первой идеей была стратегия, но была откинута из-за полной безумности (хотя я надеюсь, что один из тех, кто продолжит историю топиков про игры на bash напишет и ее). Поэтому я остановился на шутере.


* На картинке изображен коридор и монстр в нескольких шагах впереди

Ссылка на скрипт: github.com/EvilTosha/labirinth/blob/master/lab2.sh

Под катом вы найдете абсолютно неинтересное и ненужное описание внутренней части игры.

Зарисовки, перспектива и общая идея


Началось все с зарисовок на листочке бумаги.

Это нужно было каким-то образом перевести в вид, приемлемый для терминала. Как оказалось, перспективу коридора, приятную для глаза, нарисовать не так-то просто. Поэтому была написана небольшая программка на C++. Впоследствии она модифицировалась для генерации разных слоев выводимого изображения.

UPD
Отвечу на часто возникающую претензию. Этот код не используется в скрипте, он нужен только для предварительной подготовки данных, которые будут внесены в скрипт на этапе инициализации переменных и массивов.

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cmath>
#include <vector>

using namespace std;

//функция определения лежит ли точка p над или под прямой, проходящей
//точки p1 и p2
bool overLine(pair<int, int> p1, pair<int, int> p2, pair<int, int> p){
return (p1.second - p2.second) * p.first + (p2.first - p1.first) * p.second >
-(p1.first * p2.second - p2.first * p1.second);
}

const int width = 128;
const int height = 36;
//отступы начала прямых пола и потолка от краев экрана
const int delta_ceil = 20;
const int delta_floor = 20;

int main(){
freopen(".out", "w", stdout);
char field[height][width];
//точки центров схождения прямых перспективы
pair<int, int> p3(11, width / 2), p6(12, width / 2);
//точки начала прямых перспективы для пола и потолка
pair<int, int> p1(0, delta_ceil), p2(0, width - delta_ceil);
pair<int, int> p4(height, delta_floor), p5(height, width - delta_floor);
//уровни глубины для дистанций
int depths[8] = {10, 27, 39, 48, 54, 58, 61, 65};
for(int x = 0; x < height; ++x){
for (int y = 0; y < width; ++y){
pair<int, int> p(x, y);
//пол
if (!overLine(p2, p3, p) && !overLine(p3, p1, p))
field[x][y] = 'c';
//потолок
else if (overLine(p6, p4, p) && overLine(p5, p6, p))
field[x][y] = 'f';
//стена с указанием уровня
else{
int wall = min(y, width - y);
int d = 0;
while (wall > depths[d])
++d;
field[x][y] = '0' + d;
}
}
}
//вывод в файл
for(int x = 0; x < height; ++x){
for (int y = 0; y < width; ++y){
cout << field[x][y];
}
cout << endl;
}
return 0;
}
* This source code was highlighted with Source Code Highlighter.


Дальше были размышления: делать стены плоскими (использовать пространство между двумя соседними клетками поля) или занимающими 1 клетку. При первом варианте было непонятно что отрисовывать когда коридор поворачивает, и сразу же еще раз в обратную сторону. (Честную геометрию делать ну очень не хотелось на баше, да и работало бы это медленнее черепахи)

Поэтому было решено некоторые клетки поля сделать стенами. Но и тут есть некоторые проблемы с отрисовкой. Что например делать в такой ситуации?

Поэтому накладывается ограничение на генерируемый лабиринт — в нем не должно быть ни одного квадратика 2 * 2 без стен. Теперь этот лабиринт нужно сгенерировать.

Генерация лабиринта


Тут используется алгоритм, похожий на поиск в глубину. Т.е. заполняем все поле стенками, выбираем начальную точку, и начинаем обходить поле как граф, только перебираем всех соседей не по определенному порядку, а случайно. Кроме того проверяем не образовалось ли «запрещенного» квадратика 2 * 2. Получаем примерно такую картинку

Работает генерация довольно долго (для 20 * 20 примерно пару секунд), предположительно разворот рекурсии в стек дал бы ощутимый прирост скорости, но зачем нам такие большие лабиринты?

Быстрая отрисовка


Изначально каждый «пиксель» выводился собственным echo. При размере «экрана» 36 * 128, рисование одного «фрейма» занимало почти секунду, и мне это очень не нравилось. Если рисовать только те «пиксели» которые изменились, скорость падает еще больше. Поэтому было предпринято следущее: кладем все символы в массив, а потом вызываем
echo -ne "${screen[*]}"

для вывода всех элементов. Но при таком вызове элементы массива разделяются пробелами. Экран для вывода у меня тоже состоит только из разноцветных пробелов, поэтому можно было бы закрыть на это глаза, но хотелось некоторой универсальности. Решение было таким: поменять IFS (Internal Field Separator, внутренний разделитель полей), который изначально равен "\n\t " (перевод строки, таб и пробел) на пустой, а при завершении скрипта поменять обратно (чтобы можно было продолжать, не переоткрывая терминала). Это полностью решило проблему. Но впрочем больше 5 FPS получить не удалось, поэтому «realtime-mode» изначально отключен и шутер получается пошаговым. Но если хочется поиздеваться над глазами «мерцанием» экрана, его можно включить в константах.
Кстати во всем скрипте используются только стандартные 8 цветов для текста и фона. Если заморочиться с большим количеством цветов, можно сделать красивое градиентное освещение, да и цвета более естественными…

Создание монстра, возможности стрелять и прочих мелочей описывать не буду, там все просто и скучно.

Заключение


Осталось много вещей которые хотелось сделать, но не хватило терпения (точнее интерес пропал раньше, чем они были реализованы). Это например несколько монстров вместо одного, корректная работа жизней (сейчас они выводятся только для красоты), смена оружия (и соответственно разные характеристики), консоль (iddqd, а как же? =) ) и, конечно, сетевая игра.

Хотел залить еще видео игрового процесса, но при установке нужного софта для записи, убунта на виртуалбоксе сломалась. =(

Спасибо за внимание, буду рад любой критике!