Flatland

From Sirikata Wiki
(Redirected from EZUI)
Jump to: navigation, search

Flatland is a user interface creation tool developed by Ben Christel. It is designed to allow an application developer to add code to an entity's script that specifies the UI's appearance and logic. The UI will then appear on a user's screen when the user interacts with one of the entity's presences.

Flatland was designed with the following goals in mind:

  • Facilitating rapid development of dialog-like UIs associated with a particular presence in the world
  • Enabling multiple users to interact with a presence simultaneously using the UI
  • Allowing application developers to easily collect and store data input by the user
  • Facilitating communication between the presence controlling the UI and the avatar interacting with it
  • Automating the synchronization of data between the UI and the controlling presence

API Documentation

UI API

here.

Server API

here.

Tutorial

Hello, World!

The Code

system.require('std/graphics/flatland.em');
fl.script(@
	write('Hello, World!');
@);

Notes

  • The line system.require('std/graphics/flatland.em') ensures that the required files are included.
  • The Emerson function fl.script() takes a string containing JavaScript to be executed when the GUI loads. The code passed to fl.script() is executed every time a user opens the GUI. In between invocations of the GUI, its contents are cleared. The result is that the GUI always starts in the same state.
  • The write() function takes a string as a parameter. This string is appended to the GUI's HTML, similar to the way document.write() works in a plain HTML document.

Testing the UI

You can test the code above by opening a scripting window on any presence in the world and running the code. Then right-click on the presence you just scripted and the UI should appear. You may have to left-click first to move the focus to the main window.

Creating Buttons And Sections

The Code

system.require('std/graphics/flatland.em');
fl.script(@
	write(sections('buttons', 'output'));
	var b1 = button('English', buttonCB);
	var b2 = button('Spanish', buttonCB);
	append('buttons', b1+'<br />'+b2);
	function buttonCB (buttonText) {
                if (buttonText == 'Spanish') {
		        setInnerHTML('output', 'Hola!');
                } else {
		        setInnerHTML('output', 'Hello!');
                }
	}
@);

Notes

  • Buttons are represented as JavaScript objects, and can be stored in vars. When you want them to appear on the screen, just pass the button object to a function like write(). Buttons can also be concatenated with strings using the + operator.
  • The button() function is used to create a button. This function takes the following parameters:
    • the text on the button
    • a callback function that is called when the user clicks the button. This callback can take the button text as a parameter, so if two buttons share the same callback, you can figure out which one was clicked.
  • The sections() function is used to divide the UI into sections. sections() takes any number of strings as parameters; these strings are used as section IDs. Note that the strings passed to sections() must be valid HTML ID attribute values (i.e. they must begin with a letter and contain only letters, numbers, and underscores). sections() returns a string that can be passed to write() or append().
  • The append() function is used to append text or HTML to a specific section of the UI. The function takes the following parameters:
    • The ID of the section
    • The text to append to the section
  • The setInnerHTML() function sets the contents of the section with the specified ID to the specified text or HTML. It has the same syntax as append.

Sending Messages to the Controlling Presence

  • The controlling presence (or simply controller) is the presence that you script with the fl.script() call.
  • You can notify the controller that a button on your UI was pressed by setting notifyController as the button's callback function.
  • The fl.onButtonPressed() function lets you specify a function to execute when your controller gets notified of a button event. Note that fl.onButtonPressed() is called in the controller's main script, not the UI script inside fl.script. The callback function must also be defined in the controller's main script. The callback can take the button's text as a parameter.

The Code

system.require('std/graphics/flatland.em');
fl.script(@
	var upButton = button('up', notifyController);
	var downButton = button('down', notifyController);
	var stopButton = button('stop', notifyController);
	write(upButton+'<br />');
	write(stopButton+'<br />');
	write(downButton+'<br />');
@);
 
fl.onButtonPressed(move);
function move (buttonText) {
	if (buttonText == "up") {
		system.self.setVelocity(<0, 1, 0>);
	}
	else if (buttonText == "down") {
		system.self.setVelocity(<0, -1, 0>);
	}
	else if (buttonText == "stop") {
		system.self.setVelocity(<0, 0, 0>);
	}
}

