Как стать автором
Обновить

Как я решился писать свою текстовую MMORPG игру с открытым миром и фронтом Telegram в 2024 году

Уровень сложностиПростой
Время на прочтение14 мин
Количество просмотров1.1K

Привет! Здесь будет много текста. Помимо технических моментов, поделюсь своими мыслями вслух. Если готов выслушать, заваривай чай или кофе и присаживайся поудобнее.

Что такое текстовая игра?

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

Последние два пункта и ещё один стали главными причинами для написания собственной игры:

  1. Доступность: Играть можно где угодно и сколько угодно, почти без ограничений.

  2. Минимальные требования: Достаточно телефона с интернетом и веб-браузером.

  3. Воображение: Возможность использовать самую мощную «видеокарту» — мозг игрока.

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

Чем я занимаюсь и какое мое хобби

Мне 35 лет, женат уже 10 лет, и у меня двое детей — 12 лет и 1 год. Последние 12 лет я занимаюсь рекламой, маркетингом и аналитикой, этим зарабатываю на хлеб и кормлю семью.

В 2011-2012 годах я познакомился с HTML4 и CSS2 и с тех пор полюбил не только вёрстку, но и программирование в целом. В 2015 году я даже преподавал начальный курс вёрстки в офлайн-школе, где мы рассматривали HTML, CSS и немного Bootstrap. Тогда слова "Front End" ещё не было в обиходе, а наш курс считался скромным даже по тем стандартам.

Со временем я создал YouTube-канал и, несмотря на отсутствие опыта в профессиональном программировании, начал обучать людей Python, JavaScript и PHP. Я не был инфоцыганом: после офлайн-преподавания заметил, что могу доступно объяснять сложные концепции, заинтересовывая людей.

Так программирование стало моим хобби. Это невероятное чувство — кодить для души, когда и как хочешь. Никому не должен и ничего за это не получаешь.

Однажды в моём Telegram-канале кто-то предложил идею создать игру и сделать по ней плейлист. Вспомнив старые WAP-игры, я начал размышлять над концепцией.

Сам YouTube канал
Сам YouTube канал

Идея и сеттинг игры

В игры я не играл уже три года. До этого у меня был огромный список в Steam и пара уникальных проектов с собственными точками входа. Меня увлекали песочницы, открытый мир, крафт и выживание, так что выбор был очевиден.

Игра будет MMORPG с открытым миром в стиле постапокалипсиса. Мир большой, с разными биомами и уровнями сложности. Игроки собирают ресурсы, крафтят, торгуют и развиваются. Настоящая песочница с выживанием и приключениями.

Не имея понятия, как все это назвать, и к какому сеттингу и жанру отнести, прочитав пару статей я просто решил это будет mmorpg с открытым миром и в стиле постапокалипсиса.

Жанр и тематика

Постапокалипсис: Мир после глобальной катастрофы, где выживание и борьба за ресурсы стали основным фокусом. Сейчас 2050 год, но выжившие откатились до технологий начала 2000-х. Им предстоит заново пройти путь цивилизации, восстанавливая мир в условиях открытого мира песочницы с элементами MMORPG.

  • Политические конфликты: Игрок оказывается в мире, полном войн за власть, интриг и предательств. Здесь выживает и правит миром тот, кто действует быстрее, смелее и опытнее других. Владение объектами и технологиями обеспечивает контроль над островом.

  • Детализированный мир:

    • Регионы: Разнообразные локации — от густых лесов до бескрайних пустынь — с уникальной флорой и фауной. В мире есть 9 уникальных биомов и 10 уровней сложности, влияющих на выживание.

    • Объекты: По всему острову разбросаны редкие объекты разной степени важности: заброшенные фабрики, фермы, военные склады, нефтяные вышки.

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

Элементы крафта и песочницы

  • Сбор ресурсов: Добыча более 60 видов ресурсов различной ценности, охота на животных и сбор растений для создания предметов. Ресурсы уникальны для каждого биома и доступны игрокам в зависимости от их уровня.

  • Строительство: Возведение домов, крепостей и мастерских. Игроки могут строить базы, промышленные объекты и защитные сооружения.

  • Торговля: Обмен товарами с NPC и другими игроками. Экономика включает динамическую систему валюты и ценообразования.

  • Исследование: Открытие новых земель и поиск тайн. Механика прокачки исследовательских дронов и роботов.

