вторник, 8 ноября 2022 г.

"Перехват"

Данная статья рассказывает о первом личном опыте работы в игровом движке GODOT. Имея некоторый опыт работы в ныне уже мертвом движке Blender Game Engine (BGE), долгое время искал ему замену. По ряду причин меня не устраивали ни Unity, ни Unreal Engine (ограниченность размеров игрового мира, явная перенасыщенность меню этих движков, сложность освоения — это прежде всего к UE, да и политическая ситуация ныне нездоровая, достаточно посмотреть на Олимпийское движение с его «спорт вне политики»).

Сам проект можно скачать здесь: https://cloud.mail.ru/public/UZ1V/4Sa2ShCMt

20 МБ. Версия GODOT 3.5.1

По ряду причин, прежде всего, технических (старый компьютер с маленьким объемом ОЗУ), я вынужден был отказаться (надеюсь, временно), от освоения российского движка UNIGINE, хотя и считаю его весьма перспективным, по некоторым причинам затормозилось освоение другогог российскогог движка NeoAxis (прежде всего из-за запутанности интерфейса и некоторых вещей, вроде импорта моделей), но потом я еще попробую вернуться к этой теме, если что-то будет получаться.

Фактически требовалась замена BGE, желательно open-source, не перетяжеленный интерфейс, язык программирования — желательно Python, хотя и C# подошел бы, да и С++, если сильно будет нужно, можно начать изучать на практике.

В GODOT используется собственный язык скриптования, очень похожий на Питон, есть правда, свои нюансы, самый важный из которых — невозможность прицепить к одному объекту несколько скриптов. Но в принципе, для быстрого старта и изучения движка на практике, это не критично.

Первым проектом, который был доведен до ума и который можно назвать завершенным, является небольшая двумерная игра «Перехват», созданная по мотивам советского игрового автомата, на котором когда-то довелось поиграть в детстве (во второй половине 1980-х годов).

Скрин стартовой сцены.




Скрин. Перехватчик ведет огонь.



Скрин. Взрыв "наглотавшейся" снарядов цели.



Скрин итоговой сцены.



Сама игра предельно проста — небольшой экран, на котором изображается небо с плывущими по нему облаками, создающее иллюзию полета. В нижней части экрана влево-вправо перемещается с помощью ручки управления силуэт перехватчика, навстречу которому по экрану сверху вниз пролетают самолеты противника. За время игрового сеанса игрок должен сбить как можно больше «вражеских» самолетов, нажимая на кнопку на ручке управления, имитрующую гашетку авиациооной пушки. Имеется табло подсчета уничтоженных целей, и таймер.

При создании игры я работал именно «по мотивам», не копируя все из оригинальной версии «Перехвата», поэтому все, что можно увидеть на экране — отличается и довольно сильно. Сама игра состоит из трех основных сцен — стартовой, собственно игровой и итоговой. На игровой сцене происходят основные события и на нее же подгружаются сцены элементов игры. GODOT предусматривает создание некоего конструктора из множества сцен, на которых размещаются элементы самого проекта, которые могут подгружаться в основную сцену по мере надобности. Это очень удобно, почти точно так же работал и почивший ныне BGE.

Итак, переходим, собственно к описанию проекта, можно даже сказать, к его технической документации.

Стартовая сцена, она же StartGameScene. Она довольно проста и состоит всего из 5 элементов, как их называют в GODOT – нод, узлов. Одна из них является корнем, родителем для всех остальных. В сцене имеются две кнопки «Старт» и «Выход», фоновая каринка и нода фоновой музыки. Картинка была создана в ГИМП, на ней фото самого игрового автомата и сопроводительный текст, в котором заодно указаны клавиши управления, их всего три — Пробел, Левая Стрелка и Правая Стрелка. Поверх картинки и размещаются две кнопки. Фоном играет музыка, представляющая из себя мелодию формата ogg. Здесь следует отметить, что если вам надо непрерывно проигрывающийся звук (циклическое воспроизведение), то надо использовать именно формат OGG. При его добавлении на ноду AudioStreamPlayer он автоматически зацикливается и вам не нужны дополнительные манипуляции. Только надо не забыть поставить галочку в графу Autoplay в Инспекторе меню (справа, верхняя часть меню). В свое время я долго не ммог понять, почему у меня не происходит циклического проигрывания звука, который был в формате wav, так вот причина крылась именно в формате — нужен был ogg.

Скриптов в первой сцене немного — всего-то один. И он расположен на корневой ноде.


extends Node2D


#Это переменная таймера кнопки «старт»

var timerStart = 0


#Эта функция выполняется РАЗОВО

func _ready():

#Установка сигналов на нажатие кнопок, и названия функций по которым должна идти

#отработка функций после выдачи сигнала. Сигнал стандартный «кнопка нажата», а

#функции разные.

$GameStart.connect("button_down", self, "_click1")

$GameEnd.connect("button_down", self, "_click2")


#Эта функция запускает таймер кнопки «Старт»

func _click1():

timerStart += 1


#Стандартная функция GODOT, работать начинает сразу после _ready и работает непреывно.

func _process(delta):

#Если таймер кнопки стартовал и не равне нулю, он непрерывно увеливает свое

#значение, пока не достигнет некоего предела.

if timerStart!= 0:

timerStart+= 1

#Все это нужно, чтобы игрок увидел реакцию кноки, иначе будет мгновенно

# загружена игровая сцена, будет не слишком красиво.

if timerStart > 10:

#Кнопка нажалась, теперь можно перейти на игровую сцену

get_tree().change_scene("res://Scenes/GeneralGameScene.tscn")


#Эта функция просто выключает игру

func _click2():

get_tree().quit()



А далее мы попадаем на игровую сцену. На ней имеются:


Фон однотонного цвета

Черная полоска с иконками и цифрами внизу

Перехватчик, перемещающийся влево-вправо

Проплывающие по экрану сверху вниз облака

Самолеты и иракеты противника, также плывущие сверху вниз

Трассера перехватчика игрока

Взрывы при попаданиях в цели


Сама сцена называется GeneralGameScene, это имя носит корневая нода, к которой подсоединены все выше перечисленные вещи.

Фон сцены — просто цветовая нода ColorRect, которая задает цвет фону.

Черная полоска с иконками снарядов, взрывов, уничтоженных целей и «проскочивших» целей также выполнена в ГИМПЕ — это просто рисунок с прозрачным слоем, в формате png.

Цифры же представляют из себя ноды типа Label, то есть «метка» с текстом в виде цифр, причем все ноды переименованы так, чтобы можно было с ходу понять, кто и за что отвечает.

А вот дальше уже идут подгружаемые объекты — перехватчик, облака и цели...

Сначал скрипт корневой ноды GeneralGameScene.


extends Node2D


#Переменная таймера облаков

var randomClouds = 0


#Словарь данных игры — текущие данные и данные за лучший сеанс

#Уничтоженные цели, Кол-во попаданий, Пропущенные цели

var DictSave = {"Frag":0, "Victories":0, "Survives":0, "BestFrag":0, "BestVictories":0, "BestSurvives":0}


#Таймер целей

var TargetGen = 0


#Боекомплект (БК), попадания, проскочившие цели, уничтоженные цели

var BK = 500

var Frag = 0

var Survives = 0

var Victories = 0


func _ready():

#Добавление сцены перехватчика игрока, срабатывает ОДИН раз

var Interceptor = preload("res://Scenes/Interceptor.tscn").instance()

add_child(Interceptor)


#Очень важная вещь — сохранение данных после игры в файл формата json

func jsonJobWrite():

var fileSave = File.new()

fileSave.open("res://Scripts/DictSave.json", File.WRITE)


#Ключеовая строчка — запись обновленного словаря скрипта в json

fileSave.store_string(to_json(DictSave))

fileSave.close()


#Здесь возможна некоторая путаница, дело в том, что сначала открывается файл json с

#данными за предыдущую игру и эти данные пишутся в словарь этого скрипта. Причем

#по окончании сеанса происходит исправление части данных словаря — а именно, текущих

#достижений, достижения лучшего сеанса (ключи словаря с приставкой Best не меняются),

#и уже пеотом, при переходе на итоговую сцену, обновленный словарь вновь пишется в json.

func jsonJob():

var LoadFile = File.new()

#Проверка, есть ли такой файл

if not LoadFile.file_exists("res://Scripts/DictSave.json"):

return

#Если есть, то открываем, читаем данные и записываем их в словарь скрипта

#Что-то вроде временной копии.

else:

LoadFile.open("res://Scripts/DictSave.json", File.READ)

var temp = parse_json(LoadFile.get_line())

#Это как раз чтение и запись в словарь скрипта

DictSave = temp


#А вот это уже пошли исправления — учитываются текущие результаты

DictSave["Victories"] = Victories

