Tic Tac Toe/Root Objects

From Second Life Wiki
< Tic Tac Toe
Revision as of 14:26, 24 April 2016 by Toady Nakamura (talk | contribs) (<source lang="lsl2">)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

I know that distributed logic is all the rage, but for a simple project like this, centralized is good. We're going to keep the user interface bits as dumb as possible and have the central controller script decide what happens.

Before we go even further, we need to think a little how the players are going to interact with this game. How are we going to know who is playing and whose turn it is?

When designing these things, try to make things as simple and automatic as possible. Don't make the users go through a sign-in process or other complicated sit-ups. In this game I decided to use the following process:

  • a game is idle if it hasn't been played in a while or has ended. It can then be reset by clicking on any cube.
  • the first player to make a move on a reset game is X
  • the second player to make a move is O
  • play continues until game ends or times out, after which it becomes idle.

One thing that isn't immediately obvious is that we can't have the X/O cubes make the decision on whether to change state, since they don't have the full information. Only the central controller script can change the states of the X/O cubes. So we need to split off the state change from the touch event handlers and use a message handler to change the states.

So let's talk about messages. This game is going to be one linkset, so we will use link messages. They are actually quite nice as long as you keep good message discipline. In this case we will decide to use the following convention:

  llMessageLinked(<target>, <message-id>, <string-arg>, <key-arg>);

Message ids will be constants defined at the beginning of every script we use. We never overload the meaning of a message id - we have more than enough integers to not make this necessary. We avoid encoding data into the message id. The string arg and the key arg provide more than enough space for codes.

Note: Constants are expensive - unfortunately. Seems like 20 bytes per integer constant, and as the project gets larger it puts you into an unfortunate vicious circle - so magic numbers will have to do - but use them only if really needed, and make sure you put a comment next to them.

Let's look at the X/O cube's script. Note the proper way to code a touch event handler. That integer argument getting passed in //is// important, even if in 99% of the cases it will be "1". It does happen that multiple touch events get packed into one, especially in a competitive game, and ignoring a user action is just about the worst insult you can inflict. So do make the effort of writing that silly while loop.

// Locations of various bits on the texture
float e_pos_s = 0.75;
float e_pos_t = 0.75;
float x_pos_s = 0.75;
float x_pos_t = 0.25;
float o_pos_s = 0.25;
float o_pos_t = 0.25;

// Face on which the texture lives
integer display_face = 4;

// Message constants
integer MSG_RESET      = 0;
integer MSG_TOUCH      = 1;
integer MSG_SET_X      = 2;
integer MSG_SET_O      = 3;
integer MSG_IDLE       = 4;
integer MSG_IDLE_TOUCH = 5;

default
{
    state_entry()
    {
        llOffsetTexture(e_pos_s, e_pos_t, display_face);
    }

    touch_start(integer touching_agents)
    {
        while (touching_agents--)
        {
            llMessageLinked(LINK_ROOT, MSG_TOUCH, "", llDetectedKey(touching_agents));
        }   
    }
    
    link_message(integer from, integer msg_id, string str, key id)
    {
        if (msg_id == MSG_SET_X) state x;
        if (msg_id == MSG_SET_O) state o;
        if (msg_id == MSG_IDLE) state idle;
    }
}

state x
{
    state_entry()
    {
        llOffsetTexture(x_pos_s, x_pos_t, display_face);
    }

    link_message(integer from, integer msg_id, string str, key id)
    {
        if (msg_id == MSG_RESET) state default;
        if (msg_id == MSG_IDLE) state idle;
    }
}

state o
{
    state_entry()
    {
        llOffsetTexture(o_pos_s, o_pos_t, display_face);
    }

    link_message(integer from, integer msg_id, string str, key id)
    {
        if (msg_id == MSG_RESET) state default;
        if (msg_id == MSG_IDLE) state idle;
    }
}

