Создание генератора мира для minecraft

Введение


Думаю, почти все читатели Хабра слышали про майнкрафт, кто-то играл в сингле, кто-то на одном из многочисленных серверов, был даже небольшой сервер у кого-то из хабраюзеров. После двух месяцев игры я задумался — а реально ли написать свой генератор карты? Как оказалось, это вполне возможно сделать за несколько дней неторопливого гугления и кодинга.

Немного технической части

Согласно вики-проекту майна, карты хранятся в файлах регионов (32x32 блока по 16x16x128 кубов, итого 262144 квадратных блоков на регион), имеющих следующую структуру:
  1. 4096 байт, содержащих оффсеты чанков (так называются блоки 16x16x128) и их размер в блоках по 4кб, округляя вверх, 3 байта оффсет, 1 — размер
  2. 4096 байт timestamp'ов чанков, по 4 байта на каждый
  3. Оставшееся место до конца файла — собственно, данные чанков, сжатые Zlib. 4 байта — размер сжатых данных, 1 — способ сжатия (по умолчанию 2, Zlib (RFC1950)), размер-1 запакованная злибом NBT-структура, т.е сам контейнер кубов
    Если упакованные данные занимают меньше целого числа секторов по 4 кб, то остаток сектора заполняется нулями, т.к каждый чанк должен начинаться с оффсета, выраженного целым числом секторов по 4096 байт

Выбор языка

Реализовать такую структуру можно на любом языке, я остановился Delphi 7. Во-первых, это пока единственный язык, который я знаю, во-вторых, именно на 7 версии года 4 назад я начинал писать блокноты по мануалам из Игромании.

Код

Так как данные хранятся в сжатом виде, нам необходим модуль zlib.
Я использовал ZlibEx

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

  Tchunk = class(TObject)
  private
  public
    Data:     tmemorystream;
    c_data:   tmemorystream;
    c_stream: tzcompressionstream;
    constructor Create;
    procedure writeblock(x, y, z, block: integer); overload;
    procedure writeblock(x, y, z, block, color: integer); overload;
    procedure compress;
  end;


Код этого класса:

constructor tchunk.Create;
begin
  Data      := TMemoryStream.Create;
  Data.size := 82360;
  Data.LoadFromFile('data.bin');
  c_data   := TMemoryStream.Create;
  c_stream := tzcompressionstream.Create(c_data, zcdefault, 15, 8, zsdefault);
end;

procedure tchunk.writeblock(x, y, z, block: integer);
begin
  Data.Seek(form1.getoffset(x, y, z) + 16487, 0);
  Data.Write(block, 1);
end;




procedure tchunk.compress;
var
  buffer: array  [0..82360] of byte;
begin
  c_data.Position := 0;
  Data.Position   := 0;
  Data.Read(buffer, 82360);
  c_stream.writebuffer(buffer, 82360);
  c_stream.Free;
  c_data.SaveToFile('file' + IntToStr(n));
end;


Функция getoffset выдает нужое смещение по формуле y + ( z *128 + ( x * 128 * 16 ) )
function tform1.getoffset(x, y, z: integer): integer;
begin
  Result := y + (z * 128 + (x * 128 * 16));
end;


Добавим в var пару переменных:

chunks:array[0..32] of array[0..32] of tchunk;
n:         integer=0;


Процедура для сборки всех чанков в готовый файл:

procedure tform1.SwapEndiannessOfBytes(var Value: cardinal);
var
  tmp: cardinal;
  i:   integer;
begin
  tmp := 0;
  for i := 0 to sizeof(Value) - 1 do
    Inc(tmp, ((Value shr (8 * i)) and $FF) shl (8 * (sizeof(Value) - i - 1)));
  Value := tmp;
end;

procedure tform1.generatefile;
var
  fileoffset: integer;
  time, compressiontype, counter: integer;
  filename: string;
  regionfile: tfilestream;
  tmp: cardinal;
  size: integer;
  n_x, n_z: integer;
  bu: array[0..99999] of byte;
  n:  integer;
  roundedsize: integer;
  neededsize: integer;
  d:  byte;