DictSave["Frag"] = Frag

DictSave["Survives"] = Survives

LoadFile.close()

# А вот теперь обновленный словарь можно и записать — перед закрытием игровой

#сцены и уходом на итоговую (см. функцию выше)

jsonJobWrite()


func _process(delta):

#Блок случайности пояления облаков, работает таймер облаков

randomize()

randomClouds = rand_range(0, 10)

if randomClouds > 9.98:

var Clouds = preload("res://Scenes/Cloud.tscn").instance()

add_child(Clouds)

#Выход из игры происходит при исчерпании БК либо по нажатии Esc

if Input.is_action_pressed("ui_cancel") or BK == 0:

#Сначал обязательно обрабатываются данные текущего сеанса, и только

#потом идет переход на итоговую сцену

jsonJob()

get_tree().change_scene("res://Scenes/EndGame.tscn")

#Работа генератора целей, а также контроль расхода БК, попаданий и т.п.

#Используется текст дочерних нод Label

TargetGen += 1

get_parent().get_node("BK").text = str(BK)

get_parent().get_node("Frag").text = str(Frag)

get_parent().get_node("Survives").text = str(Survives)

get_parent().get_node("Victories").text = str(Victories)

#Появление новой цели на экране

if TargetGen == 300:

var Target = preload("res://Scenes/Target.tscn").instance();

add_child(Target)

TargetGen = 0


Скрипт относительно длинный, но много места в нем занимает работа по контролю и сохранению данных. А теперь перейдем к подгружаемым объектам.

Сначала облака — это чисто декоративные объекты, их задача медленно и величественно проплыть по экрану, иногда мешая, иногда помогая игроку. Скрипт прост.


extends Sprite


func _ready():

#При появлении облака происходят три случайные вещи. Задается положение

#размер и форма (через изменение текстуры вида облака)

randomize()

var coordinateX = rand_range(40,980)

var scaleCloud = rand_range(0.1, 0.6)

var textureNum = rand_range(0,1)

#Смена текстуры, просто выбирается одна картинка из трех

if textureNum < 0.66 and textureNum > 0.33:

set_texture(load("res://GamePics/Cloud2.png"))

elif textureNum > 0.66:

set_texture(load("res://GamePics/Cloud3.png"))

#Случайное место появления — только по горизонтали, по вертикали облако ВСЕГДА

#ставится за пределами экран

position.x = coordinateX

position.y = -20

scale = Vector2(scaleCloud, scaleCloud)


func _process(delta):

#Дальше оно просто плывет сверху вниз, при выходе за пределы видимости

#убирается, чтобы не захламлять экран и память

position.y += 0.5

#Это как раз егог исчезновение

if position.y > 700:

self.queue_free()


А вот теперь переходим уже к перезватчику, трассерам, противникам и взрывам...

Скрипт перехватчика:


extends Sprite


#Таймер стрельбы

var shoot = 0


func _ready():

#При своем появлении перехватчик устанавливается в правом нижнем углу экрана

position.x = 980

position.y = 500


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

#Их три штуки, довольно сильно отличающиеся, но только видом

randomize()

var textureNum = rand_range(0,1)

if textureNum < 0.66 and textureNum > 0.33:

set_texture(load("res://GamePics/Interceptor2.png"))

elif textureNum > 0.66:

set_texture(load("res://GamePics/Interceptor1.png"))

func _process(delta):

#Управление — вправо-влево и стрельба, перемещение ограничено в координатах,

#чтобы не «уезжать» за пределы экрана

if Input.is_action_pressed("ui_right") and position.x < 980:

position.x +=8

elif Input.is_action_pressed("ui_left") and position.x > 40:

position.x -=8

if Input.is_key_pressed(KEY_SPACE):

if get_parent().get_parent().BK > 0:

shoot += 1

if shoot == 7:

#Это и есть стрельба — добавление сцены трассера раз в 8

#тиков таймера, чтобы темп был не слишком высок

var Trasser = preload("res://Scenes/Trasser.tscn").instance();

add_child(Trasser)

shoot = 0


Дальше смотрим скрипт трассера-снаряда. Его задача — пролететь по экрану вверх, если он не встретился с целью, то просто исчезнуть, если было пересечение — вызвать взрыв, сообщить о своем попадании «счетчику» и исчезнуть.


extends Sprite


func _ready():

#В этом блоке разово выставляется размер трассера, отыскивается корневой элемент

#элемент сцены со счетчиком и включается нода отслеживания столкновения с целью

#Area2D, которая должна подать сигнал всем «заинтересованным лицам»

get_parent().get_parent().get_parent().get_parent().BK -= 1

$AudioStreamPlayer.play()

$Area2D.connect("area_entered", self, "hit")

scale = Vector2(1.5, 1.5)


func _process(delta):

#Это просто полет трассера вверх по экрану и его «самоликвидация» при выходе

#за пределы оного

position.y -= 100

if position.y < -2500:

self.queue_free()


func hit(object):

#В случае попадания следует сообщение для «счетчика», смотрим скрипт главной

#игровой сцены GeneralGameScene, там есть переменная Frag

get_parent().get_parent().get_parent().get_parent().Frag += 1

#Все, сообщение передано, убираем ненужный трассер

queue_free()


Все, что происходит с целью — описано в скрипте Target, в котором описано ее поведение.


extends Sprite


#Переменные урона и «порога стойкости»

var healthAnti = 0

var limitLife = 0


func _ready():

#Появление цели по горизонтали случайно, но она всегда появляется за верхним

#обрезом экрана

randomize()

var numX = rand_range(40,980)

#Выставляем «порог стойкости», то есть сколько попаданий выдержит цель

#стартовые координаты, и включаем сигнал столкновения со снарядом

limitLife = rand_range(0,5)

position.x = numX

position.y = -20

$Area2D.connect("area_entered", self, "hit")

var textureNum = rand_range(0,1)

#Это длинный и однообразный блок для смены текстур вида цели, выбирается аж

#девять картинок

if textureNum < 0.3 and textureNum > 0.15:

set_texture(load("res://GamePics/Target1.png"))

elif textureNum < 0.45 and textureNum > 0.3:

set_texture(load("res://GamePics/Target2.png"))

elif textureNum < 0.6 and textureNum > 0.45:

set_texture(load("res://GamePics/Target3.png"))

elif textureNum < 0.75 and textureNum > 0.6:

set_texture(load("res://GamePics/Target4.png"))

elif textureNum < 0.9 and textureNum > 0.75:

set_texture(load("res://GamePics/Target5.png"))

elif textureNum > 0.9:

set_texture(load("res://GamePics/Target6.png"))

func _process(delta):

#А дальше цель плывет вниз, и исчезает, если в нее не попали, только и всего

position.y += 1

#print(get_parent().get_parent().name)

if position.y > 700:

get_parent().get_parent().Survives += 1

self.queue_free()


func hit(object):

#А вот если попали, идет добавление урона, и как только цель «наглотается» снарядов

#следует взрыв и сопутствующая этому возня с картинками, звуками и подсчетами

healthAnti += 1

if healthAnti > limitLife:

deadTarget()


#Основная возня описана здесь

func deadTarget():

#Сначала подгружаем сцену взрыва

var Explode = preload("res://Scenes/Explode.tscn").instance();


#Сообщение об уничтожении цели в ноду GeneralGameScene

get_parent().get_parent().Victories += 1


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

# левом верхнем углу, в нулевой точке, надо переместить спрайт взрыва в точку своих

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

#есть приходится менять родительский объект...

var generalNode = get_tree().get_root().get_child(0)

add_child(Explode)

self.remove_child(Explode)

generalNode.add_child(Explode)

Explode.position = self.position


#Вот теперь все, и цель может быть убрана с экрана после своего уничтожения

queue_free()



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


extends AnimatedSprite


var timerLife = 0


func _ready():

#Ради эксперимента здесь выставлено проигрывание звука через код

$AudioStreamPlayer.play()


func _process(delta):

#Чтобы анимация спрайта проигрывалась, ее надо создать и дать ей название

#а уже потом ее можно проиграть одной строчкой

play("Explode")

#А это просто задержка исчезновения ненужной после анимации и звука сцены,

#Чтобы звук не «обрезался»

timerLife += 1

if timerLife == 100:

get_parent().queue_free()


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

Скрипт EndGame проводит подведение итогов, он тоже длинный.


extends Node2D


#Таймер для кнопки, чтобы была небольшая задержка (см. самый первый скрипт)

var timerStart = 0


#Все уже знакомо — словарь сохранения результатов, переменные для попаданий, побед и пр

#Плюс лучшие достижения

var DictSave = {"Frag":0, "Victories":0, "Survives":0, "BestFrag":0, "BestVictories":0, "BestSurvives":0}


var CurrentVictories = 0

var CurrentBullet = 0