state idle
{
    link_message(integer from, integer msg_id, string str, key id)
    {
        if (msg_id == MSG_RESET) state default;
    }

    touch_start(integer touching_agents)
    {
        llMessageLinked(LINK_ROOT, MSG_IDLE_TOUCH, "", NULL_KEY);
    }
}

Note that as long as the game is in progress, there //is// no touch event for the x and o states, thereby trivially avoiding bad touches.

As promised, state changes happen only when the central controller says so...

Another detail: the message sent on an idle touch is different that the touch message in the default state. Since at that point we really do not care who clicked, we omit the while loop.

And now let's look at the central controller script:

// Message constants
integer MSG_RESET      = 0;
integer MSG_TOUCH      = 1;
integer MSG_SET_X      = 2;
integer MSG_SET_O      = 3;
integer MSG_IDLE       = 4;
integer MSG_IDLE_TOUCH = 5;


// Game timeout
integer GAME_TIMEOUT = 20;

// Game state
key player_x;
key player_o;

default
{
    state_entry()
    {
        llSay(0, "state default");
        player_x = NULL_KEY;
        player_o = NULL_KEY;
        llMessageLinked(LINK_ALL_CHILDREN, MSG_RESET, "", NULL_KEY);
        state playing;
    }
}

state playing
{
    state_entry()
    {
        llSay(0, "state playing");
    }
    
    link_message(integer from, integer msg_id, string str, key id)
    {
        llSay(0, "from = "+(string)from+" msg_id = "+(string)msg_id);
        if (msg_id == MSG_TOUCH)
        {
            llSay(0, "touch from "+(string)from);
            if (NULL_KEY == player_x)
            {
                player_x = id;
                llSetTimerEvent(GAME_TIMEOUT);
                llMessageLinked(from, MSG_SET_X, "", NULL_KEY);
            }
            else if (NULL_KEY == player_o)
            {
                player_o = id;
                llSetTimerEvent(GAME_TIMEOUT);
                llMessageLinked(from, MSG_SET_O, "", NULL_KEY);
            }
            else if (id == player_x)
            {
                llSetTimerEvent(GAME_TIMEOUT);
                llMessageLinked(from, MSG_SET_X, "", NULL_KEY);
            }
            else if (id == player_o)
            {
                llSetTimerEvent(GAME_TIMEOUT);
                llMessageLinked(from, MSG_SET_O, "", NULL_KEY);
            }
        }
    }
    
    timer()
    {
        llSetTimerEvent(0);
        state idle;
    }
}

state idle
{
    state_entry()
    {
        llSay(0, "state idle");
        llMessageLinked(LINK_ALL_CHILDREN, MSG_IDLE, "", NULL_KEY);
    }
    
    link_message(integer from, integer msg_id, string str, key id)
    {
        if (msg_id == MSG_IDLE_TOUCH) state default;
    }
}

That one starts with a cut & paste job to import the message constants. Now wouldn't #include be nice here - but hence the rule about these being constants - if later other scripts need more, you only add those you need.

In the previous section we talked about states and when to use them, and here is an example of the other use case for states: a bootstrapping process. Note that during an actual game, we stay in a single state. We only change state when the event behaviour is changing.

Note how we pass along the id of the touchers from the X/O Cube script.

Note how we keep resetting the timeout. In particular, it is necessary to explicitly disable the timer event when leaving a state, since for some reason the timer setting persists and will come and cause trouble when re-entering that state.

No actual game logic is implemented yet, but it should be easy to see where this is going...

At this point, we can simply copy the X/O Cubes to make 9. The script's message will be identifiable by the link number. It is important to remember how those numbers get assigned when you build the object. Essentially, every new object you add to your selection prior to linking will be number one, shifting everyone else up one number. You then select your intended root prim last, then execute the link command (ctrl-l).

As your project grows, you may find that link numbers are too fragile, especially if you keep refining the shape of your object. An alternative solution would be to use llGetObjectName() or llGetObjectDesc() to store a symbolic link name and pass it along in your llMessageLinked() calls.