Динамичный и живой мир

  • События: Постоянно меняющийся мир, с войнами, стихийными бедствиями и вторжениями разбойников или роботов.

  • Экономика: Торговая система, влияющая на цены и доступность товаров. Отношения с торговцами, репутация и авторитет играют важную роль.

  • Социальные взаимодействия: Кланы, фракции, дружба или вражда с другими игроками. Совместные походы за редким лутом на ценные локации.

Предыстория Игрока
Игрок — один из немногих, кто пережил катастрофу и оказался на острове. Его прошлое остаётся неясным, но быстро становится очевидным, что остров полон ресурсов и возможностей для создания новой цивилизации.

Звучит красиво, а как все это написать в коде

Открытый мир, это значит карта, огромных (но не бесконечных) масштабов, с нее и начнем.

Приготовься погрузиться в увлекательное повествование о том, как текстовая карта превратилась в визуальное произведение искусства и техническое чудо в рамках одного проекта.

Игровая карта мира
Игровая карта мира

Начало пути
Вооружившись DALL·E, я отправился на поиски идеального вида игрового мира. Эксперименты с генерацией карты заняли несколько дней, но, несмотря на горящий энтузиазм, первые результаты оставили желать лучшего. Мир моей будущей игры требовал особенного внимания.

Первые шаги в создании карты
Я решил начать с создания каркаса карты: квадрат 1000 на 1000 ячеек, каждая из которых символизировала 100x100 метров реального мира. Таким образом, размер всего острова составлял 100 на 100 километров. Несколько скриптов было написано для генерации начальной структуры, но результаты оставляли много вопросов.

Генератор карт 1
import numpy as np
import sqlite3
from scipy import ndimage
import skimage.draw

# Определить размер карты и общее число ячеек
map_width, map_height = 1000, 1000
total_cells = map_width * map_height

# Define biome IDs
BIOME_IDS = {
    'mountains': 3,
    'tundra': 5,
    'forest': 1,
    'field': 7,
    'swamp': 6,
    'desert': 4,
    'river': 2,
    'volcanic': 8,
    'cave': 9,
    'tropical': 10
}

# Путь к файлу SQLite
sqlite_file_path = 'generated_world_map.sqlite3'

# Создание массива для отображения карты
world_map = np.zeros((map_height, map_width), dtype=int)