var CurrentLoose = 0


var BestVictories = 0

var BestBullet = 0

var BestLoose = 0


func _ready():

#И снова загрузка и чтение фала json после только что проведенного игрового сеанса

var LoadFile = File.new()

if not LoadFile.file_exists("res://Scripts/DictSave.json"):

return

else:

LoadFile.open("res://Scripts/DictSave.json", File.READ)

var temp = parse_json(LoadFile.get_line())

#print(temp)

CurrentVictories = temp["Victories"]

CurrentBullet = temp["Frag"]

CurrentLoose = temp["Survives"]

BestVictories = temp["BestVictories"]

BestBullet = temp["BestFrag"]

BestLoose = temp["BestSurvives"]

#Меняем текст дочерних нод типа Labael на итоговом меню

$CurrentBullet.text = str(temp["Frag"])

$CurrentVictories.text = str(temp["Victories"])

$CurrentLoose.text = str(temp["Survives"])

$BestBullet.text = str(temp["BestFrag"])

$BestVictories.text = str(temp["BestVictories"])

$BestLoose.text = str(temp["BestSurvives"])

#Файлы json после использования надо обязательно закрывать

LoadFile.close()

#Три кнопки с сигналами при их нажатии - «Выход», «Новая игра», «Сброс»

$ButtonReset.connect("button_down", self, "_click1")

$ButtonNewGame.connect("button_down", self, "_click2")

$ButtonQuit.connect("button_down", self, "_click3")


func _process(delta):

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

if timerStart!= 0:

timerStart+= 1

if timerStart > 10:

Resultat()

get_tree().change_scene("res://Scenes/GeneralGameScene.tscn")

func _click1():

#Для кнопки «Сброс» - просто обнуляет все значения для словаря, игра начинается

# с нулевой точки, все старые достижения стерты

DictSave = {"Frag":0, "Victories":0, "Survives":0, "BestFrag":0, "BestVictories":0, "BestSurvives":0}

var fileSave = File.new()

fileSave.open("res://Scripts/DictSave.json", File.WRITE)

fileSave.store_string(to_json(DictSave))

fileSave.close()


func _click2():

#Новая игра

Resultat()

timerStart += 1

func _click3():

#”Выход» - просто выключение игры

get_tree().quit()

func Resultat():

#Идет сравнение только что проведенного игрового сеанса с лучщими достижениями

#Если результат лучше, то старые достижения сменяются новыми

if CurrentVictories > BestVictories:

DictSave["BestVictories"] = CurrentVictories

DictSave["BestFrag"] = CurrentBullet

DictSave["BestSurvives"] = CurrentLoose

elif CurrentVictories == BestVictories:

if CurrentBullet < BestBullet:

DictSave["BestVictories"] = CurrentVictories

DictSave["BestFrag"] = CurrentBullet

DictSave["BestSurvives"] = CurrentLoose

elif CurrentBullet == BestBullet:

if CurrentLoose < BestLoose:

DictSave["BestVictories"] = CurrentVictories

DictSave["BestFrag"] = CurrentBullet

DictSave["BestSurvives"] = CurrentLoose

DictSave["Victories"] = CurrentVictories

DictSave["Frag"] = CurrentBullet

DictSave["Survives"] = CurrentLoose

#Запись обновленного словаря в файл json

var fileSave = File.new()

fileSave.open("res://Scripts/DictSave.json", File.WRITE)

fileSave.store_string(to_json(DictSave))

fileSave.close()


Надеюсь, было более-менее понятно. Следует учесть, что далеко не все возможности и нюансы GODOT были мною освоены, очень возмржно, что в некоторых местах код может быть более лаконичным, возможно, что-то было наворочено лишнего, но проект был доведен до рабочего состояния, был успешно экспортирован в приложение типа exe для Виндовс, может быть существенно доработан, к примеру, при увеличении числа побед увеличивается частота появления целей и их скорость и скорость перехватчика, можно добавлять бонусы в виде дополнительного боезапаса, ограничить время игры по таймеру, включить новые виды оружия типа самонаводящихся ракет, постановку помех, и так далее (но это уже ближе к авиасимулятору будет).


Для освоения GODOT использовались примеры из блога Дзен;

https://dzen.ru/godotengine

Официальная документация GODOT

https://godot-ru.readthedocs.io/ru/latest/

Музыка взята с сайта Opengameart

https://opengameart.org/









суббота, 24 июля 2021 г.

Игра "Морской бой. След торпеды" Альфа-версия.

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


И вот подумалось, а почему бы не попробовать сделать то же самое в БГЕ. Пусть и мертв ныне этот движок, в смысле, не занимаются им больше создатели Блендера, но уж такое-то должен он потянуть.

Сказано- сделано. Альфа версия


Простенькая игра. "Морской бой. след торпеды" Создана в BGE, для работы игры испольуйте Blender 2.79b.Управление - перископ влево-вправо - стрелочки влево-вправо, выстрел - - пробел. При переходе на сцену подведения итогов - выход из игры - стрелочка вниз, новый круг - стрелочка вверх.
Длительность игрового сеанса - 3 минуты, боезапас - 20 торпед. После первого сеанса в разделе "Лучший результат будут нули, затем идет сравнение и результат улучшается в первую очередь по количеству побед за сеанс, во-вторых, если количество текущих побед совпало с лучшим результатом, идет сравнение с количеством израсходованного боезапаса, чем меньше израсходовано торпед на одну цель, тем лучше.

Можете поэкспериментировать с дальностью до генерируемых кораблей - открывайте скрипт Submarine_Control.py и ищите строчку почти в самом конце randomXY = random.randrange(200, 1400). В скобках эта самая дальность, можете расширить ее пределы, но не более 4500, хотя сомневаюсь, что на такой дистанции удастся вообще когда-нибудь попасть. А вот уменьшить, скажем до 800-1000 - улучшит количество попаданий. Отрицательные цифры ставить нельзя, и меньше 100 тоже.

Объем упакованного файла - 49 Мб, распакованного проекта - 74. Запускаемый файл - бленд SeaBattle>blend. Скрипт, о котором я говорил, находится в нем же.

Альфа версия. Только один тип мишени - незатекстуренный, тонет при попадании без всяких дополнительных эффектов, движение слева-направо. Предполагаю следующие изменения/дополнения:
1) Еще пара-тройка моделей кораблей.
2) Все модели с текстурами.
3) Смена текстуры неба случайным образом при старте (для разнообразия).
4) Возможность появления кораблей с другой стороны, но движение все равно будет только в одну сторону (типа конвой в море)
5) Если БГЕ не будет сильно возмущаться - добавить эффект разрушения - пожар на тонущем корабле с дымом.

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

https://drive.google.com/file/d/1cfOjeJLvPIUFGBjxt1e19u2ubN6V5XBh/view?usp=sharing
https://yadi.sk/d/nqPDWcWfxCe93Q


Скрины:

Старт


Сама игра.

Подведение итогов



вторник, 28 июля 2020 г.

Снова о ландшафтах и квадратах, то есть блоках.

Чтение учебной литературы по разным темам отнимает много времени, но работа над проектом все ще продолжается, хотя и непонятно, чем все это закончится и закончится ли вообще. Но и сидеть, ничего не делая, тоже неохота.
Недавно отработал самонаведение ракеты в Unity, отыскал аналогичный урок для UE4, провел массовое обновление ПО - Юнити и UE4, плюс VisualStudio, обновил Блендер несколько раз, пробовал UPBGE для EEVEE. Увы, для последнего альфа сборка - это альфа и есть. Категорически не хочет работать подгрузка бленд-файлов через LibLoad - UPBGE мгновенно вылетает. Смотрел и Armory3D, но толком не занимался. Просмотрел и прослушал курсы по Блендеру от А. Слаквы, для Блендер 2.8, пытаюсь в нем работать - с непривычки тяжело.
Есть мысли по поводу упрощения и сокращения проектов в Юнити и БГЕ, касающиеся прежде всего моделей оружия и их json файлов. Если вкратце - стоит провести объединение в одном файле вариантов подвески для ракет однотипного семейства, например AIM-120, AIM-7, Р-3/13, Р-23/24, Р-27 и так далее. Все дело в том, что для нескольких файлов json имеются одинаковые координаты и углы поворота ракет для подвески, отличаются они лишь наименованиями самих ракет - всего-то пара слов, даже не строчек. Поэтому имеет смысл просто перечислить в отдельной строке меши ракет, которые надо найти для подгрузки, а вместо названий ракет в словах проставить missileStr, а не R-23R, которые будут указаны выше.