Notes

  • notifyController is only used when you don't need the UI to store any state about the user's actions. It can be handy for making context menus that allow the user to select from a list of options that cause the controller to take some action. Other methods of communicating with the controller will be discussed in later tutorials.

Managing User Data

  • The _() function lets you get and set user-specific data that can persist across multiple invocations of the UI.
  • For example, the following code gets a user variable called "score" and increments it.
var s = _('score') + 1;
_('score', s);
  • The expression _('score') retrieves the value of the variable named "score".
  • The line _('score', s); stores the value of s in the variable named "score".
  • User data can be automatically saved when the UI closes. To autosave user data, call onUiWillClose(save);.
  • onUiWillClose() registers a callback function that will be executed when the UI is about to close.
  • save() is a function that you can call to manually save user data.

The Code

system.require('std/graphics/flatland.em');
fl.script(@
 
if (!_('score')) _('score', 0);
write(sections("welcome", "button", "score"));
append("welcome", "<b>Welcome to ButtonClick</b>,<br/> the game where you click a button.");
append("button", button("score++", incrementScore));
displayScore();
 
function displayScore () {
    setInnerHTML("score", _('score'));
}
 
function incrementScore () {
    var s = _('score') + 1;
    _('score', s);
    displayScore();
}
 
onUiWillClose(save);
 
@);

Testing the Code

  • Open the UI, click the button a few times, and close it. Reopen it, and notice that your score is the same as when you left off.
  • Your score is unique to you; if someone else opens the UI, the game will keep track of their score separately.

Using _('viewer')

  • When a UI is first opened it already contains some data about the user who is viewing it. This information is stored in user data and can be accessed with _('viewer').
  • _('viewer') has the following fields:
    • _('viewer').pos stores the viewer's position in x, y, and z fields.
    • _('viewer').vel stores the viewer's velocity in x, y, and z fields.
    • _('viewer').ori stores the viewer's orientation in x, y, z, and w fields.
    • _('viewer').orivel stores the viewer's orientation velocity in x, y, z, and w fields.
    • _('viewer').mesh stores the viewer's mesh URL as a string.
    • _('viewer').id stores the viewer's presence id as a string.
  • Note that the data in _('viewer') does not update while the UI is open, and writing to it has no effect on the viewer's avatar.

The Code

system.require('std/graphics/flatland.em');
 
fl.script(@
    write("<b>You Are Here:</b><br/>");
    write("x: "+_('viewer').pos.x+"<br/>");
    write("y: "+_('viewer').pos.y+"<br/>");
    write("z: "+_('viewer').pos.z+"<br/>");
@);

Managing Global Data

  • Global data is common to all users of a UI. For example, you might use global data to store the high score in a game or the log for a chatroom.
  • UI code may read from global data, but is not allowed to write to it. The reason for this is that individual instantiations of a UI only have user data for one user. Global data, however, may depend on multiple users' data (for example, the high score in a game can only be computed if you know each player's score). In order to ensure that global data is consistent with user data, only the controller may modify global data.
  • When global data is changed by the controller, the UI is notified and a callback gets executed. You can register a callback function by passing it to onGlobalDataDidChange(). There is also a function onGlobalDataWillChange() that registers a callback to be executed when the global data is about to be updated. This is useful if you want to do something with the old values of the data before it gets modified.
  • In the UI, you can read from global data with the GLOBAL_() function, which takes a variable name as a parameter just like _(). There is also the shorthand version __() which does the same thing as GLOBAL_().
  • You can set global data in the controller's code using fl.setGlobalData(), which takes the name of the variable to set and the value to set it to. The UI is notified of changes automatically. The updates are processed in batches, so if you call fl.setGlobalData() multiple times, the UI will only be notified once.
  • In this tutorial we'll modify the game from the previous section to keep track of the high score.

The Code