# Функция добавления рек на карту
def add_rivers(world_map, biome_id, num_rivers):
    for _ in range(num_rivers):
        # Отправная точка для реки
        start_x = np.random.randint(0, map_width)
        start_y = np.random.randint(0, map_height)
        # Определите конечную точку реки, гарантируя, что она останется в пределах границ карты
        end_x = np.clip(start_x + np.random.randint(-20, 20), 0, map_width - 1)
        end_y = np.clip(start_y + np.random.randint(-map_height // 2, map_height // 2), 0, map_height - 1)
        # Сгенерировать координаты речной линии и нарисовать их на карте
        rr, cc = skimage.draw.line(start_y, start_x, end_y, end_x)
        world_map[rr, cc] = biome_id
    return world_map

# Функция добавления случайных биомов на карту
def add_random_biomes(world_map, biome_id, num_areas, min_size, max_size):
    for _ in range(num_areas):
        # Центральная точка для данного района
        center_y, center_x = np.random.randint(0, map_height), np.random.randint(0, map_width)
        # Определите размер территории, чтобы она оставалась в пределах границ карты
        size = np.random.randint(min_size, max_size)
        # Сгенерировать координаты района и нарисовать их на карте
        rr, cc = skimage.draw.disk((center_y, center_x), size, shape=world_map.shape)
        world_map[rr, cc] = biome_id
    return world_map

# Присваивать биомы на основе координаты Y
for biome, biome_id in BIOME_IDS.items():
    if biome == 'mountains':
        world_map[:int(map_height * 0.2), :] = biome_id
    elif biome == 'tundra':
        world_map[int(map_height * 0.2):int(map_height * 0.3), :] = biome_id
    elif biome == 'forest':
        world_map[int(map_height * 0.3):int(map_height * 0.5), :] = biome_id
    elif biome == 'field':
        world_map[int(map_height * 0.5):int(map_height * 0.7), :] = biome_id
    elif biome == 'swamp':
        world_map[int(map_height * 0.7):int(map_height * 0.8), :] = biome_id
    elif biome == 'desert':
        world_map[int(map_height * 0.8):, :] = biome_id

# Добавить реки на карту
world_map = add_rivers(world_map, BIOME_IDS['river'], num_rivers=int(total_cells * 0.10))

# Добавить другие биомы в виде небольших патчей
world_map = add_random_biomes(world_map, BIOME_IDS['volcanic'], num_areas=int(total_cells * 0.02), min_size=1, max_size=3)
world_map = add_random_biomes(world_map, BIOME_IDS['cave'], num_areas=int(total_cells * 0.02), min_size=1, max_size=2)
world_map = add_random_biomes(world_map, BIOME_IDS['tropical'], num_areas=int(total_cells * 0.09), min_size=3, max_size=5)

# sigma=3: Контролирует степень размытия границ между биомами. Чем выше значение, тем плавнее переходы.
world_map = ndimage.gaussian_filter(world_map, sigma=2, order=0, mode='reflect')

# Преобразование карты мира в список кортежей для вставки
records = []
for y in range(map_height):
    for x in range(map_width):
        cell_number = y * map_width + x + 1
        biome_id = int(world_map[y, x])
        records.append((cell_number, x, y, biome_id))

# Создание или подключение к базе данных SQLite
conn = sqlite3.connect(sqlite_file_path)
cursor = conn.cursor()

# Создать таблицу games_map
cursor.execute('''
    CREATE TABLE IF NOT EXISTS games_map (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        cell_number INTEGER,
        coordinate_x INTEGER,
        coordinate_y INTEGER,
        biome_id INTEGER
    )
''')

# Insert records into the games_map table
cursor.executemany('INSERT INTO games_map (cell_number, coordinate_x, coordinate_y, biome_id) VALUES (?, ?, ?, ?)', records)

# Commit and close the database connection
conn.commit()
conn.close()

print("Генерация мировой карты завершена и сохранена для:", sqlite_file_path)

Генератор карт 2
import numpy as np
import sqlite3
from scipy import ndimage
import skimage.draw

# Define the map size
map_width, map_height = 1000, 1000
total_cells = map_width * map_height

# Define biome IDs
BIOME_IDS = {
    'mountains': 3,
    'tundra': 5,
    'forest': 1,
    'field': 7,
    'swamp': 6,
    'desert': 4,
    'river': 2,
    'volcanic': 8,
    'cave': 9,
    'tropical': 10
}

# Path to the SQLite file
sqlite_file_path = 'generated_world_map.sqlite3'

# Create a numpy array to represent the map
world_map = np.zeros((map_height, map_width), dtype=int)

# Присваивать биомы на основе У-координаты положения
biome_boundaries = {
    'mountains': 0.2,
    'tundra': 0.3,
    'forest': 1.7,
    'field': 1.7,
    'swamp': 0.8,
    'desert': 1.0
}

# Assign biomes based on the defined boundaries
previous_boundary = 0
for biome, boundary in biome_boundaries.items():
    biome_id = BIOME_IDS[biome]
    world_map[int(previous_boundary * map_height):int(boundary * map_height), :] = biome_id
    previous_boundary = boundary

# Add rivers, volcanic areas, caves, and tropical regions as smaller patches
# Rivers
num_rivers = int(total_cells * 0.05)  # Сокращение числа рек для контроля за охватом рек
for _ in range(num_rivers):
        start_x = np.random.randint(0, map_width)
        start_y = np.random.randint(0, map_height)
        end_x = start_x + np.random.randint(-20, 20)
        end_y = start_y + np.random.randint(-100, 100)
        rr, cc = skimage.draw.line(start_y, start_x, end_y, end_x)
        # Убедимся, что координаты находятся в пределах карты
        valid = (rr >= 0) & (rr < map_height) & (cc >= 0) & (cc < map_width)
        # Обновим массивы координат так, чтобы они соответствовали длине
        rr, cc = rr[valid], cc[valid]
        world_map[rr, cc] = biome_id

# Volcanic areas
num_volcanic = int(total_cells * 0.001)  # Reduced for less coverage
for _ in range(num_volcanic):
    center_y, center_x = np.random.randint(0, map_height), np.random.randint(0, map_width)
    radius = np.random.randint(1, 2)  # Smaller radius for volcanic areas
    rr, cc = skimage.draw.disk((center_y, center_x), radius, shape=world_map.shape)
    world_map[rr, cc] = BIOME_IDS['volcanic']

# Caves
num_caves = int(total_cells * 0.001)  # Reduced for less coverage
for _ in range(num_caves):
    cave_y, cave_x = np.random.randint(0, map_height), np.random.randint(0, map_width)
    world_map[cave_y, cave_x] = BIOME_IDS['cave']

# Tropical regions
num_tropical = int(total_cells * 0.02)  # Adjusted for balanced coverage
for _ in range(num_tropical):
    center_y, center_x = np.random.randint(0, map_height), np.random.randint(0, map_width)
    radius = np.random.randint(3, 5)  # Radius for tropical regions
    rr, cc = skimage.draw.disk((center_y, center_x), radius, shape=world_map.shape)
    world_map[rr, cc] = BIOME_IDS['tropical']

# Smooth transitions between biomes using a Gaussian filter
world_map = ndimage.gaussian_filter(world_map, sigma=1, order=0, mode='reflect')

# Convert the world map into a list of tuples for SQLite insertion
records = []
for y in range(map_height):
    for x in range(map_width):
        cell_number = y * map_width + x + 1
        biome_id = int(world_map[y, x])
        records.append((cell_number, x, y, biome_id))

# Create or connect to SQLite database
conn = sqlite3.connect(sqlite_file_path)
cursor = conn.cursor()

# Create the games_map table
cursor.execute('''
    CREATE TABLE IF NOT EXISTS games_map (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        cell_number INTEGER,
        coordinate_x INTEGER,
        coordinate_y INTEGER,
        biome_id INTEGER
    )
''')

# Insert records into the games_map table
cursor.executemany('INSERT INTO games_map (cell_number, coordinate_x, coordinate_y, biome_id) VALUES (?, ?, ?, ?)', records)

# Commit and close the database connection
conn.commit()
conn.close()

print("World map generation completed and saved to:", sqlite_file_path)

Генератор карт 3
import numpy as np
import sqlite3
from scipy import ndimage
import skimage.draw

# Определение размера карты
map_width, map_height = 1000, 1000
world_map = np.zeros((map_height, map_width), dtype=int)

# Определение ID биомов
BIOME_IDS = {
    'forest': 1,
    'rivers': 2,
    'mountains': 3,
    'desert': 4,
    'tundra': 5,
    'swamp': 6,
    'field': 7,
    'volcanic': 8,
    'cave': 9,
    'tropical': 10
}

# Вероятности встречаемости биомов в процентах
biome_occurrence = {
    'forest': 35.0,
    'rivers': 7.0,
    'mountains': 3.5,
    'desert': 6.0,
    'tundra': 6.0,
    'swamp': 3.0,
    'field': 30.5,
    'volcanic': 2.0,
    'cave': 1.0,
    'tropical': 6.0,
}

def fill_biomes(world_map, biome_occurrence, BIOME_IDS):
    total_cells = map_width * map_height

    for biome, occurrence in biome_occurrence.items():
        num_cells = int((occurrence / 100.0) * total_cells)
        available_cells = np.where(world_map == 0)
        if len(available_cells[0]) < num_cells:
            num_cells = len(available_cells[0])
        indices = np.random.choice(range(len(available_cells[0])), size=num_cells, replace=False)
        world_map[available_cells[0][indices], available_cells[1][indices]] = BIOME_IDS[biome]

    return world_map

world_map = fill_biomes(world_map, biome_occurrence, BIOME_IDS)

# Дополнительные функции для генерации специфических биомов (реки, вулканические зоны, пещеры, тропические джунгли)
def add_rivers(world_map, num_rivers, BIOME_IDS):
    for _ in range(num_rivers):
        start_x = np.random.randint(0, map_width)
        start_y = np.random.randint(0, map_height)
        end_x = start_x + np.random.randint(-20, 20)
        end_y = start_y + np.random.randint(-100, 100)
        rr, cc = skimage.draw.line(start_y, start_x, end_y, end_x)
        valid = (rr >= 0) & (rr < map_height) & (cc >= 0) & (cc < map_width)
        world_map[rr[valid], cc[valid]] = BIOME_IDS['rivers']

def add_special_biomes(world_map, biome_type, num_areas, radius_range, BIOME_IDS):
    for _ in range(num_areas):
        center_y, center_x = np.random.randint(0, map_height), np.random.randint(0, map_width)
        radius = np.random.randint(*radius_range)
        rr, cc = skimage.draw.disk((center_y, center_x), radius, shape=world_map.shape)
        world_map[rr, cc] = BIOME_IDS[biome_type]

add_rivers(world_map, num_rivers=int((biome_occurrence['rivers'] / 100.0) * map_width * map_height), BIOME_IDS=BIOME_IDS)
add_special_biomes(world_map, 'volcanic', int((biome_occurrence['volcanic'] / 100.0) * map_width * map_height), (1, 3), BIOME_IDS)
add_special_biomes(world_map, 'cave', int((biome_occurrence['cave'] / 100.0) * map_width * map_height), (0, 1), BIOME_IDS)  # Пещеры как точки
add_special_biomes(world_map, 'tropical', int((biome_occurrence['tropical'] / 100.0) * map_width * map_height), (3, 5), BIOME_IDS)

world_map = ndimage.gaussian_filter(world_map, sigma=1, order=0, mode='reflect')

# Подготовка данных для вставки в SQLite
records = []
for y in range(map_height):
    for x in range(map_width):
        cell_number = y * map_width + x + 1
        biome_id = int(world_map[y, x])
        records.append((cell_number, x, y, biome_id))

# Подключение к базе данных SQLite и создание таблицы
sqlite_file_path = 'generated_world_map.sqlite3'
conn = sqlite3.connect(sqlite_file_path)
cursor = conn.cursor()

cursor.execute('''
CREATE TABLE IF NOT EXISTS games_map (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    cell_number INTEGER,
    coordinate_x INTEGER,
    coordinate_y INTEGER,
    biome_id INTEGER
)
''')

# Вставка записей в таблицу
cursor.executemany('INSERT INTO games_map (cell_number, coordinate_x, coordinate_y, biome_id) VALUES (?, ?, ?, ?)', records)

# Фиксация изменений и закрытие подключения к базе данных
conn.commit()
conn.close()

print("Генерация игрового мира завершена и сохранена в файл:", sqlite_file_path)

Пиксельный арт в действии
Устав от неудачных попыток и руководствуясь идеей обратного процесса, я обратился к Photoshop. Там я превратил скачанную карту в попиксельное искусство, вручную, клик за кликом. В итоге, картинка, которую вы видели ранее, обрела новое, более детализированное измерение.

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

С MySQL в изображение
import mysql.connector
from PIL import Image, ImageDraw

# Параметры подключения к базе данных
config = {
    'user': 'root',
    'password': '',
    'host': '127.0.0.1',
    'database': 'mmorpg',
    'raise_on_warnings': True
}

def hex_to_rgb(hex_color):
    """Преобразование шестнадцатеричного цвета в кортеж RGB."""
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

# Подключение к базе данных
cnx = mysql.connector.connect(**config)
cursor = cnx.cursor()

# Выполнение запроса на выборку данных для карты
cursor.execute("SELECT x, y, color_hex FROM images")
map_data = cursor.fetchall()

# Выполнение запроса для выборки уникальных биомов и их цветов
cursor.execute("SELECT DISTINCT biome_id, color_hex FROM images ORDER BY biome_id ASC")
biomes_info = cursor.fetchall()

# Определение размера блока информации о биомах
info_height = ((len(biomes_info) - 1) // 3 + 1) * 50 + 20
total_height = 1000 + info_height

# Создание нового изображения с дополнительным местом для информации о биомах
img = Image.new('RGB', (1000, total_height), "black")
pixels = img.load()

# Заполнение карты данными из базы
for (x, y, color_hex) in map_data:
    pixels[x, y] = hex_to_rgb(color_hex)

# Добавление информации о биомах
draw = ImageDraw.Draw(img)
for i, (biome_id, color_hex) in enumerate(biomes_info):
    x = (i % 3) * 330 + 10
    y = 1000 + (i // 3) * 50
    color = hex_to_rgb(color_hex)
    draw.rectangle([x, y, x + 10, y + 10], fill=color)
    text = f"Biom: {biome_id} Color: {color_hex}"
    draw.text((x + 20, y), text, fill="white")

# Сохранение изображения
img.save("game_map_with_info11.png")

# Закрытие соединения с базой данных
cursor.close()
cnx.close()

Работы проделано много, а результат меня не устраивает, решил я пойти снова другим путем, от обратного. Взял скачанную красивую карту и пошел в Photoshop делать с картинки попиксельный арт. Не стану описывать сколько раз я нажал мышкой чтобы из одного получить другое, но в результате у меня появилась вот такая картинка:

Карта из 9 цветов
Карта из 9 цветов

Вы можете пролистать выше, где уже читали и сравнить ту карту и эту. Для чего она нужна была, а вот для этого:

Из картинки в SQL
from PIL import Image
import pymysql

def connect_to_db():
    return pymysql.connect(host='localhost', user='root', password='', db='rpg', charset='utf8mb4')

def rgb_to_hex(rgb):
    return '#' + ''.join(['{:02x}'.format(x) for x in rgb])

image_path = "Bioms_map.png"

connection = connect_to_db()
cursor = connection.cursor()

cursor.execute("DROP TABLE IF EXISTS images;")
cursor.execute("""
    CREATE TABLE IF NOT EXISTS images (
        id INT PRIMARY KEY AUTO_INCREMENT,
        x INT,
        y INT,
        color_hex VARCHAR(7),
        biome_id INT
    );
""")

BIOMES = {}

image = Image.open(image_path)
width, height = image.size
pixel_counter = 0
unique_id = 1

for x in range(width):
    for y in range(height):
        pixel_counter += 1
        if pixel_counter % 50000 == 0:
            print(f"Обработано {pixel_counter} пикселей.")

        rgb = image.getpixel((x, y))

        if rgb not in BIOMES:
            BIOMES[rgb] = unique_id
            unique_id += 1

        biome_id = BIOMES[rgb]
        color_hex = rgb_to_hex(rgb)

        cursor.execute("INSERT INTO images (x, y, color_hex, biome_id) VALUES (%s, %s, %s, %s)", (x, y, color_hex, biome_id))

connection.commit()

print(f"Обработка пикселей завершена. Всего обработано пикселей: {pixel_counter}. Найдено уникальных биомов: {len(BIOMES)}.")

cursor.close()
connection.close()

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

А далее уже пошла логика самой игры. Но давай так, если тебе интересно читать именно в таком формате 50% описаний и 50% технической части. Тогда я смело продолжу свое повествование далее.

А что с игрой сейчас?

На данный момент игра находится в стадии альфа-тестирования, с планами на скорый переход в бета-версию и, в конечном итоге, к полноценному релизу. Всё пишется на стеке с фронтом на телеграм-боте и бэкендом на PHP8 (CodeIgniter 4.4.6), а динамичная карты — это результат работы моих скриптов на Python и базы данных MySQL.

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

Заключение
Спасибо, что уделили время чтению моего рассказа. Надеюсь, он вдохновит вас так же, как и меня вдохновил процесс создания мира для моей игры. Всем мира и творческих успехов!

А ваши комментарии к статье дадут мне понимание писать далее о развитии игры или похоронить в себе писателя, так и не родив его!

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+7
Комментарии0

Публикации

Работа

Data Scientist
47 вакансий
PHP программист
71 вакансия

Ближайшие события