{

"objList":["R-23R", "R-23T", "R-24R", "R-24T", "R-24RM"],

"obves":{"FLG_APU23_|":{"parentObj":"CntAircraft","locObj":[0.0,0.0,0.0],"rotObj":[0.0,0.0,0.0]},
         "missileStr|1":{"parentObj":"CntAircraft","locObj":[-1.428,-2.0,0.03],"rotObj":[-0.035,0.0,0.0],"weapon":1,"CatapultSbros":0},
         "missileStr|2":{"parentObj":"CntAircraft","locObj":[1.428,-2.0,0.03],"rotObj":[-0.035,0.0,0.0],"weapon":1,"CatapultSbros":0}
         }
}

Поскольку у меня отлажена система поиска файлов с разным расширением, то поисковик-скрипт отыщет и подгрузит все нужное. А вместо пяти файлов для ракет можно получить один. Это для Р-23 и Р-24, а для aIM-120 вместо 8 будет 1 и для "Спэрроу" вместо 12 - 1, столько же для "Сайдуиндеров"...
Есть еще один нюанс. Для ракет типа "Спэрроу" и "АМРААМ модели внешне почти неотличимы или совсем неотличимы, поэтому в файле ТТХ ракеты надо просто перечислить названия моделей ракет например Sparrow_II и скоратить число блендов. А сами ТТХ ракет объединить в json - поисковик все найдет...

Но все это по ракетам, а есть еще ландшафт. Тут тоже есть новые задумки. Ландшафт разбивается на квадраты типа А1, Б4 и так далее. Все эти квадраты упакованы в отдельные бленды, и снабжены json  с перечислениями стоящих на них объектов. В зависимости от положения активной камеры сначала грузятся стартовый квадрат и 8 квадратов вокруг него. В адльнейшем идет отслеживание положения камеры и "догрузка", если надо, но происходить это будет редко. Фактически, выбирается один квадрат, что-то вроде "центра мира" и вокруг него выстраивается "периферия". Но и это еще не все. Новый "центр мира" "оттаскивается" в нулевое исходное положение, вместе с ним на ту же величину переносятся и ранее сгенеренные "квадраты" со всем их содержимым. Плюс юниты игры также сменяют сове положение на величину "единицы" ландшафта. А создавалась эта система с прицелом на Юнити. Большой ландшафт единым кусокм делать неудобно, плюс говорилось, что координаты больше 100 тысяч единипц приводят к некорректной работе и тормозам, значит, надо уменьшать масштаб самих юнитов, ну раз в 10. Тогда надо учесть, что и их скорости и величина ускорения свободного падения и размеры статических объектов (деревья, здания, дороги) надо также отмасшатбировать, уменьшив в 10 раз.
Возвращаясь к ландшафтуи отрабатываемой сейчас системой его "постройки", скажу, что с "передвиганиями" юнитов, по идее, должно получиться "удерживать" юнит игрока внутри некоторого предела, да и остальные юниты, в общем-то тоже.

Чкрипт  на данный момент:
import bge
scene = bge.logic.getCurrentScene()

cont = bge.logic.getCurrentController()
own = cont.owner

bge.logic.globalDict["nameBlock"] = "D4"
scaleBlock = 2.5

#Конфигурация расположения блоков террайна - вложенные списки
configBlock = [
              ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8"],
              ["B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"],
              ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8"],
              ["D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8"],
              ["E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8"],
              ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8"],
              ["G1", "G2", "G3", "G4", "G5", "G6", "G7", "G8"],
              ["H1", "H2", "H3", "H4", "H5", "H6", "H7", "H8"]                             
              ]
   
def BlockTerrain():
    cont = bge.logic.getCurrentController()
    own = cont.owner       
    #Индексы списка блоков - внешний и вложенный
    x = 0
    y = 0
    #Величина перемещения блоков и их направление
    posX = 0.0
    posY = 0.0
   
    nameBlock = bge.logic.globalDict["nameBlock"]
   
    #Список элементов, содержащих информацию о блоках и их смещении при появлении
    listIndex = []
    #Список уже имеющихся блоков террайна
    terrainList = []
   
    #Сначала ищем в общем списке вложенный с наименованием "центра", вокруг которого
    # выстраиваются еще 8 дополнительных блоков террайна 
    for listObj in configBlock:
        for obj in listObj:
            #После нахождения "центра мира" в списке блоков внутри общего списка блоков террайна
            if nameBlock == obj:
                #Заносим его в список индексов, это обязательно, смещение для "центра" нулевое
                listIndex.append( str( configBlock.index(listObj) ) + "_" + str( listObj.index(obj) ) + "|" + "0.0" + "_" + "0.0" )
                #Загоняем в список блоки "перед" и "позади" "центра мира", учитывая пределы индексов списка
                if listObj.index(obj)-1 > -1:
                    listIndex.append( str( configBlock.index(listObj) ) + "_" + str( listObj.index(obj)-1 ) + "|" + "0.0" + "_" + str(-scaleBlock) )
                if listObj.index(obj)+1 < len(listObj):
                    listIndex.append( str( configBlock.index(listObj) ) + "_" + str( listObj.index(obj)+1 ) + "|" + "0.0" + "_" + str(scaleBlock) )
               
                #А теперь осматриваем вложенные списки "выше" и "ниже"  найденного, опять учитываем пределы индексов
                #Если такие списки есть, то заносим в listIndex информацию о блоках с индеками "центра мира" и плюс-минус 1
                if configBlock.index(listObj)-1 > -1:         
                    listIndex.append( str( configBlock.index(listObj)-1 ) + "_" + str( listObj.index(obj) ) + "|" + str(scaleBlock) + "_" + "0.0" )
                    if listObj.index(obj)-1 > -1:
                        listIndex.append( str( configBlock.index(listObj)-1 ) + "_" + str( listObj.index(obj)-1 ) + "|" + str(scaleBlock) + "_" + str(-scaleBlock) )
                    if listObj.index(obj)+1 < len(listObj):
                        listIndex.append( str( configBlock.index(listObj)-1 ) + "_" + str( listObj.index(obj)+1 ) + "|" + str(scaleBlock) + "_" + str(scaleBlock) )
               
                if configBlock.index(listObj)+1 < len(configBlock):
                    listIndex.append( str( configBlock.index(listObj)+1 ) + "_" + str( listObj.index(obj) ) + "|" + str(-scaleBlock) + "_" + "0.0" )
                    if listObj.index(obj)-1 > -1:
                        listIndex.append( str( configBlock.index(listObj)+1 ) + "_" + str( listObj.index(obj)-1 ) + "|" + str(-scaleBlock) + "_" + str(-scaleBlock) )
                    if listObj.index(obj)+1 < len(listObj): 
                        listIndex.append( str( configBlock.index(listObj)+1 ) + "_" + str( listObj.index(obj)+1 ) + "|" + str(-scaleBlock) + "_" + str(scaleBlock) )
   
    #Проверка на нличие блоков террайна в сцене
    for obj in scene.objects:
        if "Terrain" in obj.name:
            terrainList.append(obj.name)
   
    #По окончании составления списка препарируем каждый его элемент типа 3_2|2.5_-2.5     
    for element in listIndex:
        #Перед "|" указаны индексы общего списка и вложенного списка, они дают выход на элемент "D3" в данном случае
        x = int( element.split("|")[0].split("_")[0] )
        y = int( element.split("|")[0].split("_")[1] )
        if "TerrainQuad_" + configBlock[x][y] not in terrainList:
            #Добавляем блок террайна и препарируем элементы после "|"
            BlockTerrain = scene.addObject("TerrainQuad_" + configBlock[x][y], own)
            #Получаем смещение ОТНОСИТЕЛЬНО "ЦЕНТРАЛЬНОГО" блока, весь отсчет идет относительно него
            posX = float( element.split("|")[1].split("_")[1] )
            posY = float( element.split("|")[1].split("_")[0] )
            #Смещаем только что добавленный блок и переходим к следующему - и так до конца списка
            BlockTerrain.worldPosition[0] += posX
            BlockTerrain.worldPosition[1] += posY
           
def control():
    cont = bge.logic.getCurrentController()
    own = cont.owner
    cam = scene.objects["Camera"]
    deltaX = 0.0
    deltaY = 0.0
    nameBlock = ""
    if cam.worldPosition[0] < -scaleBlock or cam.worldPosition[1] < -scaleBlock or cam.worldPosition[1] > scaleBlock or cam.worldPosition[0] > scaleBlock:
        if cam.worldPosition[0] < -scaleBlock:
            deltaX = -scaleBlock
        elif cam.worldPosition[0] > scaleBlock:
            deltaX = scaleBlock
        elif cam.worldPosition[1] < -scaleBlock:
            deltaY = -scaleBlock
        elif cam.worldPosition[1] > scaleBlock:
            deltaY = scaleBlock
       
        for obj in scene.objects:
            if "Terrain" in obj.name:
                obj.worldPosition[0] -= deltaX
                obj.worldPosition[1] -= deltaY
               
                if -scaleBlock * 0.1 < obj.worldPosition[0] < scaleBlock * 0.1 and -scaleBlock * 0.1 < obj.worldPosition[1] < scaleBlock * 0.1:
                    bge.logic.globalDict["nameBlock"] = obj.name.split("_")[1]
                   
        cam.worldPosition[0] -= deltaX
        cam.worldPosition[1] -= deltaY
        BlockTerrain()
       