system.require("std/graphics/flatland.em");
fl.script(@
/* Initialize the user data if it isn't already set */
if (!_('score')) _('score', 0);
if (!_('upgrade_price')) _('upgrade_price', 2);
if (!_('increment')) _('increment', 1);
 
/* Draw the UI */
write(sections("welcome", "button", "score", "high_score"));
append("welcome", "<b>Welcome to ButtonClick</b>,<br/> the game where you click a button.");
var buttonText = (_('increment') == 1) ? "score++" : "score+=" + _('increment');
append("button", button(buttonText, incrementScore, "", "main_button"));
append("button", button("buy upgrade", buyUpgrade));
append("button", "<br/>"+span().id("upgrade_price"));
 
displayScore();
displayUpgradePrice();
displayHighScore();
 
function displayScore () {
    setInnerHTML("score", _('score'));
}
 
function displayUpgradePrice () {
    setInnerHTML("upgrade_price", "upgrade costs "+_('upgrade_price')+" points");
}
 
function buyUpgrade () {
    var s = _('score');
    var i = _('increment');
    var p = _('upgrade_price');
    if (s >= p) {
        s -= p;
        i++;
        p *= 2;
        setInnerHTML("main_button", "score+="+i);
    }
    _('score', s);
    _('increment', i);
    _('upgrade_price', p);
    displayScore();
    displayUpgradePrice();
    save();
}
 
function incrementScore () {
    var s = _('score') + _('increment');
    _('score', s);
    if (s > __('high_score') && __("high_score_user") != _('viewer').id) save();
    displayScore();
}
 
function displayHighScore () {
    var s = _('score');
    var h = __('high_score') || s;
    if (s >= h) {
        setInnerHTML("high_score", "You have the highest score!");
    } else {
        setInnerHTML("high_score", __("high_score_user")+" leads with "+__('high_score')+" points");
    }
}
 
onUiWillClose(save);
onGlobalDataDidChange(displayHighScore);
 
@);
 
fl.onUserDataDidChange(calculateHighscore);
 
function calculateHighscore() {
    var hs = -1;
    var hsu = "Mr. Nobody";
    fl.iterateUserData(function(user) {
        if (user['score'] > hs) {
            hs = user['score'];
            hsu = user['viewer']['id'];
        }
    });
/* the UI is notified automatically after each batch of calls to fl.setGlobalData */
    fl.setGlobalData('high_score', hs);
    fl.setGlobalData('high_score_user', hsu);
}

A Simple Chat Client

The Code

system.require('std/graphics/flatland.em');
fl.script(@
 
write(sections('chat_log', 'chat_form'));
 
displayChatLog();
 
append('chat_form', '<input type="text" size="40" id="my_chat_input_box"></input><br />');
append('chat_form', button('submit', chatSubmitButtonCB));
 
function chatSubmitButtonCB () {
    var msg = {};
    msg.chatLine = $("#my_chat_input_box").val();
    $("#my_chat_input_box").val("");
    msg.avatar = _('viewer').id.substring(0, 6);
    sendToController(msg);
}
 
function displayChatLog () {
    clear('chat_log');
    var lines = __("chat_lines");
    if (lines) {
        for (var i = 0; i < lines.length; i++) {
            append('chat_log', lines[i]+'<br />');
        }
    }
}
 
onGlobalDataDidChange(displayChatLog);
 
@);
 
fl.setGlobalData('chat_lines', []);
 
function handleNewChat (msg) {
    var chatLines = fl.globalData("chat_lines");
    chatLines.push("<span style='color:#"+msg.avatar+"'>"+msg.avatar+": "+msg.chatLine+"</span>");
    if (chatLines.length > 10) chatLines.splice(0, 1);
    fl.setGlobalData('chat_lines', chatLines);
}
handleNewChat << [{'chatLine'::}];

Notes

  • The sendToController() function sends a message to the UI's controller. Of course, the controller needs to be set up to handle this message, which is done with handleNewChat << [{'chatLine'::}];. See the tutorial on message passing in Sirikata.
  • clear() clears the contents of the specified section of the UI.
  • onGlobalDataDidChange() registers a function to be called when the UI's controller calls fl.setGlobalData(). Note that updates to global data are processed in batches, so if there are many calls to setGlobalData, the callback will only be called once.