惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
G
Google Developers Blog
H
Help Net Security
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Proofpoint News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
GbyAI
GbyAI
D
Docker
Hugging Face - Blog
Hugging Face - Blog
I
InfoQ
博客园 - 司徒正美
Last Week in AI
Last Week in AI
Microsoft Security Blog
Microsoft Security Blog
美团技术团队
Stack Overflow Blog
Stack Overflow Blog
M
MIT News - Artificial intelligence

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Разработка карточной игры в Game Maker
butuzoff_ya · 2026-05-05 · via Все публикации подряд на Хабре

Простой

8 мин

8.1K

Всем привет! Меня зовут Яков, и я разработчик игр. Возможно, вы играли в мои предыдущие проекты: Dom Rusalok, Loretta и Anoxia Station. Сейчас я заканчиваю работу над новой игрой — Bonereader. И, поскольку я много думаю сейчас о балансе игры, я решил поделиться опытом, в свободной форме порассуждать обо разработке, и дать, надеюсь, полезные советы, которые помогут другим разрабам.

Костяной барон

Костяной барон

Bonereader — карточная игра с механикой покера на костях. Вас поджаривают на электрическом стуле, и вы оказываетесь в Чистилище, вдохновлённом романами Карлоса Кастанеды и Кормака Маккарти, где вынуждены играть в кости с разными духами за призрачный шанс на перерождение.

В основе механики лежит костяной покер (yatzy). В детстве мы с братом часто играли в одну из его вариаций. Когда работа над Anoxia Station подходила к концу, я, как и многие, увлёкся Balatro. А мне за короткое время нужно было придумать концепт для новой игры. И я вспомнил про покер «на костях».

Вот такое поле мы с братом рисовали, когда играли.

Вот такое поле мы с братом рисовали, когда играли.

Я не программист, а врач по образованию. Несмотря на то что занимаюсь этим уже лет восемь. Моя главная проблема в том, что у меня нет систематических знаний и я не знаю ни одного языка программирования, кроме GML. Если я нахожу элегантное решение какой‑то проблемы в чужом проекте на GitHub, я, конечно же, «заимствую» его, но всегда существенно переписываю. Хотя сейчас в коде для меня нет задачи, которая могла бы поставить меня в тупик. В крайнем случае, я всегда могу обратиться за советом к бесплатной версии Claude.

Пожалуй, единственное, чего я не освоил — сетевой код, но лишь потому, что такая задача никогда передо мной и не стояла. В остальном есть лишь два ограничения: невозможность работать в 3D и фантазия.

Код

По сути, в моём проекте есть три главных объекта: obj_dice, obj_combination и obj_controller, который отвечает за global.game_state (то есть за всё происходящее в игре).

/// [GAME STATES]
///  "start_round"
///  "rolling"
///  "in_play"
///  "tally_score"
///  "finalize_score"
///  "menu_screen"
///  "in_shop"
///  "in_hub"

Вы можете выбрать одну из стартовых колод, содержащую несколько карт‑комбинаций и россыпь разнообразных костей. Ваша цель: набрать необходимое количество очков за ограниченное количество попыток. Всё просто, верно? Ну, не совсем:)

Экран с выбором исходной колоды

Экран с выбором исходной колоды

После короткой беседы с тем или иным духом вам открывается магазин, в котором вы можете приобрести предметы и кости, продать или купить карты‑комбинации. У каждого монстра есть «правило дома» — специальное условие, которое необходимо выполнять.

Реролл магазина такой дорогой из-за выбранной колоды.

Реролл магазина такой дорогой из‑за выбранной колоды.

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

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

С каждым противником вы играете четыре партии, последняя проходит под случайным «Знамением» — по сути, дебаффом. Наконец, на каждый ход или бросок на случайную карту из вашей руки накладывается случайное: есть как выгодные, так и не очень, так и что‑то между.

Игровой стол у каждого противника тоже свой собственный!

Игровой стол у каждого противника тоже свой собственный!

Все виды костей (пока что).

Все виды костей (пока что).

Математика подсчёта примерно такая:

function _check_card_effect_to_final_score(add_to_score_all, card_level) {
    var final_score = add_to_score_all; 
    var temp_mult = 1;
    var temp_bonus = 0;
    
    var basepips = ceil(card_base + global.dice_slot1 + global.dice_slot2 
    + global.dice_slot3 + global.dice_slot4 + global.dice_slot5);

    var base_score_with_pips = basepips;
    var item_bonus_score = global.item_bonus_score;
    var item_bonus_mult = global.item_bonus_mult;

Затем мы накладываем проклятие карты, если оно есть:

if (ENEMY_EFFECT_CARD_HAND_CURSE) {
    final_score = max(0, final_score - (100 * eff_power));
}

Затем проверяем татуировки — перманентные баффы, награды за повышение уровня. Ну и наконец, считаем результат:

if (global.GET_BONUS_TATTOO_ODD_BONUS) {
    var countodd = 0;
    var dice = [global.dice_slot1, global.dice_slot2, 
    global.dice_slot3, global.dice_slot4, 
    global.dice_slot5];

    for(var i=0; i<5; i++) { if(dice[i] % 2 == 1) countodd += tattoo_odd_bonus; }
    temp_mult += countodd; 
}

if (global.GET_BONUS_TATTOO_EVEN_BONUS) {
    var counteven = 0;
    var dice = [global.dice_slot1, global.dice_slot2, 
    global.dice_slot3, 
    global.dice_slot4, global.dice_slot5];

    for(var i=0; i<5; i++) { if(dice[i] != 0 && dice[i] % 2 == 0) 
    counteven += tattoo_even_bonus; }
    
    temp_mult += counteven;
}

// --- ФИНАЛЬНЫЙ РАСЧЕТ ---
var result = (final_score + temp_bonus) * temp_mult;

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

Также в игре есть разнообразные предметы.

Также в игре есть разнообразные предметы.

Предметы работают по принципу:

if (sprite_index == spr_item_golden_tooth) 
{
    audio_play_sound(snd_item_golden_thoot, 0, false, global.SOUNDS_VOLUME_MAX)
    add_bonus_per_dice_value(6, 10); 
    ACTIVATED = true
}

Где add_bonus_per_dice_value ():

function add_bonus_per_dice_value(dice_value, bonus_per_die) {
    var count = 0;
    if (global.dice_slot1 == dice_value) count++;
    if (global.dice_slot2 == dice_value) count++;
    if (global.dice_slot3 == dice_value) count++;
    if (global.dice_slot4 == dice_value) count++;
    if (global.dice_slot5 == dice_value) count++;
    
    global.item_bonus_score += count * bonus_per_die;
}

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

Всех монстров художница сначала рисует от руки, а затем уже создает digital-версию.

Всех монстров художница сначала рисует от руки, а затем уже создает digital‑версию.

Баланс

Поскольку в проекте я работаю один, то не веду таблиц (кроме локализации) и документации. У меня есть файл _Balance (раньше в GM это называлось script, но я не хочу никого путать, и когда‑то он мог нести в себе только одну функцию, но сейчас туда можно записывать сколько угодно функций), куда я в виде #macro записал основные показатели: например, базовое количество очков, которое даёт карта, её цена покупки/продажи и так далее.

В игре два режима: сюжетный и роглайк. Если сюжетный более выверен с геймдизайнерской точки зрения — враги идут по очереди, мы примерно представляем себе кривую прогрессии, определяем пул бонусов для того или иного уровня игрока, — то в роглайт‑режиме многое зависит от рандома.

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

Вот как идёт подсчёт требуемых очков очки для противника. Для своего удобства я использовал термин «анте», который взял из Balatro и который и в самом Balatro, на самом деле, означает не своё первое смысловое значение, но мы это опустим.

function calculate_roguelite_level_settings () {
    
    // Текущий анте (противник) - от 0 до 5
    var _current_ante = array_length(global.DEFEATED_ENEMIES_CASINO);
    
    // Базовые очки для каждого раунда (уровня)
    var _base_points = 0;
    
    switch (global.level) {
        case 0: _base_points = LEVEL_ONE_TARGET_POINT;   break;  
        case 1: _base_points = LEVEL_TWO_TARGET_POINT;   break;  
        case 2: _base_points = LEVEL_THREE_TARGET_POINT; break;  
        case 3: _base_points = LEVEL_BOSS_TARGET_POINT;  break;  
        case 4: _base_points = LEVEL_BOSS_TARGET_POINT;  break;  
    }
    
    var _ante_multipliers = [
        1.0,   
        3.2,   
        6.0,   
        10.0,  
        14.0,  
    ];
	
    // Если антов больше 6, продолжаем увеличивать множитель
    var _ante_multiplier = 1.0;
    if (_current_ante < array_length(_ante_multipliers)) {
        _ante_multiplier = _ante_multipliers[_current_ante];
    } else {
        // Для антов больше 6: продолжаем экспоненциальный рост
        _ante_multiplier = _ante_multipliers[array_length(_ante_multipliers) - 1] * 
                          power(1.6, _current_ante - array_length(_ante_multipliers) + 1);
    }
    
    // Рассчитываем финальные очки
    global.target_score_base = floor(_base_points * _ante_multiplier);
    
    // Применяем бонус татуировки (-15% к требуемым очкам)
    if (global.GET_BONUS_TATTOO_MINUS_10_TARGET_POINTS) {
        global.target_score_base = floor(global.target_score_base * 0.85);
    }
    
    global.target_score = global.target_score_base;
    
    if (global.BOSS_DEBUFF == "HALF_COMBO" && (global.level == 3))
    {
        global.target_score = round(global.target_score/3)
    }

    // Назначаем "Знамение" для финального раунда
    if (global.level == 0) {
        SET_UP_THE_BOSS_EFFECT(); 
    }
}

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

В Bonreader же я гораздо больше потратил времени на создание «ощущения» от карт и кубиков. Их анимаций, наведения, шейдеров для проклятий.

Моя идея в том, чтобы игрок мог выбирать, как двигаться к огромным числовым значениям.  Выборов и синергий масса.

Моя идея в том, чтобы игрок мог выбирать, как двигаться к огромным числовым значениям. Выборов и синергий масса.

Советы разработчикам

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

Если вы тоже хотите создать игру на GM, вот немного сермяжной правды:

  1. Используйте минимум шрифтов, при этом по возможности заранее подыскивайте аналоги в CJK‑шрифтах, чтобы стиль текста и дизайна игры был похожим для всех игроков.

  2. Продумывайте текстурные и аудиогруппы заранее. Вообще, продумывайте архитектуру проекта (систему сохранений, контроллер). Это сэкономит много времени при создании уровней. Учитывайте ограничения выбранного движка и свои навыки ещё на этапе планирования.

  3. Создавать игры с поддержкой геймпада выгоднее.

  4. Не создавайте «hard‑сoded» текст. Используйте.csv‑таблицы. Если не знаете, как это сделать, обязательно научитесь.

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

  6. Не используйте внутри игры видео. Кодеки — это отдельная головная боль, которая вряд ли стоит результата.

  7. По возможности используйте меньше шейдеров. Особенно если вы не пишете их сами, а покупаете или находите и внедряете. Нет гарантии, что шейдер на одной платформе будет работать так же хорошо, как на другой. Keep it simple.

  8. Чаще рассказывайте о своей игре. Общайтесь с аудиторией, делитесь процессом и идеями.

  9. Я бы советовал держать код для API‑разных магазинов в пределах одного участка кода. Ну, к примеру, ачивки я делаю так:

// Achievement #1
function  Achievement_Unlock_1() 
{
	if (IS_DEMO_BUILD) {exit}
	
	switch (os_type)
	{
		case os_windows:
			
			switch(WINDOWS_TARGET_PLATFORM)
			{
				case WindowsTargetPlatforms.Steam:
				
					if !steam_get_achievement ("ach_01_win_shaman") 
                    steam_set_achievement ("ach_01_win_shaman");
					
				break;
				
				case WindowsTargetPlatforms.GOG:
				
					if !instance_exists(obj_gog_ach_1) 
                    {instance_create_depth(x, y, -999, obj_gog_ach_1)}
					
				break;
				
				case WindowsTargetPlatforms.EOS:
	
					//
					
				break;
			}
    		break;
		case os_ps4:
			//
		break;
		case os_ps5:
			//
		break;
		case os_gdk:
			// 		
		break;
	}
}

В апреле вышла бесплатная демка Bonereader, в которую я приглашаю вас сыграть и поделиться со мной своими мыслями! Мы находимся на финишной прямой. Мне осталось дописать финальные тексты, перевести их на японский и два вида китайского, вставить звуки в концовки, которых в игре будет несколько, к слову. А Даше, моему партнеру и художнице, осталось закончить артбук и несколько больших и не очень артов.

Но сейчас у меня ещё есть время, а глаз, откровенно говоря, замылен. Мне не хватает фидбека от реальных игроков и любителей жанра, потому с нетерпением жду ваши отзывы. А написать вы можете в наш тг или вк, да и просто оставить отзыв в Steam, GOG или Itch.

И спасибо за внимание!:)