#Первый стартовый запуск функции генерации и расстановки блоков террайна
BlockTerrain()

Думаю, комментарии делают этот код понятным... Надеюсь, во всяком случае. )))
Сам же террайн в БГЕ (или УПБГЕ, когда его отладят) планируется рскрасить по способу denis8424 - с разделением материалов по высоте, но с одним дополнением. Для каждого блока террайна провести смешивание текстур через маски, правда, это уж как получится. Там надо большое разрешение масок, все же даже масштабированные блоки - это 10 км, но можно попробовать маски при наложении дублировать - при больших размерах повторяемость не будет сильно бросаться в глаза, плюс для разных блоков маски будут разными, а число блоков в сумме - не слишком велико, вряд ли больше 100 (уж точно не 2500).


понедельник, 11 мая 2020 г.

Новое меню и несколько туманные перспективы...

После очень долгого перерыва решил все же написать очередной пост. Поскольку за это время произошло довольно мн6ого событий в мире 3D, а многие так и довольно давно, но мой консерватизм вкупе с известными проблемами не давал возможности заняться новым вплотную...
Итак, БГЕ - все. Обновлять его не будут, из версии 2.8 он выпилен, это и так многие знают. Остается, правда, UPBGE, да время от времени всплывают слухи, что в Блендер все же вставят что-то вроде игрового движка, но что это будет за новый "БГЕ" и будет ли он вообще - неясно.
Версия 2.8 уверенно развивается, 2.83 уже почему-то обозвали 2.9 - во всяком случае, при распаковке 2.9 в названии присутствует.
Следовательно, рано или поздно, но пришлось бы осваивать 2.8, с его новым интерфейсом, и новыми фишками, в которых автор этих строк пока не слишком (он и в 2.7 не слишком-то - имеется в виду рендер и использование всех возможностей программы).
За это время я принялся изучать Cycles и делать по урокам материалы (уроки Striver), а также читать, смотреть видеоуроки (которые перед этим скачивал ночью, по причине огромного трафика), и вообще пытаться больше понять 3D, его терминологию и возможности. И не только Блендер, но и Юнити и Анрил Энжин (с последним все же сильно не очень, потому как в Юнити куда как больше справочных материалов).
Но бросать свой бге-ешный проект не хотелось и работа продолжилась. Итогом стал очередной погром, который, похоже, выльется в переход на UPBGE  с новым, еще большим погромом. Все дело в том, что два с половиной года, отнятые у меня катарактой и сопутствующими ей "прелестями" привели к тому, что многое из того, что делалось, забылось, и просто-напросто устарело. Фактически проект будет делаться заново, правда в нем будут использоваться огромные куски кода, которые не забыты и могут быть использованы. Помимо этого необходимо освоить PBR-материалы, рендер EEVEE, заодно до кучи продолжить изучение Юнити и Cycles, а также заняться еще кое-чем, что непосредственно к 3D отношения не имеет и здесь поэтому перечисляться не будет. Как говорил Ильич: "Учиться, учиться и еще раз учиться" (Я помню этот лозунг, сложенный из вырезанных из пенопласта букв, прикленных к стене нашего школьного класса под потолком рядом с портретом вождя мирового пролетариата).
Некоторое время назад я занялся меню игры, которое было собрано наспех и кое-как. Сделать его удалось, хотя и было в итоге завалено дело с загрузкой объектов, но меню тут ни при чем, все дело было в загрузочном скрипте игровой сцены, которое было чрезмерно раздуто и усложнено. Само же меню прекрасно работает (хотя и не доделано по причине провала с загрузкой, но, судя по распечаткам консоли, все что нужно, оно делало, а редактор миссий и кампаний делать пока бессмысленно).
Меню позволяет переключаться между разделами, выбирать одиночную миссию, переходить на игровую сцену (где все и рвется), вызывает справку, меняет язык интерфейса. В общем, все работает, кроме редактора миссий и кампаний.
Ниже приводится текст скрипта меню, для которого используется логика лишь на одном объекте стартовой сцены-меню, причем логических кирпичей используется только 4. В ранней версии их было втрое больше, плюс имелась логика на других объектах. Само меню перед стартом подгружает бленд с нужными объектами, изначально в сцене только камера с логикой и плейн со стартовым черным фоном.
Итак, скрипт:

import bge
import sys
import os
import json
scene = bge.logic.getCurrentScene()

#Обьявление объекта сцены курсора
CursorMenu = scene.objects["CursorMenu"]

#Загрузка элементов меню из папки с компонентами
bge.logic.LibLoad("//Menu/Blend_Folder/Menu_Component.blend", 'Scene', load_actions = True)
   
pathJSON = ""
pathFon = ""
bge.logic.globalDict["bglText"] = ""
bge.logic.globalDict["DayNight"] = 1.3
bge.logic.globalDict["textGame"] = "Rus"

#Далее идет блок клавиатурных команд
keyboard = bge.logic.keyboard
JUST_ACTIVATED = bge.logic.KX_INPUT_JUST_ACTIVATED
JUST_RELEASED = bge.logic.KX_INPUT_JUST_RELEASED
INPUT_ACTIVE = bge.logic.KX_INPUT_ACTIVE

cont = bge.logic.getCurrentController()
own = cont.owner

#Сенсоры для управления меню
mouseMotion = cont.sensors["MouseMotion"]
mouseClick = cont.sensors["MouseClick"]

#Звуковой актуатор щелчка по кнопке
audioTemp = cont.actuators['SoundClick']

#Стартовые значения для меню - миссия по умолчанию
bge.logic.globalDict['currentMission'] = "MiG-23MF_vs_F-5E"
bge.logic.globalDict["dictUnit"] = []
bge.logic.globalDict["listUnit"] = []
bge.logic.globalDict["listEnemy"] = [ [[],[]], [[],[]] ]

scaleButtonStandart = {
                       "ButtonShort":[5.775, 1.5, 0.0],
                       "ButtonLong":[8.49, 1.5, 0.0],
                       "ButtonExit":[1.5, 1.5, 0.0],
                       "ButtonUp":[1.5, 1.5, 0.0],
                       "ButtonDown":[1.5, 1.5, 0.0],
                       #"ButtonUniversal":[1.5, 1.5, 0.0],
                       #"ButtonBig":[5.775, 4.12, 0.0],
                       "ButtonForward":[1.5, 1.5, 0.0],
                       "ButtonBack":[1.5, 1.5, 0.0]
                       }
 
#Создание проперти объекта для положения курсора
if "cursorPos" not in own:
    own["cursorPos"] = {"addSceneGame":0,
                        "tempX":0.0,
                        "tempY":0.0,
                        "language":"",
                        "listButton":[],
                        "animationButton":0,
                        "indexButton":"",
                        "oldPathFon":"",
                        "newPathFon":"",
                        "TimerColor":0,
                        "AlphaColor":0.0,
                        "newPatchMenu":"//Menu/JSON_Folder/Menu_Start.json",
                        "oldPatchMenu":"",
                        "PathFon":"",
                        "PathFonMonitor":"",
                        "limitXscroll":0,
                        "limitYscroll":0,
                        "valueButtonCoord":[0.0, 0.0, 0.0],
                        "pathButtonTXT":{},
                        "pathNameMission":{}
                        }

#Функция создания файла json с путями к файлам всего проекта
def startPath():
    cont = bge.logic.getCurrentController()
    scene = bge.logic.getCurrentScene()
    own = cont.owner
 
    #Создаем переменные - списки путей блендов и json плюс пустую строку для путей
    k = ""
    endStrPy = ""
    pathGeneral = []
    pathBlend = []
    pathJSON = []
    pathPy = []
 
    #Циклом перебираем файлы, директории и папки, создаем и сшиваем строки в пути
    for d, dirs, files in os.walk(bge.logic.expandPath("//")):
        for f in files:
         
            k = os.path.join(d,f)
         
            if ".blend1" in k:
                os.remove(k)
         
            #Питон-файлы добавляем в sys.path, остальные в список путей
            if ".py" not in k:
                k = k.split("BlendSim_2.1")[1]
                k = k.replace("\\", "/")
                if ".blend" in k:
                    if ".blend1" not in k:
                        pathGeneral.append("/"+k)
                if ".json" in k:
                    pathGeneral.append("/"+k)
         
            elif ".py" in k:
                if "__pycache__" not in k:
                    endStrPy = k.split("\\")[-1]
                    k = k.replace(endStrPy, "")
                    sys.path.append(k)
                    pathPy.append(k)
                 
    #Открываем файл json  с путями (если его нет, то он создается) и сбрасываем туда список путей
    with open(bge.logic.expandPath('//NodePath.json'), 'w') as NODEPATH:
        json.dump(pathGeneral, NODEPATH)

