Start Development

Published: November 04, 2020
Edited: November 06, 2020

We start by looking at App.js file (the one which is referenced in our index.js file). This is where we start importing the Lightning App framework via our SDK and Utils.

import { Lightning, Utils } from "@lightningjs/sdk";

After the import we create a new default export for our App Component.

export default class App extends Lightning.Component { 

}

Inside our class declaration we define a new member getFonts that we're going to use. Get fonts returns an array of object with properties for all the different fonts our app uses. For this app We've downloaded a pixel font but feel free to use any font(s) you like.

static getFonts() {
    return [
        {family: 'pixel', url: Utils.asset('fonts/pixel.ttf'), descriptor: {}}
    ];
}

After including the font we start by defining the root template of our app on which we will be attaching the components that are needed in our app. For now we specify the rect property which will use Lightning.texture.RectangleTexture to draw a black rectangle of 1920px by 1080px

Colors are ARGB values

static _template(){ 
    return {
        rect: true, color: 0xff000000, w: 1920, h: 1080
    }
}

Now we add an empty implementation of the statemachine which we will be starting to fill soon.

static _states(){
    return [
    
    ]
}

Lets take a step back for a brief moment and think about the different Views / Components we want to create for a game like Tic Tac Toe We probably need a:

  1. A Splash screen to display a logo, or when you're creating a different type of app acts as a placeholder up until all external request are fulfilled and assets are preloaded (if that is a requirement)
  2. A Main screen on which the user of the app lands after the splash screen hides. In the main screen we also render a menu which has some interaction with the remote control
  3. A Game screen. This screen will hold all the components which are needed to display our Tic Tac Toe game.
  4. A Menu Component with Item Components.

Also we need something like a Utils library which holds the Ai related logic because we're creating a Game which can be played against a computer controlled player.


Splash.js

Inside the src folder we create a new file Splash.js. After the file is created we open the file and import the Lightning framework from the SDK.

import { Lightning } from "@lightningjs/sdk";

Next, export your Component class so the App can import it.

export default class Splash extends Lightning.Component {

}

Inside the class definition we create the template (For now we stick to a simple text label that will be fading in and out for a couple of seconds)

static _template(){
    return {
        Logo:{
            x: 960, y: 540, mount:0.5,
            text:{text:'LOADING..', fontFace:'pixel'}
        }
    }
}

Lets briefly go over every line inside the template definition to get a bit of understanding what is going on.

return {
    Logo:{}
}

We add a new empty Component to our render-tree with the reference (name) Logo. A component's reference name must always start with an uppercase character. We use the name to get a reference for the component so we can manipulate it's properties in the future:

// set x position
this.tag("Logo").x = 200;

// change alpha
this.tag("Logo").alpha = 0.5;

// store a reference 
const logo = this.tag("Logo");

Next we see the components properties, we position the component 960px on the x-axis and 540 on the y-axis. By settings the mount property we the component to exactly align in the center, no matter the future dimensions of the property.

x: 960, y: 540, mount:0.5,

By setting the text property we force the component to be of type Lightning.texture.TextTexture, this means we can start adding text properties (see our documentation for all the possible text properties)

text:{text:'LOADING..', fontFace:'pixel'}

Now that we've successfully set up our Splash template we start by adding our first lifecycle event

_init() {

}

The init hook will be called when a component is attached for the first time. Inside the _init hook we will start defining our animation (Go to the animation part of our documentation)

_init(){
    // create animation and store a reference, so we can start / stop / pause 
    this._pulse = this.tag("Logo").animation({
        duration: 4, repeat: 0, actions:[
            {p:'alpha', v:{0:0, 1:0.5, 1:0}}
        ]
    });
    
    // add a finish eventlistener, so we can send a signal
    // to the parent when the animation is completed
    this._pulse.on("finish", ()=>{
        this.signal("loaded");
    })
    
    // start the animation
    this._pulse.start();
}

Next we add a active hook to our component, this will be called when a component is activated, visible and on screen. Inside the active hook we start our animation.

_active(){
    this._pulse.start();
}

Now that our Splash Component is ready we open the App.js file and start adding our component to the root template. We import our new component:

import Splash from "./Splash.js";

And add the component to the template. To add an instance of defined Component we use the type attribute in our template definition.

static _template() {
    return {
        rect: true, color: 0xff000000, w: 1920, h: 1080,
        Splash: {
            type: Splash, signals: {loaded: true}, alpha: 0
        }
    };
}

One new thing we see in our splash implementation is the use of the signals property.

A Signal tells the parent component that some event happened on this component.


Main.js

Next stop, is creating the Main component which will be shown at the moment the Splash component sends the loaded signal. The Main's responsibility will be showing a Menu Component in his template and accepting remote control presses so a user can navigate through the menu items. We create a new file called Main.js inside our src and add the following code:

import { Lightning } from "@lightningjs/sdk";

export default class Main extends Lightning.Component {
    static _template(){
        return {
        
        }
    }
}
Menu.js

We add a new folder inside our src folder called menu. In a real world app you may want to structure your re-useable components a bit differently. Inside the menu folder we create a new file called Menu.js and populate it with the following content:

export default class Menu extends Lightning.Component{
    static _template(){
        return {
            // we define a empty holder for our items of 
            // position it 40px relative to the component position 
            // so we have some space for our focus indicator
            Items:{
                x:40
            },
            // Create a text component that indicates 
            which item has focus
            FocusIndicator:{y:5,
                text:{text:'>', fontFace:'pixel'}
            }
        }
    }
}

Next step is adding an init, active and inactive hook in which we create and start our animation. An index property is also created which holds the index of the focused menu item.

