[Из песочницы] Участие в конкурсе LudumDare #33
Несмотря на то, что за все два дня конкурса фантастическая идея игры ко мне так и не пришла, хочу поделиться небольшими наработками, которые были сделаны за пару дней. Возможно, статья найдет своего читателя, и кому-то до сих пор нравится pure-java вместо модных движков.
Это мое пятое участие, и каждый божий раз я пишу каждую буковку с нуля. Так уж повелось, что пока я пишу основные методы для работы с графикой (их не так много как кажется), придумываю что именно писать. Иногда даже что-то выходит.
Если не вдаваться в подробности, игра довольно примитивная. Пока идет день, вы управляете медлительным монстром, на которого со всех сторон нападают рыцари. Но как только приходит ночь, ситуация резко меняется, теперь вы очень шустрый одноглазый монстр, который способен истреблять рыцарей. Но все не так радужно, так как я привнес в агрессивное поведение (ночью) автоконтроль, который практически все делал за вас: и убивал, и искал новых жертв-рыцарей.
Интересных моментов (для разработчиков) всего парочку, это, вероятно, генерация тайлов и ночное зрение, которое сильно искажает видимость, нечто похоже на эффект fish eye. Эффект, к слову сказать, получился сам собой, от скуки. Дело в том, что я терзал себя мыслью, что так и не придумал годной идеи для игры, но останавливаться было поздно, и я делал, делал, делал.
Давайте рассмотрим эти моменты поподробней. Для начала, ночное зрение:
Как водится, рендеринг сперва происходит в оффскринный буфер, и как только все игровые элементы нарисованы, следует делать пост-обработку, такую как шум, искажения и пр.
public void postRender(Bitmap screenBitmap, Graphics2D g2d) {
//получаем данные из оффскринного буфера
int[] pixels = screenBitmap.pixels;
//бежим по нему
for (int i = 0; i < pixels.length; i++) {
//находим координаты относительно центра экрана
int x = i % screenBitmap.w - GameComponent.WIDTH / 2;
int y = i / screenBitmap.w - GameComponent.HEIGHT / 2;
//считаем угол на который повернем систему координат и возьмем пиксель из буфера
//тут надо пояснить что x делим на высоту экрана, только ради эффекта закрытия глаза
//ну и применяем скаляр зависимости от времени суток, это просто число от 0..255
double angle = (x / (double) screenBitmap.h * y / (double) screenBitmap.h) * Math.PI * 2.0 * (255 - dayFactor) / 255.0;
//поворачиваем координаты и получаем исходную точку
int xx = (int) (x * Math.cos(angle) - y * Math.sin(angle)) + GameComponent.WIDTH / 2;
int yy = (int) (y * Math.cos(angle) + x * Math.sin(angle)) + GameComponent.HEIGHT / 2;
//разумеется эта точка может лежать за пределами, поэтому надо проверять
if (xx >= 0 && yy >= 0 && xx < screenBitmap.w && yy < screenBitmap.h) {
int c = pixels[xx + yy * screenBitmap.w];
int r = (c >> 16) & 0xff;
int g = (c >> 8) & 0xff;
int b = (c >> 0) & 0xff;
//тут мы получаем gray scale, формулу взял отсюда
//https://ru.wikipedia.org/wiki/%D0%9E%D1%82%D1%82%D0%B5%D0%BD%D0%BA%D0%B8_%D1%81%D0%B5%D1%80%D0%BE%D0%B3%D0%BE
int m = (r * 30 + g * 59 + b * 11) / 100;
//ну и стандартные преобразования изменяя насыщенность цветов от времени суток
r = (r + m) / 2 * dayFactor / 255;
g = (g + m) / 2 * dayFactor / 255;
b = (b + m) / 2 * dayFactor / 255;
//все это засовываем в другой буфер, так как исходный нельзя менять до полного прогона
postData[i] = 0xff << 24 | r << 16 | g << 8 | b;
} else {
//ну и если не попали, рисуем "закрытый глаз" немного шума с добавлением красного цвета
int rnd = (int) (random.nextDouble() * 16);
postData[i] = 0xff << 24 | (0x4C + rnd) << 16 | rnd << 8 | rnd;
}
}
//теперь можно скопировать все из пост буфера в оффскринный буфер, который дальше пойдет на экран.
for (int i = 0; i < postData.length; i++) {
pixels[i] = postData[i];
}
g2d.setColor(Color.WHITE);
g2d.drawString("score: " + score, 10, GameComponent.HEIGHT - 10);
}
Я думаю тут вопросов возникнуть не должно. Разумеется, можно поиграть с некоторыми значениями и достичь более приятного эффекта.
Теперь давайте про генерацию тайлов. В принципе, секретов тут нет, если вы занимались когда-нибудь созданием 2D игр, то знаете, что для перехода одной текстуры в другую необходимо 16 тайлов. Наша задача сгенерировать эти тайлики. Итак давайте подумаем, у каждого тайла есть 4 угла, необходимо сделать 16 видов текстур, учитывая переходы от одного тайла в другой. Вот пример сгенерированных текстур по этому методу, разве что с преобразованием в изометрию.
Но мы сейчас рассмотрим простой пример для игры без изометрии.
//итак на входе у нас две текстуры 16х16, на выходе 16 текстур с переходами одной в другую
public static BufferedImage[] generate(int[] t0, int[] t1, int sz) {
double iSz = 1.0 / sz;
BufferedImage[] result = new BufferedImage[16];
for (int i = 0; i < 16; i++) {
//создаем новый битмап в котором будем рисовать тайл
result[i] = new BufferedImage(sz, sz, BufferedImage.TYPE_INT_RGB);
int[] data = new int[sz * sz];
//находим состояние 4 углов для данного тайла
//очень важно понимать, что состояния будут уникальными для каждого значения i для всех 4 углов
int a = (i >> 0) & 1;
int b = (i >> 1) & 1;
int c = (i >> 2) & 1;
int d = (i >> 3) & 1;
//бежим сверху внизу
for (int y = 0; y < sz; y++) {
double yp = y * iSz;
for (int x = 0; x < sz; x++) {
double xp = x * iSz;
//сперва интерполируем углы ab между собой используя в качестве t = xp
double ab = a + (b - a) * xp;
//затем углы cd используя в качестве t = xp
double cd = c + (d - c) * xp;
//финальная интерполяция состояния ab и cd но используем в качестве t = yp
double val = ab + (cd - ab) * yp;
//...возможно добавления какого-нибудь стабильного шума для val
//в теории значение не должно выходить за границы, но у меня выше были преобразования для val
if (val < 0) val = 0;
if (val > 1.0) val = 1.0;
//получаем значение из текстуры 1 и 2 и интерполируем цвета в качестве t = val
int c0 = t0[x + y * sz];
int c1 = t1[x + y * sz];
int col = Mth.lerpRGB(c0, c1, val);
//исходный код размещаем в битмапе
data[x + y * sz] = col;
}
}
result[i].setRGB(0, 0, sz, sz, data, 0, sz);
}
return result;
}
А теперь самое вкусное: как же отображать полученные тайлы на основании сгенерированной карты? Ответ — очень и очень просто:
public void render(Graphics2D g2d, Level level, int x, int y, int xOffs, int yOffs) {
int xx = x >> 4;
int yy = y >> 4;
int t = 0;
//чтоб получить текущую текстуру нам необходимо проверить сперва тайл в центре
if (level.getTile(xx, yy) != Tile.water) t += 1;
//затем тайл справа от центра
if (level.getTile(xx + 1, yy) != Tile.water) t += 2;
//затем тайл снизу от центра
if (level.getTile(xx, yy + 1) != Tile.water) t += 4;
//затем тайл справа снизу от центра
if (level.getTile(xx + 1, yy + 1) != Tile.water) t += 8;
g2d.drawImage(Art.waterToGrassTiles[t], x - xOffs, y - yOffs, null);
}
Вот собственно и все. Ничего фантастического, но я от таких мелочей получаю массу удовольствий, чего и вам советую.