#Функция работы курсора меню и всех его компонентов - УЗЛОВАЯ ФУНКЦИЯ для  GeneratorMenuObj, buttonJob и on_click
def cursorPos():
    cont = bge.logic.getCurrentController()
    own = cont.owner
 
    #Сработка псевдоанимации при старте игры
    if mouseMotion.positive:
        if own["cursorPos"]["addSceneGame"] == 0:
            #Работет таймер
            if own["cursorPos"]["TimerColor"] < 600:
                own["cursorPos"]["TimerColor"] += 1
         
                #Сначала меняем меш курсора на логотип и медленно увеличиваем его "видимость"
                if own["cursorPos"]["TimerColor"] == 1:
                    CursorMenu.replaceMesh("Logo", True, False)
                    CursorMenu.color = [1.0, 1.0, 1.0, 0.0]
         
                if 1 < own["cursorPos"]["TimerColor"] < 200:
                    if own["cursorPos"]["AlphaColor"] < 1.0:
                        own["cursorPos"]["AlphaColor"] += 0.005
                 
                #Затем логотип медленно истаивает - увеличивается прозрачность
                if 200 < own["cursorPos"]["TimerColor"] < 400:
                    if own["cursorPos"]["AlphaColor"] > 0.0:
                        own["cursorPos"]["AlphaColor"] -= 0.005
         
                #Теперь меняем меш логотипа обратно на меш курсора
                if own["cursorPos"]["TimerColor"] == 401:
                    CursorMenu.replaceMesh("CursorMenu", True, False)
             
                #И заставляем курсор постепенно "проступать" на фоне меню
                if 400 < own["cursorPos"]["TimerColor"] < 600:
                    if own["cursorPos"]["AlphaColor"] < 1.0:
                        own["cursorPos"]["AlphaColor"] += 0.005
         
                #За все время работы таймера управление прозрачностью курсора идет через словарь свойств
                CursorMenu.color = [1.0, 1.0, 1.0, own["cursorPos"]["AlphaColor"]]
                #И местоположение курсора задется жестко - в центре меню
                CursorMenu.worldPosition = [0.0, 0.0, 0.5]
 
        #И только когда таймер прекратил работу - начинает работать перемещение и клики курсором
        if own["cursorPos"]["TimerColor"] == 600:
            if mouseMotion.positive:
                if own["cursorPos"]["tempX"] != mouseMotion.position[0]:
                    CursorMenu.worldPosition[0] += (mouseMotion.position[0] - own["cursorPos"]["tempX"])/4
                    own["cursorPos"]["tempX"] = mouseMotion.position[0]
                if own["cursorPos"]["tempY"] != mouseMotion.position[1]:
                    CursorMenu.worldPosition[1] -= (mouseMotion.position[1] - own["cursorPos"]["tempY"])/4
                    own["cursorPos"]["tempY"] = mouseMotion.position[1]

                if CursorMenu.worldPosition[0] > 43.0:
                    CursorMenu.worldPosition[0] = 43.0
                if CursorMenu.worldPosition[0] < -45.0:
                    CursorMenu.worldPosition[0] = -45.0
         
                if CursorMenu.worldPosition[1] > 26.0:
                    CursorMenu.worldPosition[1] = 26.0
                if CursorMenu.worldPosition[1] < -24.0:
                    CursorMenu.worldPosition[1] = -24.0
         
            if mouseClick.positive:
                on_click(own)
                if own["cursorPos"]["oldPathFon"] != own["cursorPos"]["newPathFon"]:
                    FonChanging(own)
     
            if own["cursorPos"]["animationButton"] > 0:
                buttonJob(own)
 
        #Эта часть кода отвечает за смену языка - при нажатии кнопок с флажками производится выбор и замена текста     
        if own["cursorPos"]["language"] != bge.logic.globalDict["textGame"]:
            for buttonObj in own["cursorPos"]["listButton"]:
                #Выбираются только кнопки с текстом
                if "IDtext" in buttonObj:
                    if bge.logic.globalDict["textGame"] == "Eng":
                        scene.objects.from_id(int(buttonObj["IDtext"]))["Text"] = buttonObj["textObj"]
                    elif bge.logic.globalDict["textGame"] == "Rus":
                        scene.objects.from_id(int(buttonObj["IDtext"]))["Text"] = buttonObj["textRus"]
            #После замены текста исправляется значение в словаре, чтобы не проверять понапрасну настройки языка по многу раз
            own["cursorPos"]["language"] = bge.logic.globalDict["textGame"]
 
        #Переустановка кнопок и прочего при смене пути к файлу json для данного раздела меню 
        if own["cursorPos"]["newPatchMenu"] != own["cursorPos"]["oldPatchMenu"]:
            GeneratorMenuObj(own)
 
    #СТАРТ ИГРЫ! - переход на игровую сцену 
    if own["cursorPos"]["addSceneGame"] != 0:
        own["cursorPos"]["addSceneGame"] += 1
        if own["cursorPos"]["addSceneGame"] == 20:
            cont.activate(cont.actuators['Scene'])
            own["cursorPos"]["addSceneGame"] = 0
         
#Функция расстановки кнопок и текста, фона раздела меню и так далее - УЗЛОВАЯ ФУНКЦИЯ для FonChanging
def GeneratorMenuObj(own):
    FonObj = None
    Monitor = None
    #Местотоположение курсора задется жестко - в центре меню
    CursorMenu.worldPosition = [0.0, 0.0, 0.5]
 
    #Прежде чем добавлять новые кнопки, удалим старые
    for oldButton in own["cursorPos"]["listButton"]:
        oldButton.endObject()
    own["cursorPos"]["listButton"].clear()

    for textObj in scene.objects:
        if textObj.name == "TextMenu":
            textObj.endObject()
 
    #После очистки экрана от старыхобъектов начинаем создавать новый вид меню, открыв нужный json
    with open(bge.logic.expandPath(own["cursorPos"]["newPatchMenu"]), 'r', encoding = 'utf-8') as directMenu:
        JSONmenu = json.load(directMenu)
 
    if own["cursorPos"]["TimerColor"] == 600:
        for obj in JSONmenu["Buttons"]: 
            newSceneObject = scene.addObject(obj.split('|')[0],own)
            newSceneObject.worldPosition = JSONmenu["Buttons"][obj]["coordObj"]
            newSceneObject.color = JSONmenu["Buttons"][obj]["colorObj"]
            own["cursorPos"]["listButton"].append(newSceneObject)
            if "meshButton" in JSONmenu["Buttons"][obj]:
                newSceneObject.replaceMesh(JSONmenu["Buttons"][obj]["meshButton"],True,False)
            if "scaleObj" in JSONmenu["Buttons"][obj]:
                newSceneObject.worldScale = JSONmenu["Buttons"][obj]["scaleObj"]
            if newSceneObject.name in scaleButtonStandart:
                newSceneObject.worldScale = scaleButtonStandart[newSceneObject.name]
         
            #Иногда нужен текст без кнопки, который еще надо прокручивать 
            if newSceneObject.name == "TextMenu":
                #Печатаем текст из txt файла, находя его по нужному пути
                if "TextPath" in JSONmenu["Buttons"][obj]:
                    newSceneObject["Text"] = ""
                 
            #Задаем кнопке проперти (для прокрутки или других изменений объект помечается тегом "objTag"
            #Кнопка, которая должна управлять изиенениями остальных объектов получает ключ "tagObj", что
            #позволяет ей "опознать" объекты, необходимые для воздействия на них
            if "propObj" in JSONmenu["Buttons"][obj]:
                for key in JSONmenu["Buttons"][obj]["propObj"]:
                    if key not in newSceneObject:
                        newSceneObject[key] = JSONmenu["Buttons"][obj]["propObj"][key]
                             
            if "childObj" in JSONmenu["Buttons"][obj]:
                #Если надо добавляем текст, парентим его к кнопке, выставляем его размер
                newObj = scene.addObject(JSONmenu["Buttons"][obj]["childObj"], newSceneObject)
                newObj.worldPosition[2] = newSceneObject.worldPosition[2] + 0.02
                newObj.worldScale = newSceneObject.worldScale
                newObj.setParent(newSceneObject, False, False)
                if "childObjScale" in JSONmenu["Buttons"][obj]:
                    newObj.worldScale = JSONmenu["Buttons"][obj]["childObjScale"]
     
            #Текст на кнопке
            if "textObj" in JSONmenu["Buttons"][obj]:
                newSceneObject["textObj"] = JSONmenu["Buttons"][obj]["textObj"]
                newSceneObject["textRus"] = JSONmenu["Buttons"][obj]["textRus"]
                #Если надо добавляем текст, парентим его к кнопке, выставляем его размер
                newTextObj = scene.addObject("TextMenu", newSceneObject)
                if "IDtext" not in newSceneObject:
                    newSceneObject["IDtext"] = str(id(newTextObj))
                newTextObj.worldScale = [ newSceneObject.worldScale[1], newSceneObject.worldScale[1], newSceneObject.worldScale[1] ]
                #Выставляем локальные координаты текста
                newTextObj.worldPosition[0] -= 0.8*newSceneObject.worldScale[0]
                newTextObj.worldPosition[1] -= 0.3*newSceneObject.worldScale[1]
                newTextObj.worldPosition[2] += 0.01
                #Печатаем текст на кнопке
                if bge.logic.globalDict["textGame"] == "Rus":
                    newTextObj["Text"] = JSONmenu["Buttons"][obj]["textRus"]
                elif bge.logic.globalDict["textGame"] == "Eng":
                    newTextObj["Text"] = JSONmenu["Buttons"][obj]["textObj"]
     
        #В этом блоке содержатся инструкции по замене мешей и текстур фоновых объектов
        if "meshFonObj" in JSONmenu:
            scene.objects["FonObj"].replaceMesh(JSONmenu["meshFonObj"],True,False)
        #Для монитора (плоскости под фоном) можно задать свой цвет, меш, и координаты
        if "meshMonitor" in JSONmenu:
            scene.objects["Monitor"].replaceMesh(JSONmenu["meshMonitor"],True,False)
        if "coordMonitor" in JSONmenu:
            scene.objects["Monitor"].worldPosition = JSONmenu["coordMonitor"]
        if "scaleMonitor" in JSONmenu:
            scene.objects["Monitor"].worldScale = JSONmenu["scaleMonitor"]
        if "colorMonitor" in JSONmenu:
            scene.objects["Monitor"].color = JSONmenu["colorMonitor"]
        #Замена текстур фоновой плоскости и монитора (необходим для отображения карты террайна и закрытия отверстий в плоскости общего фона)         
        if "pathFonObjTexture" in JSONmenu:
            FonObj = scene.objects["FonObj"]
            own["cursorPos"]["PathFon"] = JSONmenu["pathFonObjTexture"]
            FonChanging(own, FonObj)
        if "pathMonitorTexture" in JSONmenu:
            Monitor = scene.objects["Monitor"]
            own["cursorPos"]["PathFonMonitor"] = JSONmenu["pathMonitorTexture"]
            FonChanging(own, FonObj)
             
        own["cursorPos"]["newPatchMenu"] = own["cursorPos"]["oldPatchMenu"]
       