_init(){
    // create a blinking animation 
    this._blink = this.tag("FocusIndicator").animation({
        duration:0.5, repeat:-1, actions:[
            {p:'x', v:{0:0, 0.5:-40,1:0}}
        ]
    });

    // current focused menu index
    this._index = 0;
}

_active(){
    this._blink.start();
}

_inactive(){
    this._blink.stop();
}

Let's take a small sidestep by going back to Main.js, and define the items we want to show in our Menu. We alter the template to the following, we import our menu component and add an items property to the implementation.

There is a little trick you can use inside the instance of a Component when you add it to template. By adding non-lightning properties (just like items in this example) the property will be directly available in Component definition (this.items). If you define a setter (set items(v){} ) the setter will be automatically called upon initialization.

import { Lightning } from "@lightningjs/sdk";
import Menu from "./menu/Menu.js";

export default class Main extends Lightning.Component {
    static _template(){
        return {
            Menu:{
                x: 600, y:400,
                type: Menu, items:[
                    {label:'START NEW GAME',action:'start'},
                    {label:'CONTINUE',action:'continue'},
                    {label:'ABOUT',action:'about'},
                    {label:'EXIT', action:'exit'}
                ]
            }
        }
    }
}

Now we go back to Menu.js and implement the items creation. Lightning support multiple ways of creating and adding components to the template. In this example we add the children accessor and feed it with an array of objects which will be automatically created by Lightning.

As noted before the items setter will be automatically called, so we can use the map function to return a new array of objects. We also specify the type (which at this moment is not existing).

set items(v){
    this.tag("Items").children = v.map((el, idx)=>{
        return {type: Item, action: el.action, label: el.label, y: idx*90}
    })
}

To actually add items we need to create the new Component Item. So we start by creating a new file in our menu folder called Item.js and add the following code:

import { Lightning } from "@lightningjs/sdk";

export default class Item extends Lightning.Component{

    static _template(){
        return {
            text:{text:'', fontFace:'pixel', fontSize:50}
        }
    }
    
    // will be automatically called
    set label(v){
        this.text.text = v;
    }
    
    // will be automatically called
    set action(v){
        this._action = v;
    }
    
    // will be automatically called
    get action(){
        return this._action;
    }
}

Now we go back to Menu.js and import the Item Component.

import Item from "./Item.js";

Now that we have a Menu component which can be filled with Items it's time to start adding logic to our component. We add an accessor to get the children inside the Items wrapper.

get items(){
    return this.tag("Items").children;
}

Next we add an accessor to quickly grab the active (focused) item.

get activeItem(){
    return this.items[this._index];
}

Now we declare the _setIndex function, this will accept an index argument changes the position of the focus indicator and it stores the new index.

_setIndex(idx){
    // since it's a one time transition we use smooth
    this.tag("FocusIndicator").setSmooth("y", idx*90 + 5);
    
    // store new index
    this._index = idx;
}

At this point we're done with our Menu logic and it's time to start letting our App component know when the Splash has send a loaded signal.


App.js

First we add a new state to our empty state machine called Splash. And force our app to go into that state upon setup via _setState().

The $enter() and $exit() will be automatically called upon when a component enters that state or exits that state so you can do some proper clean up if needed. In this specific case we want to make sure that our Splash component shows / hides.

_setup(){
    this._setState("Splash");
}

static _states() {
    return [
        class Splash extends this {
            $enter() {
                this.tag("Splash").setSmooth("alpha", 1);
            }
            $exit() {
                this.tag("Splash").setSmooth("alpha", 0);
            }
            // because we have defined 'loaded'
            loaded() {
                this._setState("Main");
            }
        }
    ]

Take notice of the loaded() function, because the function is nested in the Splash state this function will only be called when Splash fires the loaded signal while the app is in the Splash state. If it's not in the Splash state it will not be called unless there is a loaded function in a different state or root state. This pattern will prevent a lot of If/else statements throughout your code.

Now add a new state to our App's statemachine implementation called Main (to save some space We've hidden the Splash state implementation, but it will still be there)

_setup(){
    this._setState("Splash");
}

static _states() {
    return [
        class Splash extends this {...},
        class Main extends this {
            $enter() {
                this.tag("Main").patch({
                    smooth:{alpha:1, y:0}
                });
            }    
            $exit() {
                this.tag("Main").patch({
                    smooth:{alpha:0, y:100}
                });
            }    
            // change focus path to main
            // component which handles the remotecontrol
            _getFocused() {
                return this.tag("Main");
            }
        }
    ]

As defined before we add the $enter() and $exit() hooks to hide / show the Main component. Also we see the _getFocused popping up for the first time.

The focus path is determined by calling the _getFocused() method of the app object. By default, or if undefined is returned, the focus path stops here and the app is the active component (and the focus path only contains the app itself). When _getFocused() returns a child component however, that one is also added to the focus path, and its _getFocused() method is also invoked. This process may repeat recursively until the active component is found.

To put it another way: the components may delegate focus to descendants.

You can read more in the documentation about focus and remote control key handling

When our app is in the Main state we delegate the focus to our Main component, which in essence means: Telling Lightning which component is the active component - and should handle key events

Now that we have delegated the focus to the Main component we can open Menu.js again and start implementing the remote control handling:

Let's implement our first remote control handler. If this component has focus and the user presses the up button, _handleUp() will be called. Inside the function we will call the _setIndex which we still need to declare.

_handleUp(){
    this._setIndex(Math.max(0, --this._index));
}

And we implement the opposite logic if a user presses down on the remote;

_handleDown(){
    this._setIndex(Math.min(++this._index, this.items.length - 1));
}

Next stop, building the Actual game!

PHP Code Snippets Powered By : XYZScripts.com