begin
  fileoffset := 2;
  time := $d8de2f4e;
  compressiontype := $02;
  filename := GetVar('Appdata') + '\.minecraft\saves\NewWorld\region\r.0.0.mcr';
  regionfile := tfilestream.Create(filename, fmcreate);
  n := 0;
  for n_x := 0 to 31 do
    for n_z := 0 to 31 do
    begin
      chunks[n_x][n_z].compress;
      roundedsize := ((chunks[n_x][n_z].c_data.Size) div 4096);
      if (((chunks[n_x][n_z].c_data.Size) mod 4096) > 0) then
        Inc(roundedsize);
      regionfile.seek((4 * ((n_x mod 32) + (n_z mod 32) * 32)), 0);
      tmp := fileoffset;
      SwapEndiannessOfBytes(tmp);
      tmp := tmp shr 8;
      regionfile.Write(tmp, 4);
      regionfile.seek(4 * ((n_x mod 32) + (n_z mod 32) * 32) + 3, 0);
      regionfile.Write(roundedsize, 1);
      size := chunks[n_x][n_z].c_data.Size + 1;
      regionfile.seek(fileoffset * 4096, 0);
      tmp := size;
      SwapEndiannessOfBytes(tmp);
      regionfile.Write(tmp, 4);
      regionfile.Write(compressiontype, 1);
      chunks[n_x][n_z].c_data.Position := 0;
      chunks[n_x][n_z].c_data.readbuffer(bu, chunks[n_x][n_z].c_data.size);
      regionfile.Writebuffer(bu, chunks[n_x][n_z].c_data.size);
      regionfile.seek((n) * 4 + 4096, 0);
      regionfile.Write(time, 4);
      fileoffset := fileoffset + ((chunks[n_x][n_z].c_data.Size) div 4096);
      if (((chunks[n_x][n_z].c_data.Size) mod 4096) > 0) then
        fileoffset := fileoffset + 1;
      Inc(n);
    end;
  neededsize := 4096 * fileoffset - regionfile.Size - 1;
  regionfile.Seek(regionfile.Size, 0);
  d := 00;
  for n := 0 to neededsize do
    regionfile.Write(d, 1);
  regionfile.Free;
end;


Всё, теперь мы имеем метод записи любого блока по любой координате, в пределах региона. При желании, несложно повторить то же для остальных регионов, надо строк 10 кода.

Обертка для writeblock:

procedure tform1.writeworld(x, y, z, block: integer);
var
  xw, zw: integer;
begin
  xw := (x div 16);
  zw := (z div 16);
  chunks[xw][zw].writeblock(x mod 16, y, z mod 16, block);
end;


Генерация мира, его сжатие и сохранение.

procedure TForm1.Button4Click(Sender: TObject);
var
  x, y, z: integer;
  xx, zz:  integer;
  image:   tbitmap;
begin
  for xx := 0 to 31 do
    for zz := 0 to 31 do
    begin
      chunks[xx][zz] := tchunk.Create;
    end;
  image := tbitmap.Create;
  image.LoadFromFile('image.bmp');
     for x:=0 to 127 do
      for y:=0 to 116 do
      begin
      if image.Canvas.Pixels[x,y]=clblack then
        form1.writeworld(x,117-y,0,49);
      if image.Canvas.Pixels[x,117-y]=clwhite then
        form1.writeworld(x,y,0,80);
      end;
form1.generatefile;


Результат:
image
image

Можно генерировать не только пиксельарт, но произвольные фигуры, все, что можно задать какой-либо формулой. Например, пол в виде синусоиды:
image

Проект можно скачать тут.

Known bugs:
  • Невозможно сохранять изменения в сгенерированном регионе (возможно, из-за того, что пишется одинаковый timestamp, который не совпадает с временем последнего сохранения в level.dat, как разберусь с форматом последнего — попробую реализовать)
  • Спавн лучше переставить с помощью McEdit, т.к вполне возможно, что после генерации он окажется в сотне блоков над землей, что чревато летальным исходом(тоже можно менять в level.dat)
  • Нет генерации света, вместо этого освещены все блоки, даже под землей(Рассчет освещения — отдельная серьёзная задача, пока не готов ее решать)

ToDo:
  • Починить сохранение, т.к без этого теряется половина смысла
  • Сделать поддержку записи дополнительной инфы(цвет шерсти, листвы, ориентация печек, etc) // частично готово
  • Какое-то подобие ландшафта(холмы/дома/озера)


Update:
  • Доработал generatefile, сделал нормальный разворот
  • Форматирование кода
  • Наброски additional block data, см. в проекте, ссылка обновлена

Ссылки:
Поделиться публикацией

Похожие публикации