#Функция смены текстуры фона меню
def FonChanging(own, FonObj):
    pathFon = own["cursorPos"]["PathFon"]
    if FonObj.name == "Monitor": 
        pathFon = own["cursorPos"]["PathFonMonitor"]
    ID = bge.texture.materialID(FonObj, "IM" + FonObj.name + ".jpg")
    object_texture = bge.texture.Texture(FonObj, ID)
    url = bge.logic.expandPath(pathFon)
    new_source = bge.texture.ImageFFmpeg(url)
    bge.logic.texture = object_texture
    bge.logic.texture.source = new_source
    bge.logic.texture.refresh(False)
    FonObj[FonObj.name] = new_source
           
#Функция сработки клика по кнопке - УЗЛОВАЯ ФУНКЦИЯ для buttonMove
def on_click(own):
    #Сработка кнопки идет, когда курсор находится в определенной области, с заданными координатами,
    #проверка которых идет в зависимости от размера кнопки и ее центра, с координатами, уже данными
    #в списке при загрузке и расстановке деталей меню
    for button in own["cursorPos"]["listButton"]:
        if button.worldPosition[0]-button.worldScale[0] < CursorMenu.worldPosition[0] < button.worldPosition[0]+button.worldScale[0]:
            if button.worldPosition[1]-button.worldScale[1] < CursorMenu.worldPosition[1] < button.worldPosition[1]+button.worldScale[1]:
                own["cursorPos"]["indexButton"] = str(id(button))
                own["cursorPos"]["animationButton"] = 1
                buttonMove(own, button)
                     
