Droid Game 3D - Third Person Shooter!

Hi! I create release Droid game! 3.0.7 update!
Screenshot :


New code :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from panda3d.core import *
from direct.gui.OnscreenText import OnscreenText
from direct.gui.OnscreenImage import OnscreenImage
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
import random
import sys
import time
import os
import math


VERSION = '3.0.7'


settings = ['window-title Droid Game ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = loader.loadMusic('./sounds/crack.wav') # звук взрыва корабля
        self.shotSound = loader.loadMusic('./sounds/shot.wav') # звук выстрела
        self.startSound = loader.loadMusic('./sounds/started.wav') # звук запуска
        self.errorSound = loader.loadMusic('./sounds/error.wav') # звук запуска
        self.errorSound.setVolume(1) # полная громкость звука
        self.startSound.setVolume(1) # полная громкость звука
        self.crackSound.setVolume(1) # полная громкость звука
        self.shotSound.setVolume(1) # полная громкость звука
        self.startSound.play() # играем звук запуска игры

        self.button_start = DirectButton(pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = DirectButton(pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме      

        self.slider_sun = DirectSlider(pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = self.addTitle("Droid game релиз " + VERSION)
        
        # Напечатаем управление
        self.inst1 = self.addInstructions(0.95, u"[ESC]: Выход")
        self.inst2 = self.addInstructions(0.90, u"[Left Arrow]: Поворот дроида в лево")
        self.inst3 = self.addInstructions(0.85, u"[Right Arrow]: Поворот дроида в право")
        self.inst4 = self.addInstructions(0.80, u"[Up Arrow]: Вперед")
        self.inst5 = self.addInstructions(0.75, u"[a]: поворот камеры в право")
        self.inst6 = self.addInstructions(0.70, u"[d]: поворот камеры в лево")
        self.inst7 = self.addInstructions(0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд")
        self.inst8 = self.addInstructions(0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд")
        self.inst9 = self.addInstructions(0.55, u"[a + Up arrow]: поворот камеры в право и вперёд")
        self.inst10 = self.addInstructions(0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд")
        self.inst11 = self.addInstructions(0.45, u"[space]: стрельба")
        self.inst12 = self.addInstructions(0.40, u"[s]: достать фонарик")
        self.inst13 = self.addInstructions(0.35, u"[w]: убрать оружие")
        self.inst14 = self.addInstructions(0.30, u"[f]: полный разгон(ОПАСНО!)")
        self.inst15 = self.addInstructions(0.25, u"[0]: пожаротушительная система")

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        self.droid = Actor("models/player/seeker.egg") # Загружаем модель игрока (созданная в blender)
        self.enemy = Actor("models/enemies/r2d2/r2d2.egg") # Загружаем модель помощника (созданная в blender)
        self.droid.reparentTo(render) # Перемещаем модель игрока в наш мир.
        self.enemy.reparentTo(render) # Перемещаем модель помощника в наш мир.
        self.droid.setScale(1) # Делаем размер игрока на 1
        self.enemy.setScale(1) # Делаем размер помощника на 1
        self.droid.setPos(droidStartPos) # Ставим позицию игрока на ранее созданую нами позицию
        self.enemy.setPos(enemyStartPos) # Ставим позицию помощника на ранее созданую нами позицию

        self.weapon = Actor('models/weapon/lightsaber.egg') # загрузим оружие
        self.bullet = Actor('models/weapon/bullet/bullet.egg.pz') # Загрузим пулю
        self.flash = Actor('models/whishlyflash/handlamp.egg') # Загрузим фонарик
               
        self.weapon.reparentTo(render) # переместим оружие в наш мир
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.weapon.setScale(.5) # зададим размер пушки
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = Actor('models/fighter/fighter.egg') # загрузим модельку истребителя
        self.fighter.reparentTo(render) # инициализируем итребитель
        self.fighter.setPos(0, 90, 0) # позиция истребителя
                

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon.setPos(self.weapon_pos) # Поместим оружие чуть выше игрока

        self.cube = Actor('models/block/crate.egg') # загружаем блок
        self.cube.reparentTo(render)
        self.cube.setPos(0, -0, 0) # позиция ящика
        self.cube.setScale(0.7)
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.t = loader.loadModel("models/weapon/lightsaber.egg") # Загрузим ещё оружие
        self.t.reparentTo(render) # Переместим оружие в наш мир
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля
        self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.motor_pos1) # мотор 1
        self.motor2 = self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.motor_pos2) # мотор 2

        self.state_info2 = OnscreenText(text='', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/lighting.vert", "./shaders/lighting.frag")) # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 


    def fight_force(self):
        # сдвижение истребителя за нашим кораблём(очень простым способом я сделал так, чтобы истребитель зажимал нас(летал в нашем квадрате)) и зажимает он реально, сближая к нам свой квадрат
        self.fighter.setY(self.fighter.getY() - self.speed) 
        self.fighter.setX(self.fighter.getX() - 10)    

        print(self.fighter.getX())

    def loadParticleConfig(self, filename, pos):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(self.t)
        self.p.setPos(pos)

    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись
        # перемещаем все обьекты за кораблём
        self.environ.setY(self.environ.getY() - self.speed) # несёмся вправо
        self.droid.setY(self.droid.getY() - self.speed) # сдвигаем дроида за кораблём
        self.enemy.setY(self.enemy.getY() - self.speed) # сдфигаем помощника за кораблём
        self.weapon.setY(self.weapon.getY() - self.speed) # перемещаем оружие
        self.flash.setY(self.flash.getY() - self.speed) # перемещаем фонарик
        self.cube.setY(self.cube.getY() - self.speed) # перемещаем ящик
        self.fight_force()

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            self.loadParticleConfig('special_effects/steam/steam.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = OnscreenText(text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
        else:
            self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            self.loadParticleConfig('special_effects/steam_critic/steam.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение
            self.state_info2 = OnscreenText(text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = OnscreenText(text='падаем!', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            self.loadParticleConfig('special_effects/fountain/fountain.ptf', self.fountain_pos)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            self.loadParticleConfig('special_effects/fountain/fountain.ptf', self.fountain_pos)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

Structure release :
./models :
./models/world :frowning: Now, download panda3d art from https://www.panda3d.org/download/noversion/art-gallery.zip
Go to the cat-environment folder. In it you will see a millenium falcon folder. Take all the files from it and put it in the directory ./models/world)
./models/block : block.zip (269.6 KB)
./models/enemies : enemies.zip (381.4 KB)
./models/fighter : fighter.zip (36.9 KB)
./models/player : player.zip (644.6 KB)
./models/sky : sky.zip (12.5 KB)
./models/weapon : weapon.zip (234.3 KB)
./models/whishlyflash : whishlyflash.zip

./fonts : fonts.zip (192.5 KB)
./sounds : sounds.zip (1.8 MB)
./special_effects : special_effects.zip (19.3 KB)
./tex : tex.zip (95.1 KB)
./shaders : shaders.zip (1.8 KB)

droid.py :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from panda3d.core import *
from direct.gui.OnscreenText import OnscreenText
from direct.gui.OnscreenImage import OnscreenImage
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
import random
import sys
import time
import os
import math


VERSION = '3.0.7'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = loader.loadMusic('./sounds/crack.wav') # звук взрыва корабля
        self.shotSound = loader.loadMusic('./sounds/shot.wav') # звук выстрела
        self.startSound = loader.loadMusic('./sounds/started.wav') # звук запуска
        self.errorSound = loader.loadMusic('./sounds/error.wav') # звук запуска
        self.errorSound.setVolume(1) # полная громкость звука
        self.startSound.setVolume(1) # полная громкость звука
        self.crackSound.setVolume(1) # полная громкость звука
        self.shotSound.setVolume(1) # полная громкость звука
        self.startSound.play() # играем звук запуска игры

        self.button_start = DirectButton(pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = DirectButton(pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме      

        self.slider_sun = DirectSlider(pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = self.addTitle("Droid game релиз " + VERSION)
        
        # Напечатаем управление
        self.inst1 = self.addInstructions(0.95, u"[ESC]: Выход")
        self.inst2 = self.addInstructions(0.90, u"[Left Arrow]: Поворот дроида в лево")
        self.inst3 = self.addInstructions(0.85, u"[Right Arrow]: Поворот дроида в право")
        self.inst4 = self.addInstructions(0.80, u"[Up Arrow]: Вперед")
        self.inst5 = self.addInstructions(0.75, u"[a]: поворот камеры в право")
        self.inst6 = self.addInstructions(0.70, u"[d]: поворот камеры в лево")
        self.inst7 = self.addInstructions(0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд")
        self.inst8 = self.addInstructions(0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд")
        self.inst9 = self.addInstructions(0.55, u"[a + Up arrow]: поворот камеры в право и вперёд")
        self.inst10 = self.addInstructions(0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд")
        self.inst11 = self.addInstructions(0.45, u"[space]: стрельба")
        self.inst12 = self.addInstructions(0.40, u"[s]: достать фонарик")
        self.inst13 = self.addInstructions(0.35, u"[w]: убрать оружие")
        self.inst14 = self.addInstructions(0.30, u"[f]: полный разгон(ОПАСНО!)")
        self.inst15 = self.addInstructions(0.25, u"[0]: пожаротушительная система")

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        self.droid = Actor("models/player/seeker.egg") # Загружаем модель игрока (созданная в blender)
        self.enemy = Actor("models/enemies/r2d2/r2d2.egg") # Загружаем модель помощника (созданная в blender)
        self.droid.reparentTo(render) # Перемещаем модель игрока в наш мир.
        self.enemy.reparentTo(render) # Перемещаем модель помощника в наш мир.
        self.droid.setScale(1) # Делаем размер игрока на 1
        self.enemy.setScale(1) # Делаем размер помощника на 1
        self.droid.setPos(droidStartPos) # Ставим позицию игрока на ранее созданую нами позицию
        self.enemy.setPos(enemyStartPos) # Ставим позицию помощника на ранее созданую нами позицию

        self.weapon = Actor('models/weapon/lightsaber.egg') # загрузим оружие
        self.bullet = Actor('models/weapon/bullet/bullet.egg.pz') # Загрузим пулю
        self.flash = Actor('models/whishlyflash/handlamp.egg') # Загрузим фонарик
               
        self.weapon.reparentTo(render) # переместим оружие в наш мир
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.weapon.setScale(.5) # зададим размер пушки
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = Actor('models/fighter/fighter.egg') # загрузим модельку истребителя
        self.fighter.reparentTo(render) # инициализируем итребитель
        self.fighter.setPos(0, 90, 0) # позиция истребителя
                

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon.setPos(self.weapon_pos) # Поместим оружие чуть выше игрока

        self.cube = Actor('models/block/crate.egg') # загружаем блок
        self.cube.reparentTo(render)
        self.cube.setPos(0, -0, 0) # позиция ящика
        self.cube.setScale(0.7)
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.t = loader.loadModel("models/weapon/lightsaber.egg") # Загрузим ещё оружие
        self.t.reparentTo(render) # Переместим оружие в наш мир
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля
        self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.motor_pos1) # мотор 1
        self.motor2 = self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.motor_pos2) # мотор 2

        self.state_info2 = OnscreenText(text='', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/lighting.vert", "./shaders/lighting.frag")) # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 


    def fight_force(self):
        # сдвижение истребителя за нашим кораблём(очень простым способом я сделал так, чтобы истребитель зажимал нас(летал в нашем квадрате)) и зажимает он реально, сближая к нам свой квадрат
        self.fighter.setY(self.fighter.getY() - self.speed) 
        self.fighter.setX(self.fighter.getX() - 10)    

        print(self.fighter.getX())

    def loadParticleConfig(self, filename, pos):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(self.t)
        self.p.setPos(pos)

    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись
        # перемещаем все обьекты за кораблём
        self.environ.setY(self.environ.getY() - self.speed) # несёмся вправо
        self.droid.setY(self.droid.getY() - self.speed) # сдвигаем дроида за кораблём
        self.enemy.setY(self.enemy.getY() - self.speed) # сдфигаем помощника за кораблём
        self.weapon.setY(self.weapon.getY() - self.speed) # перемещаем оружие
        self.flash.setY(self.flash.getY() - self.speed) # перемещаем фонарик
        self.cube.setY(self.cube.getY() - self.speed) # перемещаем ящик
        self.fight_force()

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            self.loadParticleConfig('special_effects/steam/steam.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = OnscreenText(text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
        else:
            self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            self.loadParticleConfig('special_effects/steam_critic/steam.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение
            self.state_info2 = OnscreenText(text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = OnscreenText(text='падаем!', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            self.loadParticleConfig('special_effects/fountain/fountain.ptf', self.fountain_pos)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            self.loadParticleConfig('special_effects/fountain/fountain.ptf', self.fountain_pos)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

Marconit Engine (ME) - New engine for our game!
Pluses :
Optimization, easy coding, source code!

marcoint_engine.py :

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    

droid.py :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '3.0.8'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме      

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        
        

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.t = loader.loadModel("models/weapon/lightsaber.egg") # Загрузим ещё оружие
        self.t.reparentTo(render) # Переместим оружие в наш мир
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля
        self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.motor_pos1) # мотор 1
        self.motor2 = self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.motor_pos2) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 


    def fight_force(self):
        # сдвижение истребителя за нашим кораблём(очень простым способом я сделал так, чтобы истребитель зажимал нас(летал в нашем квадрате)) и зажимает он реально, сближая к нам свой квадрат
        self.fighter.setY(self.fighter.getY() - self.speed) 
        self.fighter.setX(self.fighter.getX() - 10)    

        print(self.fighter.getX())

    def loadParticleConfig(self, filename, pos):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(self.t)
        self.p.setPos(pos)

    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись
        # перемещаем все обьекты за кораблём
        self.environ.setY(self.environ.getY() - self.speed) # несёмся вправо
        self.droid.setY(self.droid.getY() - self.speed) # сдвигаем дроида за кораблём
        self.enemy.setY(self.enemy.getY() - self.speed) # сдфигаем помощника за кораблём
        self.weapon.setY(self.weapon.getY() - self.speed) # перемещаем оружие
        self.flash.setY(self.flash.getY() - self.speed) # перемещаем фонарик
        self.cube.setY(self.cube.getY() - self.speed) # перемещаем ящик
        self.fight_force()

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            self.loadParticleConfig('special_effects/steam/steam.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            self.loadParticleConfig('special_effects/steam_critic/steam.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            self.loadParticleConfig('special_effects/steam_critic+/fireish.ptf', self.steam_pos)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = OnscreenText(text='падаем!', pos=(-0.8, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            self.loadParticleConfig('special_effects/fountain/fountain.ptf', self.fountain_pos)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            self.loadParticleConfig('special_effects/fountain/fountain.ptf', self.fountain_pos)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

3.0.9 update!
New functional in the our game!

New code marconit_engine.py :

# Самописное API к direct и panda3d

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.particles.Particles import Particles
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    def loadParticleConfig(self, filename, pos, object):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(object)
        self.p.setPos(pos)

New code droid.py :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '3.0.9'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме      

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        
        

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля
        self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 


    def fight_force(self):
        # сдвижение истребителя за нашим кораблём(очень простым способом я сделал так, чтобы истребитель зажимал нас(летал в нашем квадрате)) и зажимает он реально, сближая к нам свой квадрат
        self.fighter.setY(self.fighter.getY() - self.speed) 
        self.fighter.setX(self.fighter.getX() - 10)    

        print(self.fighter.getX())

    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись
        # перемещаем все обьекты за кораблём
        self.environ.setY(self.environ.getY() - self.speed) # несёмся вправо
        self.droid.setY(self.droid.getY() - self.speed) # сдвигаем дроида за кораблём
        self.enemy.setY(self.enemy.getY() - self.speed) # сдфигаем помощника за кораблём
        self.weapon.setY(self.weapon.getY() - self.speed) # перемещаем оружие
        self.flash.setY(self.flash.getY() - self.speed) # перемещаем фонарик
        self.cube.setY(self.cube.getY() - self.speed) # перемещаем ящик
        self.fight_force()

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

4.0.0 update - bug fixes and more !

New marconit_engine.py code :

# Самописное API к direct и panda3d

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    def loadParticleConfig(self, filename, pos, object):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(object)
        self.p.setPos(pos)

droid.py source code :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.0'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме      

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        
        

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля
        self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 



    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

All structure 4.0.0(no update!) :
./models :
./models/world :frowning: Now, download panda3d art from https://www.panda3d.org/download/noversion/art-gallery.zip
Go to the cat-environment folder. In it you will see a millenium falcon folder. Take all the files from it and put it in the directory ./models/world)
./models/block : block.zip (269.6 KB)
./models/enemies : enemies.zip (381.4 KB)
./models/fighter : fighter.zip (36.9 KB)
./models/player : player.zip (644.6 KB)
./models/sky : sky.zip (12.5 KB)
./models/weapon : weapon.zip (234.3 KB)
./models/whishlyflash : whishlyflash.zip

./fonts : fonts.zip (192.5 KB)
./sounds : sounds.zip (1.8 MB)
./special_effects : special_effects.zip (19.3 KB)
./tex : tex.zip (95.1 KB)
./shaders : shaders.zip (1.8 KB)

droid.py source code :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.0'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме      

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        
        

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля
        self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 



    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

marconit_engine.py source code :

# Самописное API к direct и panda3d

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    def loadParticleConfig(self, filename, pos, object):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(object)
        self.p.setPos(pos)

Thank for helping all!

Hey, guys! I create 4.0.1 update - fix force!
New code droid.py :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.1'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме      

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        
        

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля
        self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 



    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)            
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

Screenshot :

Hi! I create 4.0.2 update - en language!!!

./models :
./models/world :frowning: Now, download panda3d art from https://www.panda3d.org/download/noversion/art-gallery.zip
Go to the cat-environment folder. In it you will see a millenium falcon folder. Take all the files from it and put it in the directory ./models/world)
./models/block : block.zip (269.6 KB)
./models/enemies : enemies.zip (381.4 KB)
./models/fighter : fighter.zip (36.9 KB)
./models/player : player.zip (644.6 KB)
./models/sky : sky.zip (12.5 KB)
./models/weapon : weapon.zip (234.3 KB)
./models/whishlyflash : whishlyflash.zip

./fonts : fonts.zip (192.5 KB)
./sounds : sounds.zip (1.8 MB)
./special_effects : special_effects.zip (19.3 KB)
./tex : tex.zip (95.1 KB)
./shaders : shaders.zip (1.8 KB)

droid.py source code :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.2'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме

        self.en = MenuApi.button(self, pos=(0.7, 0, -0.2), text='EN',
                                 scale=.2, pad=(.2, .2),
                                 command=self.en_lang)        

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def en_lang(self):
        '''Изменение языка на английский'''
        self.EN = True

    def load_game(self):
        self.button_start.hide() # удалим кнопку старта
        self.gb_start.hide() # удалим кнопку чёрно белого режима.
        self.title.hide() # удалим заголовок игры
        self.en.hide() # удалим ангоязычную кнопку
        # удалим кнопки управления
        self.inst1.hide()
        self.inst2.hide()
        self.inst3.hide()
        self.inst4.hide()
        self.inst5.hide()
        self.inst6.hide()
        self.inst7.hide()
        self.inst8.hide()
        self.inst9.hide()
        self.inst10.hide()
        self.inst11.hide()
        self.inst12.hide()
        self.inst13.hide()
        self.inst14.hide()
        self.inst15.hide()
        self.slider_sun.hide() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.
        
        

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля

        if not self.EN : # если язык не английский
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        else : # если янглийский язык
            self.state_info = MenuApi.text(self, text='machine :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 



    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            
            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='motor error', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

                
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение

            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='critic state', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение

            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='fall!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

marconit_engine.py source code :

# Самописное API к direct и panda3d

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    def loadParticleConfig(self, filename, pos, object):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(object)
        self.p.setPos(pos)

Hi! I create 4.0.3 update - en language fix and new optimization of game

./models :
./models/world :frowning: Now, download panda3d art from https://www.panda3d.org/download/noversion/art-gallery.zip
Go to the cat-environment folder. In it you will see a millenium falcon folder. Take all the files from it and put it in the directory ./models/world)
./models/block : block.zip (269.6 KB)
./models/enemies : enemies.zip (381.4 KB)
./models/fighter : fighter.zip (36.9 KB)
./models/player : player.zip (644.6 KB)
./models/sky : sky.zip (12.5 KB)
./models/weapon : weapon.zip (234.3 KB)
./models/whishlyflash : whishlyflash.zip

./fonts : fonts.zip (192.5 KB)
./sounds : sounds.zip (1.8 MB)
./special_effects : special_effects.zip (19.3 KB)
./tex : tex.zip (95.1 KB)
./shaders : shaders.zip (1.8 KB)

droid.py source code :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.3'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.EN = False # Не будем включать английский язык
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме

        self.en = MenuApi.button(self, pos=(0.7, 0, -0.2), text='EN',
                                 scale=.2, pad=(.2, .2),
                                 command=self.en_lang)        

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.GB = True

    def en_lang(self):
        '''Изменение языка на английский'''
        self.EN = True

    def load_game(self):
        self.button_start.destroy() # удалим кнопку старта
        self.gb_start.destroy() # удалим кнопку чёрно белого режима.
        self.title.destroy() # удалим заголовок игры
        self.en.destroy() # удалим ангоязычную кнопку
        # удалим кнопки управления
        self.inst1.destroy()
        self.inst2.destroy()
        self.inst3.destroy()
        self.inst4.destroy()
        self.inst5.destroy()
        self.inst6.destroy()
        self.inst7.destroy()
        self.inst8.destroy()
        self.inst9.destroy()
        self.inst10.destroy()
        self.inst11.destroy()
        self.inst12.destroy()
        self.inst13.destroy()
        self.inst14.destroy()
        self.inst15.destroy()
        self.slider_sun.destroy() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля

        if not self.EN : # если язык не английский
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        else : # если янглийский язык
            self.state_info = MenuApi.text(self, text='machine :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 



    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            
            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='motor error', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

                
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение

            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='critic state', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение

            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='fall!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

marconit_engine.py source code :

# Самописное API к direct и panda3d

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    def loadParticleConfig(self, filename, pos, object):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(object)
        self.p.setPos(pos)

Hi! I new update is created coming soon!

Sorry, for no updates, i develop new game - Tank To Tank :frowning:

Hi! I create 4.0.4 update - en language fix and new sounds!

./models :
./models/world :frowning: Now, download panda3d art from https://www.panda3d.org/download/noversion/art-gallery.zip
Go to the cat-environment folder. In it you will see a millenium falcon folder. Take all the files from it and put it in the directory ./models/world)
./models/block : block.zip (269.6 KB)
./models/enemies : enemies.zip (381.4 KB)
./models/fighter : fighter.zip (36.9 KB)
./models/player : player.zip (644.6 KB)
./models/sky : sky.zip (12.5 KB)
./models/weapon : weapon.zip (234.3 KB)
./models/whishlyflash : whishlyflash.zip

./fonts : fonts.zip (192.5 KB)
./sounds : sounds.zip (1.8 MB)
./special_effects : special_effects.zip (19.3 KB)
./tex : tex.zip (95.1 KB)
./shaders : shaders.zip (1.8 KB)

droid.py source code :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.4'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.EN = False # Не будем включать английский язык
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        self.click_sound = GameApi.sound(self, './sounds/GUI_click.wav', 1) # звук нажатия
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме

        self.en = MenuApi.button(self, pos=(0.7, 0, -0.2), text='EN',
                                 scale=.2, pad=(.2, .2),
                                 command=self.en_lang)        

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.click_sound.play() # играем звук клика
        self.GB = True

    def en_lang(self):
        '''Изменение языка на английский'''
        self.click_sound.play() # играем звук клика
        self.EN = True

    def load_game(self):
        self.click_sound.play() # играем звук клика
        self.button_start.destroy() # удалим кнопку старта
        self.gb_start.destroy() # удалим кнопку чёрно белого режима.
        self.title.destroy() # удалим заголовок игры
        self.en.destroy() # удалим ангоязычную кнопку
        # удалим кнопки управления
        self.inst1.destroy()
        self.inst2.destroy()
        self.inst3.destroy()
        self.inst4.destroy()
        self.inst5.destroy()
        self.inst6.destroy()
        self.inst7.destroy()
        self.inst8.destroy()
        self.inst9.destroy()
        self.inst10.destroy()
        self.inst11.destroy()
        self.inst12.destroy()
        self.inst13.destroy()
        self.inst14.destroy()
        self.inst15.destroy()
        self.slider_sun.destroy() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.black_light() # Сделаем выключенный свет

        self.state = 100 # Состояние корабля

        if not self.EN : # если язык не английский
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        else : # если янглийский язык
            self.state_info = MenuApi.text(self, text='machine :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 



    def particle_start(self):
        self.force = True # Поставим, что мы уже разгонялись

        self.state -= 1 # сделаем меньше очков
        self.state_info.hide() # удалим текстовые очки
        
        if self.state != 10 and self.state > 10:
            self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение
            
            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='motor error', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

                
        else:
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide()  # убираем предедущее сообщение

            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='critic state', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
    
        if self.state < 10:
            self.state_info.hide()
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            self.check_loss() # проверяем поражение
            self.steam_pos = random.randrange(0, 5), random.randrange(0, 5) + 300, random.randrange(0, 5)            
            GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
            self.errorSound.play() # играем звук ошибки
            self.state_info2.hide() # убираем предедущее сообщение

            if not self.EN :
                self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля
            else :
                self.state_info2 = MenuApi.text(self, text='fall!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля


    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5) + 500, random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)



marconit_engine.py source code :

# Самописное API к direct и panda3d

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    def loadParticleConfig(self, filename, pos, object):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(object)
        self.p.setPos(pos)

Hi all! My project old to 21 days!
Thank this humans :
@rdb
@panda3dmastercoder (Very!)

See to old forum my project - Droid game with Panda3D

Thank all very!

Hey, new update coming soon! Sorry, i develop Tank To Taank :frowning:

@panda3dmastercoder, where’s 4.0.5 update ??

Sorry :frowning: No 4.0.5 update.

And i sorry for no updates :frowning:
I develop new game - ‘Tanchiki Terrain’
If you like for i sharing this game - LIKE THIS MESSAGE!

1 Like

Directories 4.0.5 update!

./models :
./models/world :frowning: Now, download panda3d art from https://www.panda3d.org/download/noversion/art-gallery.zip
Go to the cat-environment folder. In it you will see a millenium falcon folder. Take all the files from it and put it in the directory ./models/world)
./models/block : block.zip (269.6 KB)
./models/enemies : enemies.zip (381.4 KB)
./models/fighter : fighter.zip (36.9 KB)
./models/player : player.zip (644.6 KB)
./models/sky : sky.zip (12.5 KB)
./models/weapon : weapon.zip (234.3 KB)
./models/whishlyflash : whishlyflash.zip

./fonts : fonts.zip (192.5 KB)
./sounds : sounds.zip (1.8 MB)
./special_effects : special_effects.zip (19.3 KB)
./tex : tex.zip (95.1 KB)
./shaders : shaders.zip (1.8 KB)

Codes 4.0.5 update :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.5'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим6

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.EN = False # Не будем включать английский язык
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        self.click_sound = GameApi.sound(self, './sounds/GUI_click.wav', 1) # звук нажатия
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме

        self.en = MenuApi.button(self, pos=(0.7, 0, -0.2), text='EN',
                                 scale=.2, pad=(.2, .2),
                                 command=self.en_lang)        

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.click_sound.play() # играем звук клика
        self.GB = True

    def en_lang(self):
        '''Изменение языка на английский'''
        self.click_sound.play() # играем звук клика
        self.EN = True

    def load_game(self):
        self.click_sound.play() # играем звук клика
        self.button_start.destroy() # удалим кнопку старта
        self.gb_start.destroy() # удалим кнопку чёрно белого режима.
        self.title.destroy() # удалим заголовок игры
        self.en.destroy() # удалим ангоязычную кнопку
        # удалим кнопки управления
        self.inst1.destroy()
        self.inst2.destroy()
        self.inst3.destroy()
        self.inst4.destroy()
        self.inst5.destroy()
        self.inst6.destroy()
        self.inst7.destroy()
        self.inst8.destroy()
        self.inst9.destroy()
        self.inst10.destroy()
        self.inst11.destroy()
        self.inst12.destroy()
        self.inst13.destroy()
        self.inst14.destroy()
        self.inst15.destroy()
        self.slider_sun.destroy() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.state = 100 # Состояние корабля

        if not self.EN : # если язык не английский
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        else : # если янглийский язык
            self.state_info = MenuApi.text(self, text='machine :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        self.black_light() # Сделаем выключенный свет
        self.collisions() # врубаем коллизии

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 

    def collisions(self):
        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)


    def particle_start(self):
        if not self.pro_machine :
            self.force = True # Поставим, что мы уже разгонялись

            self.state -= 1 # сделаем меньше очков
            self.state_info.hide() # удалим текстовые очки
            
            if self.state != 10 and self.state > 10:
                self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                self.check_loss() # проверяем поражение
                self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
                GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
                self.errorSound.play() # играем звук ошибки
                self.state_info2.hide() # убираем предедущее сообщение
                
                if not self.EN :
                    self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                else :
                    self.state_info2 = MenuApi.text(self, text='motor error', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

                    
            else:
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                self.check_loss() # проверяем поражение
                self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
                GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
                self.errorSound.play() # играем звук ошибки
                self.state_info2.hide()  # убираем предедущее сообщение

                if not self.EN :
                    self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                else :
                    self.state_info2 = MenuApi.text(self, text='critic state', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
        
            if self.state < 10:
                self.state_info.hide()
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                self.check_loss() # проверяем поражение
                self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)            
                GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
                self.errorSound.play() # играем звук ошибки
                self.state_info2.hide() # убираем предедущее сообщение

                if not self.EN :
                    self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, ont=self.font) # Напишем сообщение о состоянии корабля
                else :
                    self.state_info2 = MenuApi.text(self, text='fall!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        else :
            self.pro_machine_engine()

    def pro_machine_engine(self):
        '''Професиональная механика корабля'''
        # Тут нужен джойстик. Если его у вас нет, увы вы не сможете использовать этот режим.
        
        # Управление джойстиком        
        self.accept("gamepad-back", exit)
        self.accept("gamepad-start", exit)
        self.accept("gamepad-face_x", self.reset)
        self.accept("gamepad-face_b", self.action, extraArgs=["face_b"])

    def reset(self):

        self.camera.setPosHpr(0, -200, 10, 0, 0, 0)
        self.environ.setPosHpr(0, -200, 9, 0, 0, 0)

    def moveTaskDjoystic(self, task):
        dt = globalClock.getDt()

        if not self.gamepad:
            return task.cont

        strafe_speed = 85
        vert_speed = 50
        turn_speed = 100

        lstick = self.gamepad.findButton("lstick")
        if lstick.pressed:
            strafe_speed *= 2.0

        strafe = Vec3(0)
        left_x = self.gamepad.findAxis(InputDevice.Axis.left_x)
        left_y = self.gamepad.findAxis(InputDevice.Axis.left_y)
        strafe.x = left_x.value
        strafe.y = left_y.value

        if strafe.lengthSquared() >= 0.01:
            self.camera.setPos(self.camera, strafe * strafe_speed * dt)

        trigger_l = self.gamepad.findAxis(InputDevice.Axis.left_trigger)
        trigger_r = self.gamepad.findAxis(InputDevice.Axis.right_trigger)
        lift = trigger_r.value - trigger_l.value
        self.camera.setZ(self.camera.getZ() + (lift * vert_speed * dt))

        right_x = self.gamepad.findAxis(InputDevice.Axis.right_x)
        right_y = self.gamepad.findAxis(InputDevice.Axis.right_y)

        if abs(right_x.value) >= 0.1 or abs(right_y.value) >= 0.1:
            self.camera.setH(self.camera, turn_speed * dt * -right_x.value)
            self.camera.setP(self.camera, turn_speed * dt * right_y.value)

            self.camera.setR(0)

        return task.cont

    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)

marconit_engine.py source code :

# Самописное API к direct и panda3d

from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectButton import DirectButton
from direct.gui.DirectSlider import DirectSlider
from direct.actor.Actor import Actor
from direct.particles.Particles import Particles
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import *

class MenuApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def slider(self, pos, scale, value, command):
        return DirectSlider(pos=pos, scale=scale, value=value,
                                    command=command)
    
    def button(self, pos, text, scale, pad, command):
        return DirectButton(pos=pos, text=text,
                                   scale=scale, pad=pad,
                                   command=command)

    def addInstruction(self, pos, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1))
        

    def addTitle(self, msg, font):
        return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=font)

    def text(self, pos, text, font, scale):
        return OnscreenText(text=text, pos=pos, scale=scale, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=font)

class GameApi(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

    def light(self):
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)

    def object(self, path, scale, pos):
        self.object = Actor(path)
        self.object.reparentTo(render)
        self.object.setScale(scale)
        self.object.setPos(pos)

        return self.object

    def sound(self, path, volume):
        self.sound = loader.loadMusic(path)
        self.sound.setVolume(volume)

        return self.sound

    def shaders(self, vert, frag):
        render.set_shader(Shader.load(Shader.SL_GLSL, vert, frag))

    def loadParticleConfig(self, filename, pos, object):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))

        self.p.start(object)
        self.p.setPos(pos)

Directories 4.0.6 update!

./models :
./models/world :frowning: Now, download panda3d art from https://www.panda3d.org/download/noversion/art-gallery.zip
Go to the cat-environment folder. In it you will see a millenium falcon folder. Take all the files from it and put it in the directory ./models/world)
./models/block : block.zip (269.6 KB)
./models/enemies : enemies.zip (381.4 KB)
./models/fighter : fighter.zip (36.9 KB)
./models/player : player.zip (644.6 KB)
./models/sky : sky.zip (12.5 KB)
./models/weapon : weapon.zip (234.3 KB)
./models/whishlyflash : whishlyflash.zip

./fonts : fonts.zip (192.5 KB)
./sounds : sounds.zip (1.8 MB)
./special_effects : special_effects.zip (19.3 KB)
./tex : tex.zip (95.1 KB)
./shaders : shaders.zip (1.8 KB)

droid.py :

#!/usr/bin/env python
# -*- coding: utf_8 -*-

# Создано : 22 число, январь, 2021 год

# Импортируем все необходимые инструменты
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerPusher, CollisionSphere, CollisionInvSphere
from panda3d.core import Filename, AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import PointLight, Spotlight
from panda3d.core import CollideMask
from panda3d.core import loadPrcFileData
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import TextPropertiesManager
from panda3d.core import *
from direct.gui.OnscreenImage import OnscreenImage
from direct.interval.IntervalGlobal import Sequence
from direct.interval.IntervalGlobal import Wait
from marconit_engine import MenuApi, GameApi
import random
import sys
import time
import os
import math


VERSION = '4.0.5'


settings = ['window-title Droid Game release ' +  VERSION,
            'fullscreen 0',
            'win-size 1440 900']
            

loadPrcFileData('', settings[0]) # Сделаем не стандартный заголовок (возмём его из переменной settings)
loadPrcFileData('', settings[1]) # не будем делать полноэкранный режим
loadPrcFileData('', settings[2]) # Размер окна

# Создадим главный класс нашей игры
class DroidShooter(ShowBase):
    def __init__(self):
        ShowBase.__init__(self) # Загружаем все селфы из direct
        self.speed = 1800 # скорость двигателя
        self.GB = False # НЕ чёрно-белый режим
        self.EN = False # Не будем включать английский язык
        self.pro_machine = False # Не включаем проффесиональное управление
        self.menu() # звпуск меню
        
    def menu(self):
        
        base.enableParticles() # инициализируем эффект дыма

        self.win.setClearColor((0, 0, 0, 1)) # Закрашиваем поверхность чёрным. Дело в том, что по умолчанию в этом игровом движке поверхность закрашивается серым.
        self.font = loader.loadFont('./fonts/doom_font.ttf') # загрузим шрифт из игры doom
        self.inst_font = loader.loadFont('./fonts/arial.ttf') # загрузим шрифт arial

        self.crackSound = GameApi.sound(self, './sounds/crack.wav', 1) # звук взрыва корабля
        self.shotSound = GameApi.sound(self, './sounds/shot.wav', 1) # звук выстрела
        self.startSound = GameApi.sound(self, './sounds/started.wav', 1) # звук запуска
        self.errorSound = GameApi.sound(self, './sounds/error.wav', 1) # звук запуска
        self.click_sound = GameApi.sound(self, './sounds/GUI_click.wav', 1) # звук нажатия
        
        self.startSound.play() # играем звук запуска игры

        self.button_start = MenuApi.button(self, pos=(0.7, 0, 0.4), text="CTAPT",
                                   scale=.2, pad=(.2, .2),
                                   command=self.load_game) # кнопка запуска игры

        self.gb_start = MenuApi.button(self, pos=(0.7, 0, 0.1), text="GB",
                                   scale=.2, pad=(.2, .2),
                                   command=self.gb_mode) # кнопка запуска игры  в чёрно-белом режиме

        self.en = MenuApi.button(self, pos=(0.7, 0, -0.2), text='EN',
                                 scale=.2, pad=(.2, .2),
                                 command=self.en_lang)
        self.pro_button = MenuApi.button(self, pos=(0.7, 0, -0.5), text='PRO',
                                 scale=.2, pad=(.2, .2),
                                 command=self.pro_system)

        self.slider_sun = MenuApi.slider(self, pos=(0.1, 0, .75), scale=0.8, value=0.5,
                                    command=self.set_sun_interval)


        self.title = MenuApi.addTitle(self, "Droid game релиз " + VERSION, self.font)
        
        # Напечатаем управление
        self.inst1 = MenuApi.addInstruction(self, 0.95, u"[ESC]: Выход", self.inst_font)
        self.inst2 = MenuApi.addInstruction(self, 0.90, u"[Left Arrow]: Поворот дроида в лево", self.inst_font)
        self.inst3 = MenuApi.addInstruction(self, 0.85, u"[Right Arrow]: Поворот дроида в право", self.inst_font)
        self.inst4 = MenuApi.addInstruction(self, 0.80, u"[Up Arrow]: Вперед", self.inst_font)
        self.inst5 = MenuApi.addInstruction(self, 0.75, u"[a]: поворот камеры в право", self.inst_font)
        self.inst6 = MenuApi.addInstruction(self, 0.70, u"[d]: поворот камеры в лево", self.inst_font)
        self.inst7 = MenuApi.addInstruction(self, 0.65, u"[Left Arrow + Up arrow]: поворот дроида в лево и вперёд", self.inst_font)
        self.inst8 = MenuApi.addInstruction(self, 0.60, u"[Right Arrow + Up arrow]: поворот дроида в право и вперёд", self.inst_font)
        self.inst9 = MenuApi.addInstruction(self, 0.55, u"[a + Up arrow]: поворот камеры в право и вперёд", self.inst_font)
        self.inst10 = MenuApi.addInstruction(self, 0.50, u"[d + Up arrow]: поворот камеры в лево и вперёд", self.inst_font)
        self.inst11 = MenuApi.addInstruction(self, 0.45, u"[space]: стрельба", self.inst_font)
        self.inst12 = MenuApi.addInstruction(self, 0.40, u"[s]: достать фонарик", self.inst_font)
        self.inst13 = MenuApi.addInstruction(self, 0.35, u"[w]: убрать оружие", self.inst_font)
        self.inst14 = MenuApi.addInstruction(self, 0.30, u"[f]: полный разгон(ОПАСНО!)", self.inst_font)
        self.inst15 = MenuApi.addInstruction(self, 0.25, u"[0]: пожаротушительная система", self.inst_font)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.

    def set_sun_interval(self):
        self.sun_interval = self.slider_sun.guiItem.getValue() * 100 # загрузим слайдерное значение умноженое на 500, да трачу оптимизацию но мне кажется многим плевать на оптимизацию меню, ибо такого понятия просто нету :)

    def gb_mode(self):
        '''включение чёрнобелого режима'''
        self.click_sound.play() # играем звук клика
        self.GB = True

    def en_lang(self):
        '''Изменение языка на английский'''
        self.click_sound.play() # играем звук клика
        self.EN = True

    def pro_system(self):
        '''Проффесиональное управление'''
        self.click_sound.play() # играем звук клика
        self.pro_machine = True # включаем проффесиональное управление

    def load_game(self):
        self.click_sound.play() # играем звук клика
        self.button_start.destroy() # удалим кнопку старта
        self.gb_start.destroy() # удалим кнопку чёрно белого режима.
        self.title.destroy() # удалим заголовок игры
        self.en.destroy() # удалим ангоязычную кнопку
        self.pro_button.destroy() # удалим кнопку проффесионального упрвления
        # удалим кнопки управления
        self.inst1.destroy()
        self.inst2.destroy()
        self.inst3.destroy()
        self.inst4.destroy()
        self.inst5.destroy()
        self.inst6.destroy()
        self.inst7.destroy()
        self.inst8.destroy()
        self.inst9.destroy()
        self.inst10.destroy()
        self.inst11.destroy()
        self.inst12.destroy()
        self.inst13.destroy()
        self.inst14.destroy()
        self.inst15.destroy()
        self.slider_sun.destroy() # удаляем слайдер солнца
        
        self.keyMap = {
            "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0} # Создадим словарь кнопок, по их нажатию        

        # Рисуем звёзды

        self.sky = loader.loadModel("./models/sky/solar_sky_sphere") # загрузим модель космоса(это сфера)

        self.sky_tex = loader.loadTexture("./tex/stars_1k_tex.jpg") # загрузим текстуру
        self.sky.setTexture(self.sky_tex, 1) # зарендерим текстуру на небо
        self.sky.reparentTo(render) # инициализируем небо
        self.sky.setScale(40000) # расширим небо до максимальной величины panda3d

        self.environ = loader.loadModel("models/world/falcon.egg") # Загрузим уже созданный в blender мир.
        self.environ.reparentTo(render) # Загружаем модель мира в окно

        droidStartPos = (-1, 0, 1.5 ) # Загружаем стартовую позицию игрока в мире.
        enemyStartPos = (-6, 1, 0.5) # Загружаем позицию помощника-дроида.

        self.droid = GameApi.object(self, "models/player/seeker.egg", 1, droidStartPos) # Загружаем модель игрока (созданная в blender)
        self.enemy = GameApi.object(self, "models/enemies/r2d2/r2d2.egg", 1, enemyStartPos) # Загружаем модель помощника (созданная в blender)

        self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 1 # Позиция пушки
        
        self.weapon = GameApi.object(self, 'models/weapon/lightsaber.egg', .5, self.weapon_pos) # загрузим оружие
        self.bullet = GameApi.object(self, 'models/weapon/bullet/bullet.egg.pz', .5, (0, 0, 0)) # Загрузим пулю
        self.flash = GameApi.object(self, 'models/whishlyflash/handlamp.egg', .5, (0, 0, 0)) # Загрузим фонарик
               
        self.bullet.reparentTo(render) # переместим пулю в мир но появится она сама не скоро
        self.flash.reparentTo(render) # переместим фонарик в наш мир
        self.bullet.setScale(.5) # зададим размер пули
        self.flash.setScale(.5) # зададим размер фонарика

        # вражеский истребитель
        self.fighter = GameApi.object(self, 'models/fighter/fighter.egg', 1, (0, 90 , 0)) # загрузим модельку истребителя
                

        self.cube = GameApi.object(self, 'models/block/crate.egg', .7, (0, 0, 0)) # загружаем блок
        self.cube.hprInterval(1.5, (0, 360, 360)).loop()

        self.Intervalcube = self.cube.posInterval(13,
                                                   Point3(0, -500, 0),
                                                   startPos=Point3(0, 10, 0))

        self.Intervalcube.loop()
        
        self.spotlight = camera.attachNewNode(Spotlight("spotlight")) # Конфиги фонарика
        self.spotlight.node().setColor((.3, .3, .3, 1))
        self.spotlight.node().setSpecularColor((0, 0, 0, 1))

        self.floater = NodePath(PandaNode("floater")) 
        self.floater.reparentTo(self.droid)
        self.floater.setZ(2.0)

        self.accept("escape", sys.exit) # При нажатии клавиши Esc выходим.
        
        self.accept("arrow_left", self.setKey, ["left", True]) # При кнопке влево - поворачиваем игрока влево
        self.accept("arrow_right", self.setKey, ["right", True]) # При кнопке вправо - поворачиваем игрока вправо
        self.accept("arrow_up", self.setKey, ["forward", True]) # При кнопке вперёд - идём вперёд
        self.accept("a", self.setKey, ["cam-left", True]) # При кнопке a - разворачиваем камеру вокруг нашей модельки
        self.accept("d", self.setKey, ["cam-right", True]) # При кнопке s - разворачиваем камеру вокруг нашей модельки
        self.accept("arrow_left-up", self.setKey, ["left", False]) # При кнопке вверх+влево - поворачиваем игрока влево и идём вперёд
        self.accept("arrow_right-up", self.setKey, ["right", False]) # При кнопке вверх+право - поворачиваем игрока вправо и идём вперёд
        self.accept("arrow_up-up", self.setKey, ["forward", False]) # При кнопке вверх+вверъ - идём вперёд и идём вперёд
        self.accept("a-up", self.setKey, ["cam-left", False]) # При кнопке a+вперёд - поворачиваем камеру влево и идём вперёд
        self.accept("d-up", self.setKey, ["cam-right", False]) # При кнопке s+вперёд - поворачиваем камеру вправо и идём вперёд

        self.accept("space", self.shot) # при пробеле стреляем
        self.accept("s", self.toggleLights, [[self.spotlight]]) # Включить фонарик
        self.accept("w", self.weapon_hide) # при кнопке w - уберём оружие.

        base.enableParticles() # Включаем инициализацию дыма
        self.p = ParticleEffect() # Включим эффект дыма
        self.accept('f', self.particle_start) # при нажатии f(от force) -  загрузим файл дыма и переместим в конфиг чтобы именно эта анимация стала отображением дыма
        self.accept('0', self.fountain) # при  нажатии кнопок f+o(fountain) включим пожаротушительную систему
        self.accept('f3', self.toggleWireframe) # при нажатии f3 - включаем полигольный режим

        taskMgr.add(self.move, "moveTask") # Добавляем задачу в наш движок

        self.isMoving = False # Ставим значение isMoving на False(Вы можете менять это значение) чтобы игрок изначально стоял.
        # Делаем так, чтобы свет был изначально выключен.
        self.disableMouse() # Отключаем перемещение через мышку
        self.camera.setPos(self.droid.getX(), self.droid.getY() + 1, 3) # Ставим позицию камеры чуть больше позиции игрока

        self.state = 100 # Состояние корабля

        if not self.EN : # если язык не английский
            self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        else : # если янглийский язык
            self.state_info = MenuApi.text(self, text='machine :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
            
                
        self.check_loss() # Проверяем поражение
        self.hide_weapon = False # Поставим, что оружие не убрано
        self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
        self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)

        self.motor_pos1 = 25.238849639892578, 8.962211608886719, 1.5 # позиция первого мотора
        self.motor_pos2 = -20.676218032836914, 10.55816650390625, 1.5 # позиция второго мотора

        self.motor1 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos1, self.environ) # мотор 1
        self.motor2 = GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.motor_pos2, self.environ) # мотор 2

        self.state_info2 = MenuApi.text(self, text='', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        self.force = False # Поставим, что не разгонялись

        self.black_light() # Сделаем выключенный свет
        self.collisions() # врубаем коллизии

        # Если  игрок захотел поиграть в чёрнобелую игру, проверим это
        if self.GB :        
            GameApi.shaders(self, "./shaders/lighting.vert", "./shaders/lighting.frag") # теперь шейдеры работают!
            #render.set_shader(Shader.load(Shader.SL_GLSL, "./shaders/terrain.vert.glsl", "./shaders/terrain.frag.glsl")) 

    def collisions(self):
        # Проверяем коллизии

        # Коллизия корабля(он у нас как главная карта).
        self.cTrav = CollisionTraverser()
        pusher = CollisionHandlerPusher()
        pusher.setHorizontal(True) 
        
        environCenter = self.environ.getBounds().getCenter()
        environRad = self.environ.getBounds().getRadius()
        cNode = CollisionNode("environment")
        cNode.addSolid(CollisionInvSphere(environCenter, environRad))
        environC = self.environ.attachNewNode(cNode)

        # Коллизия дроида.
        droidCenter = self.droid.getBounds().getCenter()
        droidRad = self.droid.getBounds().getRadius()
        cNode = CollisionNode("droid")
        cNode.addSolid(CollisionSphere(droidCenter, droidRad))
        droidC = self.droid.attachNewNode(cNode)
        
        pusher.addCollider(droidC, self.droid)
        self.cTrav.addCollider(droidC, pusher)

        # Коллизия помощника.
        enemyCenter = self.enemy.getBounds().getCenter()
        enemyRad = self.enemy.getBounds().getRadius()
        cNode = CollisionNode("enemy")
        cNode.addSolid(CollisionSphere(enemyCenter, enemyRad))
        enemyC = self.enemy.attachNewNode(cNode)

        pusher.addCollider(enemyC, self.enemy)
        self.cTrav.addCollider(enemyC, pusher)

        # Коллизия вражеского истребителя.
        fighterCenter = self.fighter.getBounds().getCenter()
        fighterRad = self.fighter.getBounds().getRadius()
        cNode = CollisionNode("fighter")
        cNode.addSolid(CollisionSphere(fighterCenter, fighterRad))
        fighterC = self.fighter.attachNewNode(cNode)

        pusher.addCollider(fighterC, self.fighter)
        self.cTrav.addCollider(fighterC, pusher)

        # Коллизия пули.
        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)

        bulletCenter = self.bullet.getBounds().getCenter()
        bulletRad = self.bullet.getBounds().getRadius()
        cNode = CollisionNode("bullet")
        cNode.addSolid(CollisionSphere(bulletCenter, bulletRad))
        bulletC = self.bullet.attachNewNode(cNode)
        
        pusher.addCollider(bulletC, self.bullet)
        self.cTrav.addCollider(bulletC, pusher)

        # Коллизия пушки. Я бы не сказал, что это именно коллизия, скорее это для реалистичности
        weaponCenter = self.weapon.getBounds().getCenter()
        weaponRad = self.weapon.getBounds().getRadius()
        cNode = CollisionNode("weapon")
        cNode.addSolid(CollisionSphere(weaponCenter, weaponRad))
        weaponC = self.weapon.attachNewNode(cNode)
        
        pusher.addCollider(weaponC, self.weapon)
        self.cTrav.addCollider(weaponC, pusher)

        # солнце(новая механика освещения)
        sun = DirectionalLight("sun")
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = render.attach_new_node(sun)
        sun_path.set_pos(10, -10, -10)
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(self.sun_interval, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        render.set_light(sun_path)


    def particle_start(self):
        if not self.pro_machine :
            self.force = True # Поставим, что мы уже разгонялись

            self.state -= 1 # сделаем меньше очков
            self.state_info.hide() # удалим текстовые очки
            
            if self.state != 10 and self.state > 10:
                self.state_info.hide() # убираем предедущее сообщение о жизнях корабля
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                self.check_loss() # проверяем поражение
                self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
                GameApi.loadParticleConfig(self, 'special_effects/steam/steam.ptf', self.steam_pos, self.environ) 
                self.errorSound.play() # играем звук ошибки
                self.state_info2.hide() # убираем предедущее сообщение
                
                if not self.EN :
                    self.state_info2 = MenuApi.text(self, text='двигатель неисправен', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                else :
                    self.state_info2 = MenuApi.text(self, text='motor error', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

                    
            else:
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                self.check_loss() # проверяем поражение
                self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)
                GameApi.loadParticleConfig(self, 'special_effects/steam_critic/steam.ptf', self.steam_pos, self.environ)
                self.errorSound.play() # играем звук ошибки
                self.state_info2.hide()  # убираем предедущее сообщение

                if not self.EN :
                    self.state_info2 = MenuApi.text(self, text='критическое состояние', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                else :
                    self.state_info2 = MenuApi.text(self, text='critic state', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
        
            if self.state < 10:
                self.state_info.hide()
                self.state_info = MenuApi.text(self, text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                self.check_loss() # проверяем поражение
                self.steam_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 5)            
                GameApi.loadParticleConfig(self, 'special_effects/steam_critic+/fireish.ptf', self.steam_pos, self.environ)
                self.errorSound.play() # играем звук ошибки
                self.state_info2.hide() # убираем предедущее сообщение

                if not self.EN :
                    self.state_info2 = MenuApi.text(self, text='падаем!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля
                else :
                    self.state_info2 = MenuApi.text(self, text='fall!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

        else :
            if not self.EN :
                self.pro_info = MenuApi.text(self, text='ПРОФИ!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о том что вы профи :)
                
            else :
                self.pro_info = MenuApi.text(self, text='PRO!', pos=(-0.8, 0.8), scale=0.1, font=self.font) # Напишем сообщение о том что вы профи :)
                
            self.pro_machine_engine()

    def pro_machine_engine(self):
        '''Професиональная механика корабля'''
        # Тут нужен джойстик. Если его у вас нет, увы вы не сможете использовать этот режим.
        self.mgr = InputDeviceManager.get_global_ptr()
        for device in self.mgr.get_devices():
            self.gamepad = device
        
        # Управление джойстиком
        taskMgr.add(self.moveTaskDjoystic, "moveTaskDjoystic")
##            self.accept("gamepad-back", exit)
##            self.accept("gamepad-start", exit)
##            self.accept("gamepad-face_x", self.reset)
##            self.accept("gamepad-face_b", self.action, extraArgs=["face_b"])

    def reset(self):

        self.camera.setPosHpr(0, -200, 10, 0, 0, 0)
        self.environ.setPosHpr(0, -200, 9, 0, 0, 0)

    def moveTaskDjoystic(self, task):
        dt = globalClock.getDt()

        strafe_speed = 85
        vert_speed = 50
        turn_speed = 100

        lstick = self.gamepad.findButton("lstick")
        if lstick.pressed:
            strafe_speed *= 2.0

        strafe = Vec3(0)
        left_x = self.gamepad.findAxis(InputDevice.Axis.left_x)
        left_y = self.gamepad.findAxis(InputDevice.Axis.left_y)
        strafe.x = left_x.value
        strafe.y = left_y.value

        if strafe.lengthSquared() >= 0.01:
            self.camera.setPos(self.camera, strafe * strafe_speed * dt)

        trigger_l = self.gamepad.findAxis(InputDevice.Axis.left_trigger)
        trigger_r = self.gamepad.findAxis(InputDevice.Axis.right_trigger)
        lift = trigger_r.value - trigger_l.value
        self.camera.setZ(self.camera.getZ() + (lift * vert_speed * dt))

        right_x = self.gamepad.findAxis(InputDevice.Axis.right_x)
        right_y = self.gamepad.findAxis(InputDevice.Axis.right_y)

        if abs(right_x.value) >= 0.1 or abs(right_y.value) >= 0.1:
            self.camera.setH(self.camera, turn_speed * dt * -right_x.value)
            self.camera.setP(self.camera, turn_speed * dt * right_y.value)

            self.camera.setR(0)

        return task.cont

    def fountain(self):
        self.state_info2.hide() # убираем текст опасности
        if self.force:
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = MenuApi.text(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)

        else :
            if self.state != 100:
                self.state += 1
                self.state_info.hide()
                self.state_info = OnscreenText(text='корабль :' + str(self.state), pos=(0.5, 0.8), scale=0.1, fg=(1, 1, 1, 1), align=TextNode.ALeft, font=self.font) # Напишем сообщение о состоянии корабля

            self.fountain_pos = random.randrange(0, 5), random.randrange(0, 5), random.randrange(0, 1)    
            GameApi.loadParticleConfig(self, 'special_effects/fountain/fountain.ptf', self.fountain_pos, self.environ)
        
        
    def black_light(self):
        # Выключеный свет
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0, 0, 0, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection((-5, -5, -5))
        directionalLight.setColor((1, 1, 1, 1))
        directionalLight.setSpecularColor((1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))

    def weapon_hide(self):
        # Убрать оружие
        self.hide_weapon = True
        self.weapon.hide()

    def toggleLights(self, lights):
        # Специализированное освещение
        if self.hide_weapon:
            for light in lights:
                if render.hasLight(light):
                    render.clearLight(light)
                else:
                    render.setLight(light)
            
            self.flash.setPos(self.weapon_pos)
        else:
            return


    def shot(self):
        dt = globalClock.getDt() + 0.5 # Cкорость движения
        if not self.hide_weapon : # если оружие убрано то мы не можем стрелять
            self.shotSound.play() # играем звук выстрела
            self.bullet.setPos(self.weapon_pos) # Пуля будет спавнится внутри пушки
            self.bullet.setY(self.bullet, -11 * dt) # Сдвигаем пулю на огромной скорости вперёд

    def check_loss(self):
        if self.state == 0: # если жизней у корабля не осталось
            self.black_light() # включим чёрный свет
            self.weapon_hide() # уберём оружие
            self.info = OnscreenText(text='поражение', style=1, font=self.font,
                                     fg=(1,1,1,1),
                            pos=(-1.3, -0.5), align=TextNode.ALeft, scale=0.3) # напишем о поражении
            self.crackSound.play() # играем звук взрыва


    def setKey(self, key, value):
        self.keyMap[key] = value # Делаем мехaнuзм нажатия клавиш.



    def move(self, task):

        ''' Делаем фуцнкцию движения игрока '''

        dt = globalClock.getDt() - .005 # Cкорость движения

        # Поворот камеры влево и вправо

        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt) # Меняем положеника камеры по икс. Таким образом получается илюзия поворота угла луча. Но на самом деле камера просто перемещается.
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt) # Тоже самое, что и наверху.

        startpos = self.droid.getPos() # Сделаем удобную переменную позиции игрока

        if self.keyMap["left"]:
            self.droid.setH(self.droid.getH() + 300 * dt)
            self.enemy.setY(self.enemy, 1 * dt)
        if self.keyMap["right"]:
            self.droid.setH(self.droid.getH() - 300 * dt)
            self.enemy.setY(self.enemy, -1 * dt)
        if self.keyMap["forward"]:
            self.droid.setY(self.droid, -25 * dt)
            self.enemy.setX(self.enemy, 1 * dt)
            # Сделаем специальное условие проверки убрано оружие или нет
            if not self.hide_weapon:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.weapon.setPos(self.weapon_pos) # Обновляем позицию пушки
            else:
                self.weapon_pos = self.droid.getX(), self.droid.getY() + 0.7, 3
                self.flash.setPos(self.weapon_pos)


        camvec = self.droid.getPos() - self.camera.getPos() # вектор камеры
        camvec.setZ(0) # 0 высота вектора
        camdist = camvec.length() # дистанция камеры от дроида
        camvec.normalize() # нормализируем вектор камеры
        if camdist > 10.0: # если дистанция камеры больше 10, то смещаем камеру за дрооидом
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0 # теперь дистанция будет снова 10
        if camdist < 5.0: # если дистагцтя камеры меньше 5, то...
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist)) # сдвигаем камеру
            camdist = 5.0 # всё обновляем
            
        self.camera.lookAt(self.floater) # вот и пригодился наш floater


        return task.cont # возвращаем задачу

    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1), font=self.inst_font, pos=(-1.3, pos), align=TextNode.ALeft, scale = .04, shadow=(0,0,0,1)) # Функция о том, как управлять

    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1, 1, 1, 1), scale=0.2,
                            parent=base.a2dBottomRight, align=TextNode.ARight,
                            pos=(-0.1, 0.09), shadow=(0, 0, 0, 1), font=self.font) # добавление текстового заголовка с шрифтом и позицией
    

droid = DroidShooter() # Создадим экземпляр класса нашей игры
droid.run() # 3апустим игру


# разроботчики :
# Главный автор : ma3rx
# Помощник : panda3dmastercoder( присоеденился к проекту 31 января, 2021 года, 12:??)
# Рекламщик : rdb (написал первое сообщение на форуме 6 февраля 2021 года, 20:11)