Source code for pyunity.scenes.scene

## Copyright (c) 2020-2022 The PyUnity Team
## This file is licensed under the MIT License.
## See https://docs.pyunity.x10.bz/en/latest/license.html

"""
Class to load, render and manage GameObjects
and their various components.

You should never use the :class:`Scene`
class directly, instead, only use
the :class:`SceneManager` class.

"""

__all__ = ["Scene"]

from ..meshes import MeshRenderer
from ..audio import AudioListener, AudioSource
from ..core import GameObject, Tag, Component
from ..events import EventLoop
from ..files import Behaviour, Asset
from ..values import Vector3, Mathf
from .. import Logger, config
from ..physics.core import CollManager
from ..errors import PyUnityException, ComponentException, GameObjectException
from ..render import Camera, Light, Screen
from pathlib import Path
import os
import sys
import uuid
import time
import inspect

if os.environ["PYUNITY_INTERACTIVE"] == "1":
    import OpenGL.GL as gl

disallowedChars = set(":*/\"\\?<>|")

[docs]def createTask(loop, coro, *args): if inspect.iscoroutinefunction(coro): loop.create_task(coro(*args)) else: loop.call_soon(coro, *args)
[docs]class Scene(Asset): """ Class to hold all of the GameObjects, and to run the whole scene. Parameters ---------- name : str Name of the scene Notes ----- Create a scene using the SceneManager, and don't create a scene directly using this class. """ def __init__(self, name): self.name = name self.mainCamera = GameObject("Main Camera").AddComponent(Camera) self.mainCamera.AddComponent(AudioListener) self.mainCamera.gameObject.scene = self light = GameObject("Light") light.transform.localPosition = Vector3(10, 10, -10) light.transform.LookAtPoint(Vector3.zero()) light.AddComponent(Light) light.scene = self self.gameObjects = [self.mainCamera.gameObject, light]
[docs] def GetAssetFile(self, gameObject): return Path("Scenes") / (self.name + ".scene")
[docs] def SaveAsset(self, ctx): ctx.savers[Scene](self, ctx.project, ctx.filename)
[docs] @staticmethod def Bare(name): """ Create a bare scene. Parameters ---------- name : str Name of the scene Returns ------- Scene A bare scene with no GameObjects """ cls = Scene.__new__(Scene) cls.name = name cls.gameObjects = [] cls.mainCamera = None return cls
@property def rootGameObjects(self): """All GameObjects which have no parent""" return [x for x in self.gameObjects if x.transform.parent is None]
[docs] def Add(self, gameObject): """ Add a GameObject to the scene. Parameters ---------- gameObject : GameObject The GameObject to add. """ if gameObject.scene is not None: raise PyUnityException("GameObject \"%s\" is already in Scene \"%s\"" % (gameObject.name, gameObject.scene.name)) gameObject.scene = self self.gameObjects.append(gameObject)
[docs] def AddMultiple(self, *args): """ Add GameObjects to the scene. Parameters ---------- *args : list A list of GameObjects to add. """ for gameObject in args: self.Add(gameObject)
[docs] def Destroy(self, gameObject): """ Remove a GameObject from the scene. Parameters ---------- gameObject : GameObject GameObject to remove. Raises ------ PyUnityException If the specified GameObject is not part of the Scene. """ if gameObject not in self.gameObjects: raise PyUnityException( "The provided GameObject is not part of the Scene") pending = [a.gameObject for a in gameObject.transform.GetDescendants()] for gameObject in pending: for component in gameObject.GetComponents(Behaviour): component.OnDestroy() for gameObject in pending: if gameObject in self.gameObjects: gameObject.scene = None self.gameObjects.remove(gameObject) if self.mainCamera is not None and gameObject is self.mainCamera.gameObject: Logger.LogLine(Logger.WARN, f"Removing Main Camera from scene {self.name!r}") self.mainCamera = None for gameObject in self.gameObjects: for component in gameObject.components: for saved in component._saved: attr = getattr(component, saved) if isinstance(attr, GameObject): if attr in pending: setattr(component, saved, None) elif isinstance(attr, Component): if attr.gameObject in pending: setattr(component, saved, None)
[docs] def Has(self, gameObject): """ Check if a GameObject is in the scene. Parameters ---------- gameObject : GameObject Query GameObject Returns ------- bool If the GameObject exists in the scene """ return gameObject in self.gameObjects
[docs] def List(self): """Lists all the GameObjects currently in the scene.""" for gameObject in sorted(self.rootGameObjects, key=lambda x: x.name): gameObject.transform.List()
[docs] def FindGameObjectsByName(self, name): """ Finds all GameObjects matching the specified name. Parameters ---------- name : str Name of the GameObject Returns ------- list List of the matching GameObjects """ return [gameObject for gameObject in self.gameObjects if gameObject.name == name]
[docs] def FindGameObjectsByTagName(self, name): """ Finds all GameObjects with the specified tag name. Parameters ---------- name : str Name of the tag Returns ------- list List of matching GameObjects Raises ------ GameObjectException When there is no tag named ``name`` """ if name in Tag.tags: return [gameObject for gameObject in self.gameObjects if gameObject.tag.tagName == name] else: raise GameObjectException( f"No tag named {name}; create a new tag with Tag.AddTag")
[docs] def FindGameObjectsByTagNumber(self, num): """ Gets all GameObjects with a tag of tag ``num``. Parameters ---------- num : int Index of the tag Returns ------- list List of matching GameObjects Raises ------ GameObjectException If there is no tag with specified index. """ if len(Tag.tags) > num >= 0: return [gameObject for gameObject in self.gameObjects if gameObject.tag.tag == num] else: raise GameObjectException( f"No tag at index {num}; create a new tag with Tag.AddTag")
[docs] def FindComponent(self, component): """ Finds the first matching Component that is in the Scene. Parameters ---------- component : type Component type Returns ------- Component The matching Component Raises ------ ComponentException If the component is not found """ for gameObject in self.gameObjects: query = gameObject.GetComponent(component) if query is not None: break if query is None: raise ComponentException( f"Cannot find component {component.__name__} in scene") return query
[docs] def FindComponents(self, component): """ Finds all matching Components that are in the Scene. Parameters ---------- component : type Component type Returns ------- list List of the matching Components """ components = [] for gameObject in self.gameObjects: query = gameObject.GetComponents(component) components.extend(query) return components
[docs] def insideFrustrum(self, renderer): """ Check if the renderer's mesh can be seen by the main camera. Parameters ---------- renderer : MeshRenderer Renderer to test Returns ------- bool If the mesh can be seen """ mesh = renderer.mesh if mesh is None: return False pos = self.mainCamera.transform.position * Vector3(1, 1, -1) directionX = self.mainCamera.transform.rotation.RotateVector( Vector3.right()) * Vector3(1, 1, -1) directionY = self.mainCamera.transform.rotation.RotateVector( Vector3.up()) * Vector3(1, 1, -1) directionZ = self.mainCamera.transform.rotation.RotateVector( Vector3.forward()) * Vector3(1, 1, -1) if renderer.transform.parent is not None: parent = renderer.transform.parent.position else: parent = Vector3.zero() rpmin = renderer.transform.rotation.RotateVector( mesh.min - renderer.transform.localPosition) rpmax = renderer.transform.rotation.RotateVector( mesh.max - renderer.transform.localPosition) rpmin += parent - pos rpmax += parent - pos minZ = rpmin.dot(directionZ) maxZ = rpmax.dot(directionZ) if minZ > self.mainCamera.near or maxZ < self.mainCamera.far: return True minY = rpmin.dot(directionY) maxY = rpmax.dot(directionY) hmin = minZ * 2 * \ Mathf.Tan(self.mainCamera.fov / Screen.size.x * Screen.size.y / 2 * Mathf.DEG_TO_RAD) hmax = maxZ * 2 * \ Mathf.Tan(self.mainCamera.fov / Screen.size.x * Screen.size.y / 2 * Mathf.DEG_TO_RAD) if minY > -hmin / 2 or maxY < hmax / 2: return True minX = rpmin.dot(directionX) maxX = rpmax.dot(directionX) wmin, wmax = hmin * \ Screen.size.x / Screen.size.y, hmax * \ Screen.size.x / Screen.size.y return minX > -wmin / 2 or maxX < wmax / 2
[docs] def startOpenGL(self): self.mainCamera.Resize(*config.size) gl.glEnable(gl.GL_DEPTH_TEST) if config.faceCulling: gl.glEnable(gl.GL_CULL_FACE) else: gl.glDisable(gl.GL_CULL_FACE) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) for gameObject in self.gameObjects: for component in gameObject.components: if isinstance(component, MeshRenderer) and component.mesh is not None: component.mesh.compile() self.mainCamera.setupBuffers()
[docs] def startScripts(self): loop = EventLoop() if config.audio: audioListeners = self.FindComponents(AudioListener) audioListeners = [c for c in audioListeners if c.enabled] if len(audioListeners) == 0: Logger.LogLine( Logger.WARN, "No enabled AudioListeners found, audio is disabled") self.audioListener = None elif len(audioListeners) > 1: Logger.LogLine(Logger.WARN, "Ambiguity in AudioListeners, " + str(len(audioListeners)) + " enabled") self.audioListener = None else: self.audioListener = audioListeners[0] self.audioListener.Init() else: self.audioListener = None for gameObject in self.gameObjects: if not gameObject.enabled: continue for component in gameObject.components: if not component.enabled: continue if isinstance(component, Behaviour): component.Awake() createTask(loop, component.Start) elif isinstance(component, AudioSource): if component.playOnStart: component.Play() # self.physics = any( # isinstance( # component, Rigidbody # ) for gameObject in self.gameObjects for component in gameObject.components # ) self.physics = True # Check is too expensive if self.physics: self.collManager = CollManager() self.collManager.AddPhysicsInfo(self) return loop
[docs] def startLoop(self): Logger.LogLine(Logger.DEBUG, "Physics is", "on" if self.physics else "off") Logger.LogLine(Logger.DEBUG, "Scene " + repr(self.name) + " has started") self.lastFrame = time.perf_counter() self.lastFixedFrame = time.perf_counter()
[docs] def Start(self): """ Start the internal parts of the Scene. Deprecated in 0.9.0. """ self.startScripts() self.startOpenGL()
[docs] def updateScripts(self, loop): """Updates all scripts in the scene.""" from ..input import Input dt = max(time.perf_counter() - self.lastFrame, sys.float_info.epsilon) self.lastFrame = time.perf_counter() if os.environ["PYUNITY_INTERACTIVE"] == "1": Input.UpdateAxes(dt) if self.mainCamera is not None and self.mainCamera.canvas is not None: if self.mainCamera.enabled and self.mainCamera.canvas.enabled: self.mainCamera.canvas.Update(loop) for gameObject in self.gameObjects: if not gameObject.enabled: continue for component in gameObject.components: if not component.enabled: continue if isinstance(component, Behaviour): createTask(loop, component.Update, dt) elif isinstance(component, AudioSource): if component.loop and component.playOnStart: if component.channel and not component.channel.get_busy(): component.Play() for gameObject in self.gameObjects: if not gameObject.enabled: continue for component in gameObject.GetComponents(Behaviour): if component.enabled: createTask(loop, component.LateUpdate, dt)
[docs] def updateFixed(self, loop): dt = max(time.perf_counter() - self.lastFixedFrame, sys.float_info.epsilon) self.lastFixedFrame = time.perf_counter() if self.physics: self.collManager.Step(dt) for gameObject in self.gameObjects: if not gameObject.enabled: continue for component in gameObject.GetComponents(Behaviour): if component.enabled: createTask(loop, component.FixedUpdate, dt)
[docs] def Render(self, loop=None): """ Call the appropriate rendering functions of the Main Camera. Parameters ---------- loop : EventLoop Event loop to run :meth:`Behaviour.OnPreRender` and :meth:`Behaviour.OnPostRender` in. If None, the above methods will not be called. """ if self.mainCamera is None or not self.mainCamera.enabled: gl.glClearColor(0, 0, 0, 1) gl.glClear(gl.GL_COLOR_BUFFER_BIT) return if loop is not None: behaviours = self.FindComponents(Behaviour) for component in behaviours: createTask(loop, component.OnPreRender) renderers = self.FindComponents(MeshRenderer) lights = self.FindComponents(Light) self.mainCamera.renderPass = True self.mainCamera.Render(renderers, lights) if loop is not None: for component in behaviours: createTask(loop, component.OnPostRender)
[docs] def cleanUp(self): """ Called when the scene finishes running, or stops running. """ if self.audioListener is not None: self.audioListener.DeInit() for gameObject in self.gameObjects: for component in gameObject.GetComponents(Behaviour): component.OnDestroy()