#Фунция работы кнопки при ее нажатии, действия, направленные на смену разделов или объектов емню, старт или выход из игры   
def buttonJob(own):
    own["cursorPos"]["animationButton"] += 1
    buttonText = None
 
    try:         
        #Находим кнопку под курсором - область координат, ограниченная размером кнопки
        button = scene.objects.from_id(int(own["cursorPos"]["indexButton"]))
     
        #Находим надписи (если есть) и окрашиваем и нажатую кнопку и ее надпись
        if "IDtext" in button:
            buttonText = scene.objects.from_id(int(button["IDtext"]))
        if own["cursorPos"]["animationButton"] == 2:
            button.color = [1.0, 0.0, 1.0, 1.0]
            if buttonText != None:
                buttonText.color = [1.0, 0.0, 1.0, 1.0]
     
        #Окрашиваем незадействованные кнопки в исходный цвет вместе с надписями (если имеются)
        for obj in own["cursorPos"]["listButton"]:
            if obj != button:
                obj.color = [1.0, 1.0, 1.0, 1.0]
                if "IDtext" in obj:
                    scene.objects.from_id(int(obj["IDtext"])).color = [1.0, 1.0, 1.0, 1.0]
     
        #Двигаем кнопку, обнаруживаем и используем ее свойства для смены меню, картинок и сцен 
        if 1 < own["cursorPos"]["animationButton"] < 3:
            button.applyMovement([0.2,-0.2,0.0],True)
            if buttonText != None:
                buttonText.applyMovement([0.2,-0.2,0.0],True)
        if 3 < own["cursorPos"]["animationButton"] < 5:
            button.applyMovement([-0.2, 0.2, 0.0],True)
            if buttonText != None:
                buttonText.applyMovement([-0.2,0.2,0.0],True)
        if own["cursorPos"]["animationButton"] > 4:
            own["cursorPos"]["indexButton"] = ""
            own["cursorPos"]["animationButton"] = 0
            if "GAME_body" in button:
                #Блок для пути к файлу миссии при нажатии кнопки выбора миссии
                if "BGE_globalDict" in button["GAME_body"]:
                    if button["GAME_body"]["BGE_globalDict"] == "pathNameMission":
                        bge.logic.globalDict['currentMission'] = own["cursorPos"]["pathNameMission"]["key_" + str(id(button))]
                #Кнопка выхода из игры
                if button["GAME_body"] == "Stop_Game":
                    bge.logic.endGame()
                #Кнопка перехода на игровую сцену
                if button["GAME_body"] == "Start_Game":
                    own["cursorPos"]["addSceneGame"] = 1
                #Кнопка выбора языка игры
                if "languageGame" in button["GAME_body"]:
                    bge.logic.globalDict["textGame"] = button["GAME_body"]["languageGame"]
                #Это смена пути к файлу раздела меню - меняется само меню, его види кнопки, для кнопки "Назад" или кнопок ивыбора других разделов
                #вроде "Миссии", "Кампании", "Справки" и тому подобных, при котором не происходит выхода из игры
                if "pathJSON" in button["GAME_body"]:
                    own["cursorPos"]["TimerColor"] = 595
                    own["cursorPos"]["newPatchMenu"] = button["GAME_body"]["pathJSON"]
                 
                #Многоступенчатый процесс "опознания" и сравнения тэгов кнопок и текста для их передвижения
                if "tagObj" in button["GAME_body"]:
                    for scrollObj in own["cursorPos"]["listButton"]:
                        if "GAME_body" in scrollObj:
                            #Проверяем совпадение тэгов после их обнаружения
                            if "objTag" in scrollObj["GAME_body"]:
                                #Теперь передвигаем найденный объект
                                if scrollObj["GAME_body"]["objTag"] == button["GAME_body"]["tagObj"]:
                                    if "scrollTagObj" in button["GAME_body"]:
                                        scrollObj.worldPosition[0] += button["GAME_body"]["scrollTagObj"][0]
                                        scrollObj.worldPosition[1] += button["GAME_body"]["scrollTagObj"][1]
                                     
                                        if "nullCoordX" in scrollObj["GAME_body"]:
                                            if scrollObj.worldPosition[0] < scrollObj["GAME_body"]["nullCoordX"]:
                                                scrollObj.worldPosition[0] = scrollObj["GAME_body"]["nullCoordX"]
                                            if scrollObj.worldPosition[0] > own["cursorPos"]["limitXscroll"]:
                                                scrollObj.worldPosition[0] -= scrollObj["Game_body"]["limitXscroll"]
                                                if "listDriverObj" in scrollObj["GAME_body"]:
                                                    for scrollChild in scrollObj["GAME_body"]["listDriverObj"]:
                                                        scrollChild.worldPosition[0] += scrollObj["GAME_body"]["scaleAddObj"][0]*button["GAME_body"]["valueButton"] #     own["cursorPos"]["limitXscroll"]
                                         
                                        if "nullCoordY" in scrollObj["GAME_body"]:
                                            #print(scrollObj.name)
                                            if scrollObj.worldPosition[1] < scrollObj["GAME_body"]["nullCoordY"]:
                                                scrollObj.worldPosition[1] = scrollObj["GAME_body"]["nullCoordY"]
                                            if scrollObj.worldPosition[1] > scrollObj["Game_body"]["limitYscroll"]:
                                                scrollObj.worldPosition[1] -= own["cursorPos"]["limitYscroll"]
                                                if "listDriverObj" in scrollObj["GAME_body"]:
                                                    for scrollChild in scrollObj["GAME_body"]["listDriverObj"]:
                                                        scrollChild.worldPosition[1] += scrollObj["GAME_body"]["scaleAddObj"][1]*button["GAME_body"]["valueButton"]  #own["cursorPos"]["limitYscroll"]
                             
                                #Изменение текста на объекте TextMenu, например при смене текста справки для видов юнитов
                                if "changTextTagObj" in button["GAME_body"]:
                                    if "pathButtonTXT" not in button["GAME_body"]["changTextTagObj"]:
                                        with open(bge.logic.expandPath(button["GAME_body"]["changTextTagObj"]+bge.logic.globalDict["textGame"]+".txt"), 'r', encoding = 'utf-8') as MenuHelpTXT:
                                            scrollObj["Text"] = MenuHelpTXT.read()
                                        #print(scrollObj.name)
                                        #with open(bge.logic.expandPath(button["GAME_body"]["changTextTagObj"]+bge.logic.globalDict["textGame"]+".txt"), 'r', encoding = 'utf-8').readlines() as changText:
                                            #own["cursorPos"]["limitYscroll"] = scrollObj.worldScale[1]*len(changText)
                                         #   scrollObj["Game_body"]["limitYscroll"] = scrollObj.worldScale[1]*len(changText)
                                    else:
                                        with open(bge.logic.expandPath(own["cursorPos"]["pathButtonTXT"]["key_"+str(id(button))]+bge.logic.globalDict["textGame"]+".txt"), 'r', encoding = 'utf-8') as MenuHelpTXT:
                                            scrollObj["Text"] = MenuHelpTXT.read()
                                     
                                if "changButtonTagObj" in button["GAME_body"]:
                                    if "listDriverObj" in scrollObj["GAME_body"]:
                                        own["cursorPos"]["pathButtonTXT"] = {}
                                        #Убираем кнопки из списка драйвера объекта
                                        if len(scrollObj["GAME_body"]["listDriverObj"]) != 0:
                                            for objOLD in scrollObj["GAME_body"]["listDriverObj"]:
                                                if objOLD in own["cursorPos"]["listButton"]:
                                                    own["cursorPos"]["listButton"].remove(objOLD)
                                                #scrollObj["GAME_body"]["listDriverObj"].remove(objOLD)
                                                objOLD.endObject()
                                            own["cursorPos"]["valueButtonCoord"] = [0.0,0.0,0.0]
                                            scrollObj["GAME_body"]["listDriverObj"] = []
                                     
                                        with open(bge.logic.expandPath('//NodePath.json'), 'r', encoding = 'utf-8') as NODEPATH:
                                            jsonButtonPath = json.load(NODEPATH)
                                            for pathButton in jsonButtonPath:
                                                if button["GAME_body"]["changButtonTagObj"] in pathButton:
                                                    if ".json" in pathButton:
                                                        newObjGen = scene.addObject(scrollObj["GAME_body"]["addObjName"], scrollObj)
                                                        scrollObj["GAME_body"]["listDriverObj"].append(newObjGen)
                                                        own["cursorPos"]["listButton"].append(newObjGen)
                                                        newObjGen.replaceMesh(scrollObj["GAME_body"]["meshAddObj"], True, False)
                                                        newObjGen.worldScale = scrollObj["GAME_body"]["scaleAddObj"]
                                                        own["cursorPos"]["valueButtonCoord"][0] += scrollObj["GAME_body"]["valueCoord"][0]
                                                        own["cursorPos"]["valueButtonCoord"][1] += scrollObj["GAME_body"]["valueCoord"][1]
                                                        own["cursorPos"]["valueButtonCoord"][2] += scrollObj["GAME_body"]["valueCoord"][2]
                                                        newObjGen.worldPosition[0] += own["cursorPos"]["valueButtonCoord"][0]
                                                        newObjGen.worldPosition[1] += own["cursorPos"]["valueButtonCoord"][1]
                                                        newObjGen.worldPosition[2] += own["cursorPos"]["valueButtonCoord"][2]
                                                     
                                                        if "textChild" in scrollObj["GAME_body"]:
                                                            with open(bge.logic.expandPath(pathButton), 'r', encoding = 'utf-8') as textButton:
                                                                jsonTextButton = json.load(textButton)
                                                                newTextGen = scene.addObject("TextMenu", newObjGen)
                                                                scrollObj["GAME_body"]["listDriverObj"].append(newTextGen)
                                                                own["cursorPos"]["listButton"].append(newTextGen)
                                                                newTextGen.worldScale = scrollObj["GAME_body"]["textChild"]
                                                                newTextGen.color = scrollObj["GAME_body"]["textChildColor"]
                                                                if bge.logic.globalDict["textGame"] == "Rus":
                                                                    newTextGen["Text"] = jsonTextButton["textRus"]
                                                                elif bge.logic.globalDict["textGame"] == "Eng":
                                                                    newTextGen["Text"] = jsonTextButton["textEng"]
                                                                #Выставляем локальные координаты текста
                                                                newTextGen.worldPosition[0] -= 0.8*newObjGen.worldScale[0]
                                                                newTextGen.worldPosition[1] -= 0.15*newObjGen.worldScale[1]
                                                             
                                                        if "addObjGAME_body" in scrollObj["GAME_body"]:
                                                            newObjGen["GAME_body"] = scrollObj["GAME_body"]["addObjGAME_body"]
                                                            if "BGE_globalDict" in newObjGen["GAME_body"]:
                                                                if newObjGen["GAME_body"]["BGE_globalDict"] == "pathNameMission":
                                                                    if "key_" + str(id(newObjGen)) not in own["cursorPos"]["pathNameMission"]:
                                                                        own["cursorPos"]["pathNameMission"]["key_" + str(id(newObjGen))] = pathButton
                                                                        #print(own["cursorPos"]["pathNameMission"])   
                                                            if "key_" + str(id(newObjGen)) not in own["cursorPos"]["pathButtonTXT"]:
                                                                own["cursorPos"]["pathButtonTXT"]["key_" + str(id(newObjGen))] = pathButton.split(".json")[0] + "_"
                                                                                                           
    except:
        print(error)
        own["cursorPos"]["indexButton"] = ""
 
#Функция звукового сопровождения и выдачи на кнопку свойств проперти
def buttonMove(own, button):
    cont = bge.logic.getCurrentController()
    audioTemp = cont.actuators['SoundClick']
 
    if "GAME_body" in button:
        if "scrollTagObj" not in button["GAME_body"]:
            cont.activate(audioTemp)
 
    if "GLOBAL_DICT_name" in button:
        if "GLOBAL_DICT_body" in button:
            if button["GLOBAL_DICT_name"] not in bge.logic.globalDict:
                bge.logic.globalDict[button["GLOBAL_DICT_name"]] = button["GLOBAL_DICT_body"]
            else:
                bge.logic.globalDict[button["GLOBAL_DICT_name"]] = button["GLOBAL_DICT_body"]
             
#Поиск путей к файлам, выстраивание списков, ОЧЕНЬ ВАЖНОЕ СТАРТОВОЕ ДЕЙСТВИЕ
startPath()

Функция startPath срабатывает всегда только один раз, отыскивая пути и файлы и добавляя их в список файла json NodePath. Это - ключ ко всему. Там расположены пути к файлам blend и файлы json  с описаниями объектов. Из-за этого файла я и решил в итоге фактически все начать с нуля. Пусть объекты сами ищут пути к нужным данным, вскрывают нужные файлы и действуют по заданным инструкциям. Раньше надо было это делать, ну да ладно, опыт сын ошибок трудных...
Фото были взяты из Сети, временно, потом будут заменены на скрины из самой игры.


Меню справки для юнитов разных "сред". Вверху - меню выбора миссии с коротким описанием оной.

Так пока и не доделанное меню редактора.

Стартовое меню.