Участие в конкурсе LudumDare #33

Несмотря на то, что за все два дня конкурса фантастическая идея игры ко мне так и не пришла, хочу поделиться небольшими наработками, которые были сделаны за пару дней. Возможно, статья найдет своего читателя, и кому-то до сих пор нравится pure-java вместо модных движков.

Это мое пятое участие, и каждый божий раз я пишу каждую буковку с нуля. Так уж повелось, что пока я пишу основные методы для работы с графикой (их не так много как кажется), придумываю что именно писать. Иногда даже что-то выходит.

image

Если не вдаваться в подробности, игра довольно примитивная. Пока идет день, вы управляете медлительным монстром, на которого со всех сторон нападают рыцари. Но как только приходит ночь, ситуация резко меняется, теперь вы очень шустрый одноглазый монстр, который способен истреблять рыцарей. Но все не так радужно, так как я привнес в агрессивное поведение (ночью) автоконтроль, который практически все делал за вас: и убивал, и искал новых жертв-рыцарей.

Интересных моментов (для разработчиков) всего парочку, это, вероятно, генерация тайлов и ночное зрение, которое сильно искажает видимость, нечто похоже на эффект fish eye. Эффект, к слову сказать, получился сам собой, от скуки. Дело в том, что я терзал себя мыслью, что так и не придумал годной идеи для игры, но останавливаться было поздно, и я делал, делал, делал.

Давайте рассмотрим эти моменты поподробней. Для начала, ночное зрение:

image

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

Пост рендер
    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);
    }


Я думаю тут вопросов возникнуть не должно. Разумеется, можно поиграть с некоторыми значениями и достичь более приятного эффекта.

Большая гифка на 33 мб
image

Теперь давайте про генерацию тайлов. В принципе, секретов тут нет, если вы занимались когда-нибудь созданием 2D игр, то знаете, что для перехода одной текстуры в другую необходимо 16 тайлов. Наша задача сгенерировать эти тайлики. Итак давайте подумаем, у каждого тайла есть 4 угла, необходимо сделать 16 видов текстур, учитывая переходы от одного тайла в другой. Вот пример сгенерированных текстур по этому методу, разве что с преобразованием в изометрию.

image

Но мы сейчас рассмотрим простой пример для игры без изометрии.

Генератор текстур
    //итак на входе у нас две текстуры 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);
    }


Вот собственно и все. Ничего фантастического, но я от таких мелочей получаю массу удовольствий, чего и вам советую.
  • +16
  • 9.7k
  • 7
Share post

Comments 7

    0
    Где ссылка на веб версию?
      0
      А я бы на github попросил ссылку, если конечно автор выкладывал. Ну и конечно «заново пишу каждую буковку» — это достойно.
      Правилами конкурсов запрещено использовать готовые наработки?
        0
        Если участвуешь в compo, то использовать наработки можно, если их опубликовать заранее.
        Вот ссылка на entry конкурса, там и веб версия, и исходный код.
          0
          Спасибо!
      0
      А почему возникает такая штука по центральным осям? Это как-то зависит от распределения рандомных значений?
        +1
        Скорей перенасыщен процент попадания при искажении в близкие исходные координаты. Наверно, можно попробовать этого избежать, но случайный эффект — тоже эффект.
          +1
          Просто я однажды сталкивался с аналогичной проблемой, когда писал реализацию спрей-кисти. Куча случайных точек густо занимало все пространство, кроме центральных оси в пиксель длиной. Вроде даже топик с проблемой на форуме кокоса остался.
          Та самая проблема
          image

      Only users with full accounts can post comments. Log in, please.