What is a logic class

Version 1: basic structure

This example shows how a new package with simple graph nodes can be created and incorporated into a program. As a result, we get a spinning cube.

Similar to the Hello World tutorial, the cube project is also located in the folder, again with the different project files in the corresponding subdirectories for each target platform.

We start with a basic structure as shown in the previous example (create / destroy functions, an app class, a logic class), but we will expand this a little.

In the method we also specify the size of the output surface and thus the window size of 800x600 pixels and define a product name for the new application with.

Bool App :: CubeApp :: Configure (IEngineConfiguration * engineConfig, IFileInterface * fileInterface) {IAppConfiguration * appConfig = engineConfig-> GetAppConfiguration (); engineConfig-> SetProductName ("Cube"); appConfig-> SetWindowTitle ("cube powered by murl engine"); appConfig-> SetDisplaySurfaceSize (800, 600); appConfig-> SetFullScreenEnabled (false); return true; }

In the method of the app class we add a condition that evaluates the selected build configuration (debug / release) so that the "debug" package is only loaded during a debug build.

In addition, we add two lines to load the "startup" and "main" packages and link them to the logic class. This instruction is commented out for the time being, because we have to create the package "main" first. The "startup" package supplied with the tutorial is in the folder and includes a simple, animated loading screen.

Bool App :: CubeApp :: Init (const IAppState * appState) {mLogic = new CubeLogic (appState-> GetLogicFactory ()); ILoader * loader = appState-> GetLoader (); if (Util :: IsDebugBuild ()) {loader-> AddPackage ("debug", ILoader :: LOAD_MODE_STARTUP); } loader-> AddPackage ("startup", ILoader :: LOAD_MODE_STARTUP); // loader-> AddPackage ("main", ILoader :: LOAD_MODE_STARTUP); return true; }

In the declaration file of the logic class, we not only overwrite the base class method, but also the two methods and. The method is called exactly once immediately before the associated package is unloaded (i.e. at the latest when the application is closed). The method is called cyclically with each logic step. Normally there is exactly one logic step per frame. But this can be changed with the object.

protected: virtual Bool OnInit (const Logic :: IState * state); virtual Bool OnDeInit (const Logic :: IState * state); virtual void OnProcessTick (const Logic :: IState * state);

In the method, the "startup" package is first unloaded again. This only serves as a loading indicator and is no longer required after all packages have been loaded. The method remains empty for the time being and the method simply returns.

Bool App :: CubeLogic :: OnInit (const Logic :: IState * state) {state-> GetLoader () -> UnloadPackage ("startup"); if (! AreGraphNodesValid ()) {return false; } state-> SetUserDebugMessage ("Cube Init succeeded!"); return true; } Bool App :: CubeLogic :: OnDeInit (const Logic :: IState * state) {return true; } void App :: CubeLogic :: OnProcessTick (const Logic :: IState * state) {}

The result is a black window with a loading and debugging display.

Output window with loading indicator

Exercises

  • Why is the user debug message not displayed?
  • What happens if the logic class is linked to the "startup" package instead of the "main" package? Why?
  • What happens when I try to load the "main" package?

Next we create the missing resource package "main". To do this, we first create a folder with the desired name and the ending in the existing subdirectory, which already contains the "startup.murlpkg" file:

    Every "murlres" package needs at least one file with the exact name in which the package content is defined. In addition, we need a file in which we can create our graph nodes. The name can be chosen freely - we name it.

    • .xml
    • .xml

    Both files contain an XML-compliant data description and begin with the XML declaration.

    The file defines which resources exist in the package and which instances are created. It begins with the root element and defines an attribute for it. The root element identifies the document as Package Resource Document. The attribute is used to define a unique name for the package. This name can then be used subsequently to create a reference to the package and the resources it contains.

    To note
    Danger! -Attributes must always be unique!

    Two child elements are defined within the root element. The texts in and in brackets are only comments and have no further meaning.

    With the child element, the file is made known as a resource and a unique ID is defined for it. With the child element an instance of this resource is created and added to the scene graph as soon as the package has been loaded.

    <?xml version="1.0" ?> <Package id="package_main"> <!-- Graph resources --> <Resource id="graph_main" fileName="graph_main.xml"/> <!-- Graph instances --> <Instance graphResourceId="graph_main"/> </Package>

    The individual nodes of the graph are defined in the file - and thus our scene.

    The element forms the root element and identifies the document as Graph Resource Document. The individual graph nodes can be listed within the root element. The declarations for the various graph nodes are in the subdirectory.

    <?xml version="1.0" ?> <Graph>

    The following steps are necessary to define our scene:

    • Define the view and thus a drawing area
    • Define the camera and thus the visible area of ​​the virtual world
    • Create a material to describe the appearance of a drawable object
    • Select the created material for the following drawing operations
    • Draw object

    All objects are positioned in the virtual world using a right-handed, three-dimensional Cartesian coordinate system. The zero point is in the center, the X-axis runs horizontally from left to right, the Y-axis runs vertically from bottom to top and the Z-axis from back to front.

    The Murl Engine's coordinate system

    One or more nodes, each with one or more camera nodes (), can exist in a graph. Any number of camera nodes can be assigned to each view node, but each camera has only one assigned view node. While the view node defines the drawing area in the window, the camera node specifies the visible area in the virtual world. This visible area is then drawn in the view area. By default, the view area fills the entire window content.

    First we create such a node and assign it the unique ID. Then we use the node to create a camera with the ID and link it to the previously created view node via the attribute.

    To add nodes to the virtual world of this camera, the camera must be activated with a node. Alternatively, the nodes can also be defined as child nodes within the camera node between the start tag and the end tag (as in this example). In this case the camera is only active for nodes within this sub-graph.

    The position of the camera in the room can be specified with the node and the attributes, and. The attributes,, and can be used to rotate the camera. By default, the camera is at the point (0/0/0) and looks along the z-axis in the direction of minus ∞.

    <View id="view"/> <Camera id="camera" viewId="view" fieldOfViewX="400" nearPlane="400" farPlane="2500" clearColorBuffer="true" > <CameraTransform cameraId="camera" posX="0" posY="0" posZ="800" />

    The information from and defines the distance from the camera to the near plane and to the far plane. The width of the near plane in the X direction is set with. As a result, a visible area of ​​the virtual world is defined as a truncated pyramid ("frustum"), which is then displayed in the display field (view).

    The view frustum of a perspective camera

    Since no entry was made in the Y direction (), the height of the viewport is automatically calculated to match the aspect ratio of the window, resulting in square pixels.

    Instead of, the height could also be specified with. In this case the width of the viewport would be calculated appropriately.

    If both the width and the height are specified, the result may be a distorted image with non-square pixels, depending on the window aspect ratio.

    Specifying ensures that the screen is automatically cleared for each frame before drawing.

    Top view of the view frustum

    The details for the camera were not chosen exactly by chance. The world coordinate window on the Z-plane 0 is exactly twice as large as the coordinate window on the near plane and is therefore exactly 800 coordinate units wide. In a window that, as in our case, has a width of exactly 800 pixels, this results in a 1: 1 assignment between coordinate unit and pixel size on Z level 0.

    This is particularly useful in 2D applications, since moving an object on the Z-plane 0 by one coordinate unit also results in a displacement of exactly one pixel in the window. Moving an object on the near plane by one coordinate unit, on the other hand, would result in a displacement of exactly two pixels.

    In order for an object to be visibly drawn, it must lie within the defined frustum. Particularly when positioning flat surfaces (e.g. nodes), care should be taken to ensure that there is sufficient distance to the near and far plane, as otherwise the calculation inaccuracies can lead to flickering images.

    The next step is to define the material with which the cube is to be drawn. A material consists of

    • a program node for the shader,
    • a material node and
    • optional parameter values ​​for the material.

    The program for the shader is either a ready-made, fixed program () or a self-created shader program (). For the sake of simplicity, we initially only define one with default values ​​and only define the attribute:

    <FixedProgram id="prg_white" />

    The material itself defines attributes that are independent of the actual shader. For now, we'll use a node with default values. We only enter an ID and link it to the previously defined FixedProgram using the attribute:

    <Material id="mat_white" programId="prg_white" />

    The next step is to activate the material using a knot. With the attribute we select the material and assign it one of 128 possible material slots. In our case, all of the following objects that use slot 0 for drawing are now drawn with the material.

    <MaterialState materialId="mat_white" slot="0" />

    The last thing we have to do is create our cube. The ID is defined, the center of the cube is positioned with, and on the coordinate (0/0/0) and the edge length is set to 200 units. The node creates a cube by default and we scale it to the size we want.

    <CubeGeometry id="myCube01" scaleFactor="200" posX="0" posY="0" posZ="0" /> </Camera> </Graph>

    The order of the elements is important in two ways:

    • Reference may only be made to known IDs, i.e. an element must first be made known and only then can it be referenced. In other words, e.g. a cannot be connected to one which is defined "further down" in the XML file or in another XML file that is loaded at a later point in time.
    • State nodes such as, or change the context and apply to all subsequent elements.

    Load package

    Now you only have to remove the comment characters in front of the line in the file so that the newly defined package is also loaded.

    Bool App :: CubeApp :: Init (const IAppState * appState) {mLogic = new CubeLogic (appState-> GetLogicFactory ()); ILoader * loader = appState-> GetLoader (); if (Util :: IsDebugBuild ()) {loader-> AddPackage ("debug", ILoader :: LOAD_MODE_STARTUP); } loader-> AddPackage ("startup", ILoader :: LOAD_MODE_STARTUP); loader-> AddPackage ("main", ILoader :: LOAD_MODE_BACKGROUND, mLogic-> GetProcessor ()); return true; }

    The result is a static, white cube that appears as a square in the center of the window. Since we are looking directly at it from the front, we only see a white square.

    The front of a white cube

    If you compare the two lines for loading the "startup" package and the "main" package, you will notice that the one specified differs.

    Load modes

    There are three different modes for loading a package:

    This is used to be able to display a startup logo or a loading screen as quickly as possible. The packages are loaded before the actual graph is displayed. The screen or window remains black.

    For packages with, the loading takes place in the background. Loaded packages are already displayed. After loading, the packages are added to the graph and the method of any linked logic is called.

    Packages with the specification of are not loaded automatically, but only made known for the time being. The packages can then be loaded and unloaded as required using the and methods. A suitable -object provides e.g. the -object that is passed with or:

    state-> GetLoader () -> LoadPackage ("demand_package"); state-> GetLoader () -> UnloadPackage ("demand_package");

    The individual packages are loaded one after the other in the specified order. It is therefore ensured for a logic object that both the linked package and all previous packages are fully loaded when the method is called.

    Basically, only small packages should be loaded for the startup display and all other packages with or. Therefore, the packages and are usually loaded with and all other packages with, whereby the last package is linked with the logic class.

    Resource Packer

    Another difference between the package and the package is the way the files are presented.

    The package is available as a resource directory with the name in which the individual resource files are located.

    The package is available as a resource package with the name. A resource package is a single file with the extension. This contains one or more resource files that have been packed into the package in binary form.

    The tool can be used to create a resource package from a resource directory. The easiest way to do this is with the dashboard. The command Project → Resource Packer Build creates a resource package from each directory in the folder.

    Alternatively, you can work directly with the Command Line Tool. Depending on the host platform used, the program is located in different subdirectories:

      During development, it is advantageous to work with a resource directory, as every change to a file is automatically adopted and there is no intermediate packing step.

      For the release, it is advantageous to work with resource packages, as this significantly shortens the loading times and these can be integrated directly into the application.

      By default (on all platforms except Android) a debug build tries to load a package from a directory first if the type has not been specified explicitly. Release builds (and debug builds on Android too) prefer files.

      To change this behavior, it is possible to append the file extension or to the name of the package to be loaded. This forces the loader to only accept a package of this type:

      // Load Resource Directory loader-> AddPackage ("main.murlres", ILoader :: LOAD_MODE_BACKGROUND); // Load Binary Package loader-> AddPackage ("main.murlpkg", ILoader :: LOAD_MODE_BACKGROUND); // Load what is preferred in the current build mode loader-> AddPackage ("main", ILoader :: LOAD_MODE_BACKGROUND);

      Alternatively, it is possible to change the order of preferred types globally using the method of the object.

      To note
      Tip! This method can be used to change the working directory in which the package loader searches for resources. By default, searches are made in the current directory on desktop platforms in debug mode and in the application resource bundle in release mode. For example, the current directory is always searched for.

      Exercises

      • How must the position of the cube be changed so that the right edge is flush with the right edge of the screen?
      • How must the size and position of the cube be changed so that the cube fills a field of 600x600 pixels in the window?
      • Change the size and aspect ratio of the window. How does the display change?
      • How does the display change if the value is also specified for the camera?

      Next we want to turn the cube. To do this, we have to get a reference to the cube object and turn a little further in each step.

      In the files and, suitable logic classes for manipulating and monitoring are defined for all graph nodes:

        A suitable class for rotating the cube is the class. This class is suitable for transforming all graph nodes that implement the interface. We declare the new member variable of the type for our logic class

        class CubeLogic: public Logic :: BaseProcessor {public: CubeLogic (Logic :: IFactory * factory); virtual ~ CubeLogic (); protected: virtual Bool OnInit (const Logic :: IState * state); virtual Bool OnDeInit (const Logic :: IState * state); virtual void OnProcessTick (const Logic :: IState * state); Murl :: Logic :: TransformNode mCubeTransform; };

        Next we need to "connect" the variable to the cube node in the graph. This is done in the method. With the method of the object we get a pointer to the root node in the graph. This node always exists - even in a completely empty graph.

        Starting from the root node, the method searches for the node with the specified ID and saves a reference to it in the object.
        A pointer to an object is returned as a return parameter. As long as there is a reference to a graph node, it must be ensured that this graph node is not removed from the graph by, for example. Otherwise it could happen that the reference refers to a graph node that no longer exists and thus to a random memory area.

        Therefore, there is a security mechanism in the Murl Engine which ensures that no graph node is removed as long as at least one reference still refers to it. If a reference is no longer required, i.e. in the destructor at the latest, it must be released again with:

        // Get Reference for mCubeTransform Logic :: IObservableNode * observableNode = mCubeTransform.GetReference (root, "myCube01"); // Remove Reference observableNode-> RemoveReference (); // alternatively mCubeTransform.RemoveReference ();

        The class provides the convenient method. This means that the release of the references can be left to the object. All references are automatically released by the object when the destructor is called.

        Bool App :: CubeLogic :: OnInit (const Logic :: IState * state) {state-> GetLoader () -> UnloadPackage ("startup"); Graph :: IRoot * root = state-> GetGraphRoot (); AddGraphNode (mCubeTransform.GetReference (root, "myCube01")); if (! AreGraphNodesValid ()) {return false; } state-> SetUserDebugMessage ("Cube Init succeeded!"); return true; }

        After we have saved a reference for the cube node in, we can use it to rotate the cube. This is done in the method. The rotation is done with the method of the interface. The value is specified for the X-axis and the Y-axis. This is simply calculated from the current tick time and is limited to the value range 0 - 2 * π.

        In addition, we use the function to display the current angle at the top right. To do this, however, the variable must first be converted into one with the function:

        void App :: CubeLogic :: OnProcessTick (const Logic :: IState * state) {Double angle = state-> GetCurrentTickTime (); angle = Math :: Fmod (angle, Math :: TWO_PI); mCubeTransform-> SetRotation (angle, angle, 0); state-> SetUserDebugMessage (Util :: DoubleToString (angle)); }

        The result is a spinning cube.