Комментарии 49

    +1
    В плагин WorldEdit входит похожий функционал: https://github.com/sk89q/worldedit/blob/master/craftscripts/draw.js
      +8
      Да, этот плагин хорош, но, согласитесь, в любом деле — гораздо приятнее сделать что-то своими руками.
        +2
        я бы не стал своими руками собирать, например, боинг
          +5
          Попробуйте — может вам понравится.
            +1
            мне не нужен боинг, говорю вам со 100% уверенностью. разве что продать… да и то, я не на столько людоненавистник, чтобы потом считать трупы)
              0
              Подарите мне! А я уже продам ;)
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              точно, стать на нос боинга, как ленин на броневике, и командовать рабочими)
              0
              В любом случае вы можете лишь попытаться собрать «боинг», наверняка получится что-то свое. И это что-то может даже и не взлетит, а, скажем, поплывет :-)
            +3
            А я пошёл ещё дальше и написал свой MineCraft с преферансом и блудницами! B]
              0
              Это офигенно.
                0
                Написать ЭТО за недельку — мне до такого еще расти и расти.
                  0
                  Заходи на IGDC и участвуй, кто ж мешает =) Исходники к работам выкладываются, можно подсмотреть что-то и потырить. А XProger — сам по себе злостный маньяк =) Как ни напишет чего на конкурс какой — так получается нечто шедевральное, и обязательно займет призовое место =) Почти как Груздев, только в 3D =)
                  0
                  маленький баг: можно лизать свою кровь с границы уровня и восстанавливать свою жизнь до бесконечности)
                    0
                    Это фитча, как и стояние в углах уровня и проход напрямую к выходу )
                    0
                    А нет желания серьезно продолжить этот проект?
                      0
                      Нет, ведь есть куча других не менее интересных проектов )
                0
                Производит впечатление чего то дельного. Я так понял по изображению вы генерите стенку из кубиков?
                  0
                  Это одна из возможностей, причем не самая сложная, все ограничено только фантазией.
                  0
                  Не совсем понятно, на этих картах можно играть потом?
                    0
                    Да, играть можно, однако после выхода и повторного входа в игру изменения почему-то не сохраняются. Сейчас работаю над эти.
                  +3
                  Про «правильный порядок» поглядите тут — это теория.
                  Примеры тут или тут

                  Еще, на сколько я помню, после каждого write в файл seek делать не нужно, потому как запись сама перемещает указатель по файлу.

                  И, вопрос, код действительно такой страшный, без отступов или это при раскраске съелось?
                    –3
                    За теорию — спасибо, как раз это и нужно. Seek действительно кое-где стоит убрать, это скорее перестраховка, но в большинстве случаев он нужен, т.к. файл собирается не последовательно, а скачет туда-сюда. Код действительно такой, ибо вручную мне его форматировать лень, надо будет поискать, как делать это автоматически. Или в более новых версиях такая возможность встроена?
                      +12
                      вручную мне его форматировать лень

                      Стыдно должно быть. Вас же ж люди читают
                        0
                        Эх, если бы форматирование было единственной проблемой. Если глянуть на код — оно там самая несущественная проблема на фоне остального безобразия.
                          +3
                          Стыдно не за форматирование, а за то, что программисту лень писать свой код с высоким качеством.
                          0
                          Раскаиваюсь. Прямо сейчас качаю Jedi Code Format. Код тоже постараюсь привести в порядок. По крайней мере, уберу этот ужасный разворот байтов(как я уже писал, мне за него самому стыдно).
                      0
                      Вроде бы уже написано достаточное количество библиотек для работы с картами на родном для Майнкрафта языке — Java.
                        0
                        Да, но также написано множество плееров/конвертеров/браузеров/ etc. Многие хотят сделать своё, с картами и блудницами.
                          0
                          этот язык — главный минус игрушки, даже на современном и мощном железе часто появляются лаги. хотя хватает сторонних визуализаторов карт(типа eihort) которые рендерят полностью мир, жрут в 5+ раз меньше оперативы и не лагают.
                            0
                            Minecraft же не только визуализатор, он ещё и механику считает. А рендер идёт через OpenGL.
                            Собираюсь попробовать запустить его на Java7 (возможно со сборщиком мусора G1).
                              0
                              под механикой вы наверно имеете ввиду примитивную физику мира, AI мобов и логику редстоуна? ну так вот я не верю что они в силах уложить на лопатки современный 4х ядерный проц(который кстати и не грузится особо при игре).

                              Вообще я ради интереса довольно долго следил за тем как себя ведет игра, но до конца так и не понял: например внутриигровая статистика(по F3) показывает что используется 200-300мб оперативы(из выделенного для жавы гига) при этом через пару часов игры винда выдает сообщение о нехватке оперативы и предлагает закрыть майнкрафт. если продолжить играть то через какое то время игра сама выдаст сообщение что память закончилась и надо перезапустить игру. на за все время игры потребление оперативы по внутренней инфе не превышало и 500мб, да и видно что сборщик мусора постоянно работает. еще мне не совсем понятно что майнкрафт постоянно делает с винтом — очень активно постоянно чтото читает/пишет… это при том что игра весит 40мб, а небольшой мир 10-20мб — которые можно держать в озу и синкать раз в минуту или реже.

                              пробовал как на jde6, так и на jde7. на линухе кстати тормозов заметно меньше, но там проблемы с управлением бывают и с захватом курсора. ну и еще стоит запустить параллельно с игрой какой нибуть хром или фаерфокс(с парой закладок) и в игре начинают появляться фризы.

                              В общем жду и надеюсь что появится аналог майнкрафта без джавы. пока поигрывая в террарию:)
                                0
                                Механика примитивная, но затратная. Для каждого кубика надо: пересчитать свет если он изменился от времени суток или другого кубика; если вода — проверить не растечётся ли она на соседние блоки; если земля — не вырастет ли на блоке трава или что-то другое; то же самое для снега, и прочее, и прочее.
                                И вот на таких вот несложных алгоритмах Java со своим JIT-компилятором не на очень много уступает в производительности Си.

                                Потребеление памяти изнутри показывается не всё, а только то, что приходится на кучу. Есть ещё так называемая PermGen память.

                                А Террария — это же .Net, та же Java, только вид сбоку.
                                  +1
                                  я прекрасно понимаю что там дофига мелких(примитивных по большей части) расчетов, но это абсолютно ничего не меняет — возьмите любой современный шутер с практически реальной физикой, динамическим освещением, аи ботов — он идет на компе без тормозов, а майнкрафт начинает лагать. и врядли виной тому сам код игры, думаю главная проблема всеже в яве.

                                  .Net может в чем то и аналог явы, но намного лучше вылизанный(но конечно не такой кроссплатформенный). хотя это взгляд пользователя софта на обоих платформах. может для программиста ява идеал, а дотнет сущий ад — об этом судить не могу.
                                    +1
                                    В шутерах как раз больше вычисляется графика — полигоны, освещение, шейдеры, и это всё работает большей частью внутри видеокарты. А с физикой там расчитывается небольшое количество близлежащих объектов. Шутеры и летают на современных видеокартах, а не самих компах. Stalker, например, с некоторыми модами, которые добавляют игровую механику, а не графику, тормозит не хуже Майнкрафта.

                                    Я понимаю, что у публики к Java намертво приклеилась слава тормоза, но на самом деле не всё так просто.
                              0
                              Возможно, лаги — расплата за кроссплатформенность. Насколько я знаю, майн существует почти под всё — Windows, MacOs, Linux, XBox360, даже под телефоны есть. Немногие языки могут этим похвастать.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  +2
                                  Warsow тоже идет под кучей платформ, имеет гораздо более крутую графику и при этом не лагает. Просто если заглянуть в код крафта (декомпилированный и деобфусцированный с помощью MCP), можно довольно легко убедиться в том, что Нотч — быдлокодер. Об этом также говорят и многочисленные баги в новых версиях — добавляют одно, ломается другое (которое вообще вроде никаким местом не причастно к изменениям). Если оптимизировать код, игра просто летала бы.
                                    0
                                    А видели бы вы как реализован сервер! Это просто песня. Любое изменение чанка, приводит к посылке всего чанка (со всеми данными) на сервер, а сервер ретранслирует всем клиентам включая пославшего. таким образом майнкрафт легко генерирует исходящий трафик от сервера на уровне 1мбит на 5 игроков.

                                    Зачем слать куски карты я не представляю, во всех играх принято слать действие игрока.
                                      0
                                      Песня даже не в этом. ВСЁ, происходящее на сервере, идет в одном потоке, т.е. при ~100 человек онлайн лаги неизбежны на любом железе. Что касается посылки карты — имхо, дело в том, что при тех же 100 онлайна проще просчитать карту на сервере и раздать ее всем, чем заставлять гораздо менее мощные клиенты просчитывать всю эту толпу.
                                        0
                                        P.S: Могу ошибаться, т.к. говорю со слов админов сервера, на котором я играю. Называть не буду, ибо реклама, но в mctop.ru он занимает одно из первых мест, название начинается на G… Причем админы там явно знают, про что говорят, тк они переписали сервер с нуля.
                              0
                              вся прелесть майкрафта в том, что там есть биомы.

                                0
                                ну не знаю, для меня биомы весьма незначительный плюс — главное это полная свобода и возможность реализовать свои задумки. если бы еще немного механику редстоуна переделать, для сбора более компактных схем — было бы вообще замечательно.
                                0
                                Буквально вчера в списке рассылки ruby-talk видел анонс библиотеки RubyCraft.
                                  0
                                  Что еще раз подтверждает, что не «достаточное количество библиотек для работы с картами»
                                  0
                                  >именно на 7 версии года 4 назад я начинал писать блокноты по мануалам из Игромании
                                  Именно на 7 версии, 7 лет назад я начинал писать <уже не помню что> по мануалам из Игромании.
                                  Похоже что-то в этом мире остаётся неизменным.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое