Разработка игровых интерфейсов в среде 1С — задача нетипичная, но крайне увлекательная. Она позволяет программисту выйти за рамки привычных учетных задач и глубже понять механизмы работы управляемых форм, клиент-серверного взаимодействия и оптимизации отрисовки интерфейса. В данной статье мы подробно рассмотрим, как реализовать интерактивную сетку (игровое поле для "Морского боя", "Сапера" или "Крестиков-ноликов") тремя разными способами: при помощи динамических кнопок, табличного документа и поля HTML-документа. Мы проанализируем преимущества и недостатки каждого подхода, разберем типичные ошибки и напишем готовый к использованию код.
Первая идея, которая приходит в голову начинающему разработчику, — создать сетку игрового поля из обычных кнопок 1С. Однако здесь нас подстерегает несколько технических сложностей, начиная от производительности платформы и заканчивая особенностями форматирования чисел.
Разберем по шагам типичную ошибку при программной генерации имен элементов. Рассмотрим популярный цикл генерации кнопок:
Для К = 0 По 100 Цикл
кмд = "НД" + К;
// Код добавления команды и кнопки...
КонецЦикла;
В чем заключается критическая ошибка этого кода? По умолчанию при неявном преобразовании числа в строку платформа 1С применяет региональные настройки информационной базы. Как только счетчик К достигнет значения 1000, строка преобразуется с использованием неразрывного пробела в качестве разделителя триад (например, "НД1 000"). Поскольку имена элементов управляемой формы и команд 1С не могут содержать пробелов и спецсимволов, платформа немедленно вызовет критическую ошибку выполнения.
Для предотвращения этой проблемы мы должны явно отключать группировку чисел с помощью функции Формат(). Исправленный вариант формирования имени команды выглядит следующим образом:
ИмяКоманды = "НД" + Формат(К, "ЧГ=0");
Теперь выясним, как правильно сгруппировать динамические кнопки на форме, чтобы они образовали ровную сетку (например, 10 на 10 клеток), как задать им единый обработчик событий и как изменять их свойства.
Для позиционирования элементов по координатам нам потребуется создать иерархическую структуру из групп формы. Мы создадим одну родительскую группу с вертикальным распределением (это будет наше игровое поле), а внутри нее в цикле будем создавать горизонтальные группы (строки поля), в которые уже поместим конечные кнопки (колонки).
Посмотрим на пример полной реализации программного создания игрового поля на сервере:
&НаСервере
Процедура СоздатьИгровоеПолеНаКнопках()
РазмерПоля = 10; // Сетка 10х10
// 1. Создаем корневую группу игрового поля с вертикальной группировкой
ГруппаПоле = Элементы.Добавить("ИгровоеПолеКнопки", Тип("ГруппаФормы"));
ГруппаПоле.Вид = ВидГруппыФормы.ОбычнаяГруппа;
ГруппаПоле.Группировка = ГруппировкаПодчиненныхЭлементовФормы.Вертикальная;
ГруппаПоле.ОтображатьЗаголовок = Ложь;
// Создаем единую команду для всех кнопок
Для СтрокаIdx = 1 По РазмерПоля Цикл
// 2. Создаем горизонтальную группу для каждой строки
ИмяСтроки = "Строка_" + Формат(СтрокаIdx, "ЧГ=0");
ГруппаСтрока = Элементы.Добавить(ИмяСтроки, Тип("ГруппаФормы"), ГруппаПоле);
ГруппаСтрока.Вид = ВидГруппыФормы.ОбычнаяГруппа;
ГруппаСтрока.Группировка = ГруппировкаПодчиненныхЭлементовФормы.ГоризонтальнаяЕслиВозможно;
ГруппаСтрока.ОтображатьЗаголовок = Ложь;
Для КолонкаIdx = 1 По РазмерПоля Цикл
ИндексКоординат = Формат(СтрокаIdx, "ЧГ=0") + "_" + Формат(КолонкаIdx, "ЧГ=0");
ИмяДействия = "Клик_" + ИндексКоординат;
// 3. Создаем команду формы
НоваяКоманда = Команды.Добавить(ИмяДействия);
НоваяКоманда.Действие = "ОбработчикНажатияКнопкиПоля";
НоваяКоманда.Заголовок = " "; // Изначально кнопка пустая
// 4. Добавляем кнопку на форму в соответствующую строку
НоваяКнопка = Элементы.Добавить("Кнопка_" + ИндексКоординат, Тип("КнопкаФормы"), ГруппаСтрока);
НоваяКнопка.ИмяКоманды = ИмяДействия;
НоваяКнопка.Ширина = 3; // Ограничиваем ширину для получения квадратных кнопок
НоваяКнопка.Высота = 1;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Обратите внимание, что мы привязали абсолютно все динамические команды к одной и той же процедуре-обработчику ОбработчикНажатияКнопкиПоля. Создавать сотни отдельных процедур не имеет смысла. Проанализируем, как определить, какая именно кнопка была нажата, и как изменить ее состояние:
&НаКлиенте
Процедура ОбработчикНажатияКнопкиПоля(Команда)
// Извлекаем имя команды, например "Клик_5_3"
ИмяКоманды = Команда.Имя;
// Парсим координаты клика
ЧастиИмени = СтрРазделить(ИмяКоманды, "_");
СтрокаКлика = Число(ЧастиИмени[1]);
КолонкаКлика = Число(ЧастиИмени[2]);
// Выполняем игровую логику на клиенте или сервере
РезультатВыстрела = ВыполнитьХодИгры(СтрокаКлика, КолонкаКлика);
// Меняем текст на кнопке. На УФ заголовок кнопки привязан к команде!
ЭтаФорма.Команды[ИмяКоманды].Заголовок = РезультатВыстрела; // Например, "X" или "O"
КонецПроцедуры
Важное предупреждение: Генерация интерфейса на кнопках крайне ресурсоемкая. При создании поля размером более 15х15 клеток (225 кнопок и 225 команд соответственно) время открытия формы на клиенте резко возрастает, а передача контекста формы между клиентом и сервером начинает заметно "зависать". Поэтому для серьезных интерактивных проектов данный метод использовать не рекомендуется.
Использование объекта ТабличныйДокумент (внедренного прямо в управляемую форму) — это классический, невероятно быстрый и производительный способ создания сеточных игр "из коробки" в 1С. Табличный документ легко выдерживает сетки размером хоть 100х100 ячеек без малейших задержек при отрисовке.
Рассмотрим по шагам, как развернуть игровое поле на базе табличного документа:
ТабличныйДокумент (назовем его ПолеБоя).Теперь напишем процедуру инициализации нашего игрового поля на сервере:
&НаСервере
Процедура ИнициализироватьТабличноеПоле()
ТабДок = РеквизитФормыВЗначение("ПолеБоя");
ТабДок.Очистить();
РазмерПоля = 10;
// Настраиваем размеры колонок и строк, чтобы ячейки стали квадратными
Для НомерКолонки = 1 По РазмерПоля Цикл
ТабДок.Область(, НомерКолонки).ШиринаКолонки = 4;
КонецЦикла;
Для НомерСтроки = 1 По РазмерПоля Цикл
ТабДок.Область(НомерСтроки, ).ВысотаСтроки = 25;
КонецЦикла;
// Оформляем сетку
Для Стр = 1 По РазмерПоля Цикл
Для Кол = 1 По РазмерПоля Цикл
ТекущаяЯчейка = ТабДок.Область(Стр, Кол, Стр, Кол);
ТекущаяЯчейка.Рамка = Новый Рамка(ТипРамкиТабличногоДокумента.Сплошная, 1);
ТекущаяЯчейка.ЦветФона = WebЦвета.Сизый; // Исходный цвет "воды"
ТекущаяЯчейка.Текст = " ";
КонецЦикла;
КонецЦикла;
ЗначениеВРеквизитФормы(ТабДок, "ПолеБоя");
КонецПроцедуры
Чтобы обрабатывать действия игрока, нам больше не нужны сотни кнопок. Мы будем использовать стандартное клиентское событие элемента формы Выбор (которое срабатывает при двойном клике или нажатии Enter на ячейке табличного документа). Создадим обработчик этого события:
&НаКлиенте
Процедура ПолеБояВыбор(Элемент, Область, СтандартнаяОбработка)
// Отменяем стандартное поведение (вход в режим редактирования текста ячейки)
СтандартнаяОбработка = Ложь;
// Получаем точные координаты клика пользователя
СтрокаВыбора = Область.Верх;
КолонкаВыбора = Область.Лево;
// Проверяем, что кликнули в пределах игрового поля 10х10
Если СтрокаВыбора > 10 Или КолонкаВыбора > 10 Тогда
Возврат;
КонецЕсли;
// Выполняем логику выстрела
ОбработатьВыстрелНаСервере(СтрокаВыбора, КолонкаВыбора);
КонецПроцедуры
А на сервере мы можем мгновенно перекрасить выбранную ячейку и записать в нее результат хода:
&НаСервере
Процедура ОбработатьВыстрелНаСервере(СтрокаВыбора, КолонкаВыбора)
ТабДок = РеквизитФормыВЗначение("ПолеБоя");
Ячейка = ТабДок.Область(СтрокаВыбора, КолонкаВыбора);
// Пример логики: если там был корабль — красим в красный, если пусто — в белый
Если ЭтоПопадание(СтрокаВыбора, КолонкаВыбора) Тогда
Ячейка.ЦветФона = WebЦвета.Красный;
Ячейка.Текст = "X";
Ячейка.Шрифт = Новый Шрифт(,,Истина); // Жирный шрифт
Иначе
Ячейка.ЦветФона = WebЦвета.Белый;
Ячейка.Текст = "•";
КонецЕсли;
ЗначениеВРеквизитФормы(ТабДок, "ПолеБоя");
КонецПроцедуры
Такой подход работает молниеносно. К тому же табличный документ позволяет легко выводить картинки поверх ячеек, масштабировать поле и использовать условное оформление.
Если вы хотите создать действительно красивую, современную игру с плавной CSS-анимацией взрывов, подсветкой клеток при наведении мыши (эффект Hover) и звуковым сопровождением, лучшим решением станет использование элемента формы Поле HTML документа.
Внутри управляемой формы 1С фактически работает полноценный браузерный движок (Webkit или Chromium в зависимости от версии платформы и операционной системы). Это позволяет нам использовать современную HTML5/CSS3 верстку.
Проанализируем архитектуру этого метода:
Строка неограниченной длины (назовем его HTMLКодПоля).Сгенерируем базовую разметку игрового поля на сервере при создании формы:
&НаСервере
Процедура СгенерироватьHTMLПоле()
РазмерПоля = 10;
ТекстHTML = "<html><head><style>
|body { margin: 0; padding: 10px; background-color: #f0f0f0; font-family: sans-serif; }
|.grid-container { display: grid; grid-template-columns: repeat(" + РазмерПоля + ", 40px); gap: 2px; }
|.cell { width: 40px; height: 40px; background-color: #4a90e2; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; transition: background-color 0.2s; }
|.cell:hover { background-color: #357abd; }
|.hit { background-color: #d0021b !important; }
|.miss { background-color: #ffffff !important; color: #4a4a4a !important; }
|</style></head><body>";
ТекстHTML = ТекстHTML + "<div class='grid-container'>";
Для Стр = 1 По РазмерПоля Цикл
Для Кол = 1 По РазмерПоля Цикл
Идентификатор = "cell_" + Формат(Стр, "ЧГ=0") + "_" + Формат(Кол, "ЧГ=0");
ТекстHTML = ТекстHTML + "<div id='" + Идентификатор + "' class='cell' onclick='cellClick(this.id)'></div>";
КонецЦикла;
КонецЦикла;
ТекстHTML = ТекстHTML + "</div>";
// Добавляем JS-скрипт обратной связи с платформой 1С
ТекстHTML = ТекстHTML + "<script>
|function cellClick(cellId) {
| // Передаем событие клика обратно в 1С через схему переходов
| window.location.href = '1c://shot?id=' + cellId;
|}
|</script>";
ТекстHTML = ТекстHTML + "</body></html>";
HTMLКодПоля = ТекстHTML;
КонецПроцедуры
Как организовать обратную связь из HTML в код 1С? Для этого мы использовали перенаправление window.location.href по кастомному протоколу 1c://. Платформа 1С умеет перехватывать любые попытки перехода по ссылкам внутри поля HTML-документа с помощью встроенного события ПриНажатии.
Настроим обработчик события ПриНажатии для нашего Поля HTML-документа на управляемой форме:
&НаКлиенте
Процедура HTMLКодПоляПриНажатии(Элемент, ДанныеСобытия, СтандартнаяОбработка)
// Перехватываем только наши кастомные ссылки схемы 1c://
Если Найти(ДанныеСобытия.Href, "1c://shot") > 0 Тогда
// Отменяем стандартный переход браузера по ссылке
СтандартнаяОбработка = Ложь;
// Извлекаем параметры из URL (например, "1c://shot?id=cell_5_3")
ПараметрыАдреса = ДанныеСобытия.Href;
ПозицияId = Найти(ПараметрыАдреса, "id=cell_");
Если ПозицияId > 0 Тогда
СтрокаКоординат = Сред(ПараметрыАдреса, ПозицияId + 8); // Вырезаем "5_3"
ЧастиКоординат = СтрРазделить(СтрокаКоординат, "_");
СтрокаКлика = Число(ЧастиКоординат[1]);
КолонкаКлика = Число(ЧастиКоординат[2]);
// Запускаем расчет хода игры
ОбработатьХодВHTML(СтрокаКлика, КолонкаКлика);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Чтобы изменить состояние ячейки в HTML-документе без полной перезагрузки страницы, мы можем использовать динамическое выполнение JavaScript прямо из 1С с помощью метода Документ.parentWindow.eval() (или метода ВыполнитьПрограмму() в новых версиях платформы):
&НаКлиенте
Процедура ОбработатьХодВHTML(СтрокаКлика, КолонкаКлика)
// Выполняем игровую логику и определяем результат
Попал = ПроверитьПопаданиеНаСервере(СтрокаКлика, КолонкаКлика);
ИдентификаторЭлемента = "cell_" + Формат(СтрокаКлика, "ЧГ=0") + "_" + Формат(КолонкаКлика, "ЧГ=0");
ДокументHTML = Элементы.HTMLКодПоля.Документ;
Если ДокументHTML <> Неопределено Тогда
Если Попал Тогда
// Находим нужный div и вешаем на него CSS-класс .hit
КодJS = "var el = document.getElementById('" + ИдентификаторЭлемента + "');
|el.className = 'cell hit';
|el.innerText = 'X';";
Иначе
// Вешаем CSS-класс .miss
КодJS = "var el = document.getElementById('" + ИдентификаторЭлемента + "');
|el.className = 'cell miss';
|el.innerText = '•';";
EndIf;
// Выполняем скрипт в контексте страницы
ДокументHTML.parentWindow.eval(КодJS);
КонецЕсли;
КонецПроцедуры
Для того чтобы окончательно определиться с выбором архитектурного решения для вашего игрового проекта к празднику 9 мая, сопоставим все три технологии по ключевым критериям:
Рекомендуем начать разработку с реализации прототипа игры на Табличном документе, так как это позволит вам быстро отладить саму математическую модель и правила игры (расстановку кораблей, проверку победных комбинаций). Впоследствии, если захочется добавить проекту яркого визуального стиля и анимации, вы сможете легко перенести готовую логику на рельсы Поля HTML-документа.