Table of Contents

ChangeLog

16 November 2011

  • Initial release.

Abstract

It is interesting to build in Second Life a series of classical games, such as Football (Soccer), Polo, Volleyball and generally all the ball games based on freefall. In the future, perhaps we will be able to see "real Second Life teams" play in such games and hold tournaments. It might be an extension to Second Life which would push the borders of the points of interests of residents. I suggest a concept of virtual sports which would allow residents in metaverses to extend the possibilities of the virtual world.

The physics engine is sufficiently equipped to support most of dynamics of ball games. In fact, the physics engine in Second Life, implements much more than needed to implement these games. However, the general problem is the interaction between a player (in real life), the viewer they use and what happens in the Second Life world.

This three-layer abstraction, being a matter of some abstract form of HCI, may have prevented such a thing to happen. For example, using the viewer, there is no way to "kick the ball with precision". In other words, the physics of Second Life, do allow you to hit a ball in any direction you wish, however the viewers are not well-equipped to perform that operation precisely. If you place a ball onto the ground and collide your avatar against it, the ball will indeed roll away based on the impact angle and force, however it is currently prohibitively difficult to kick a ball towards a precise direction and at a given angle by colliding your avatar against it.

Thus, the only needed modifications are interface tools and in-world fields which would make such an operation convenient to users.

Although this article implements Volleyball (out of personal preference), it would be considerably easier to implement Football (Soccer). In some ways, only the HUD would be needed and instead of two fields, just one big field would be enough.

Introduction

Every ball-game is based more or less on some variations of freefall. My artillery script and calculations are sufficient to understand the basic concepts of physical movement as well as being generic enough to be able to derive any ball game. I can enumerate a few games that relate to artillery:

However, in all these cases, the problem is that although we have a full-blown physics engine, we cannot really directly influence the shooting of the ball. If you have ever tried to play "Soccer" with a friend, you would have realized pretty soon that, even by running towards the ball, you still cannot induct the speed you wish into the ball. Nor can you control the angle at which you shoot the ball. These two pretty much eliminate the possibility to create any game since all the mentioned cases are heavily based on where and how you wish to send a ball.

We can abstract over these games and use the principles to implement any ball-game. The first part consists in implementing a system in order to be able to control the ball. The last part, left up to builders, consists in respecting the characteristics of the game in question and implementing the ruleset as well as possible. This article implements the game of Volleyball, or rather beach volley in the sense that it is governed by less strict rules, and subparts of it may be used to generate other derivations of artillery in order to create other games.

The article will be written as a technical report, describing the assembly of the Volleyball system and including how to overcome difficulties. It is in that way, different from the rest of the articles since you will have to understand how it is done, instead of being copy-pasteable. This article will also refer you to the full assembly on marketplace (distributed as a freebie) which contains the full-permission result of implementing this game.

Terminology

Game Mechanics

This section describes how the assembly works. For the other rules of Volleyball, feel free to consult the abundant information on the Internet.

Two teams, McCoys and Hatfields, both receive a HUD. This HUD will be used to send the ball from one field to the other, as well as passing the ball between the same team-members.

When a player holds down the left mouse button, on a point in the area of the HUD, velocity is built up. When the player releases the button, that velocity (along with other calculations) is transferred to the ball, provided that they are close enough to the ball.

The (in this assembly) black patches, help all the players to determine where the ball will land. This is an optional feature, left up to the builder to include or take out. Provided that the maximum velocity a player can transfer to a ball is capped at something sensible, this optional feature may be left out to make it even more challenging.

Players must anticipate how much velocity to build up and intercept the ball so that they send it back to the opposing field without the ball hitting the ground. The purpose of the game is to confuse, outwit and maintain enough concentration so that the ball will eventually hit the ground in the opposing team's field.

Abstractions on Volleyball

Volleyball court dimensions.

The field in this assembly thus respects that schematic as far as ratios go. However, a builder / creator may choose to scale the field up and make it larger. This will not affect the scripts.

General Considerations for Builders

Overview of Components

General overview of the volleyball field assembly.

  1. Volleyball fields.
  2. Volleyball fields landing prediction overlay.
  3. Targeting HUD for two different teams.
  4. Ball.
  5. Wind direction and intensity measuring device.

Description of Components

1. Volleyball fields.

The volleyball fields consists of 2 (two) subparts and the net:

2. Targeting HUD

The targetting HUD consists in two descriptive fields, placed from top to bottom and describes the opponent's field and the allies field. This HUD will be used to allow players to pass the ball to teammates, as well as shoot the ball into the opponent's field. It does that by mapping the HUD relative-coordinate system to the in-world fields' coordinate system and sending the ball the landing coordinates if the ball is within a certain user-specified range.

The HUD will also inform the filed overlays of the predicted landing position. This will allow players to run to that point and intercept the ball.

3. Ball

The ball will respond in-world from commands given by the HUDs and will also compute the required artillery trajectory to hit its destination.

4. Wind direction and Speed

I have optionally implemented a simple wind measuring device which will be displayed in-world. Keep in mind that the ball's trajectory is indeed influenced by wind and might induce an error from the trajectory which the field overlay will not compute.

Breakdown of Fields

  1. The field itself with white delimiters.
  2. Meant for hinting to the player the predicted landing spot.

The field for both teams have two overlay primitives. One of them (1) is the field itself that just displays the field borders and the other (2) will be used to indicate the predicted landing spot of the volleyball. These two primitives should overlap by setting their position one over the other.

McCoys

For the McCoys, the volleyball field (1), contains a named script [K] Volley - McCoys with the following content:

volley_mccoys.lsl
 ///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
integer comChannel;
 
default
{
    state_entry() {
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer ahash;
        for(itra=0, ahash=0; itra<llStringLength(llGetScriptName()); ++itra) {
            ahash += llListFindList(alpha, (list)llToLower(llGetSubString(llGetScriptName(), itra, itra)));
        }
        comChannel = ahash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llListen(comChannel, "", "", "@rc");
    }
    listen(integer channel, string name, key id, string message) {
        vector pos = llGetPos();
        vector scale = llGetScale();
        llRegionSay(comChannel, "@ac=" + (string)pos.x + "," + (string)pos.y + "," + (string)pos.z + "," + (string)scale.x + "," + (string)scale.y);
        llSetColor(<1,0,0>, ALL_SIDES);
        llSetTimerEvent(5);
    }
    timer() {
        llSetColor(<1,1,1>, ALL_SIDES);
        llSetTimerEvent(0);
    }
}

The field shadow overlay (2.) overlay contains the following named script [K] Volley - McCoys:

volley_mccoystarget.lsl
 ///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
//              _             ___                 
//     |\/|  _ /   _      _    |  _. ._ _   _ _|_ 
//     |  | (_ \_ (_) \/ _>    | (_| | (_| (/_ |_ 
//                    /                 _|        
//
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
default 
{
    state_entry() {
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer mhash;
        string mfield = llList2String(llParseString2List(llGetScriptName(), [" - "], []),1);
        for(itra=0, mhash=0; itra<llStringLength(mfield); ++itra) {
            mhash += llListFindList(alpha, (list)llToLower(llGetSubString(mfield, itra, itra)));
        }
        integer comChannel = mhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llListen(comChannel+1, "", "", "");
    }
    listen(integer channel, string name, key id, string message) {
        list tData = llParseString2List(message, ["="], []);
        if(llList2String(tData, 0) != "@la") return;
        list landPos=llCSV2List(llList2String(tData,1));
        vector landXY;
        landXY.x=llList2Float(landPos, 0);
        landXY.y=llList2Float(landPos, 1);
        vector pos = llGetPos();
        llOffsetTexture(landXY.x/2, landXY.y/2, ALL_SIDES);
    }
}

As you might notice, the names are identical, yet they contain different code. That is because the code builds an identifier hash on which channels are generated. In this case, both the field and the field shadow overlay are based on the same channel. For the opponents field, the Hatfields, other channels are generated based on the script's name.

Similarly, you will see that the ball uses common identifiers to allow control from both teams.

Hatfields

Symmetrically, the Hatfield's volleyball field (1), contains the named script [K] Volley - Hatfields:

volley_hatfields.lsl
 ///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
integer comChannel;
 
default
{
    state_entry() {
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer ahash;
        for(itra=0, ahash=0; itra<llStringLength(llGetScriptName()); ++itra) {
            ahash += llListFindList(alpha, (list)llToLower(llGetSubString(llGetScriptName(), itra, itra)));
        }
        comChannel = ahash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llListen(comChannel, "", "", "@rc");
    }
    listen(integer channel, string name, key id, string message) {
        vector pos = llGetPos();
        vector scale = llGetScale();
        llRegionSay(comChannel, "@ac=" + (string)pos.x + "," + (string)pos.y + "," + (string)pos.z + "," + (string)scale.x + "," + (string)scale.y);
        llSetColor(<0,0,1>, ALL_SIDES);
        llSetTimerEvent(5);
    }
    timer() {
        llSetColor(<1,1,1>, ALL_SIDES);
        llSetTimerEvent(0);
    }
}

And the shadow overlay (2), for the Hatfields is a named script [K] Volley - Hatfields:

volley_hatfieldstarget.lsl
 ///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
//               _                  ___                 
// |_|  _. _|_ _|_ o  _  |  _|  _    |  _. ._ _   _ _|_ 
// | | (_|  |_  |  | (/_ | (_| _>    | (_| | (_| (/_ |_ 
//                                            _|        
//
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
default 
{
    state_entry() {
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer mhash;
        string mfield = llList2String(llParseString2List(llGetScriptName(), [" - "], []),1);
        for(itra=0, mhash=0; itra<llStringLength(mfield); ++itra) {
            mhash += llListFindList(alpha, (list)llToLower(llGetSubString(mfield, itra, itra)));
        }
        integer comChannel = mhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llListen(comChannel+1, "", "", "");
    }
    listen(integer channel, string name, key id, string message) {
        list tData = llParseString2List(message, ["="], []);
        if(llList2String(tData, 0) != "@la") return;
        list landPos=llCSV2List(llList2String(tData,1));
        vector landXY;
        landXY.x=llList2Float(landPos, 0);
        landXY.y=llList2Float(landPos, 1);
        vector pos = llGetPos();
        llOffsetTexture(landXY.x/2, landXY.y/2, ALL_SIDES);
    }
}

Breakdown of HUDs

There are two HUDs, one for McCoys and one for Hatfields. The one depicted in the screenshot above is the McCoys HUD because the McCoys have a red identifier and that identifier is facing to the bottom of the screen. Symmetrically, the Hatfields obtain a reversed HUD with the blue box pointing downwards and indicating they are a Hatfield team member. Thus, each HUD has two linked primitives. The "upper" primitive box representing the opponent and the "lower" primitive box representing the player's own team (allies).

The HUDs are used to shoot the ball towards a certain point on the field. They do that by mapping differently aligned and rotated coordinate systems to the coordinate system of the fields. As a general rule, a player belongs to a certain team, either McCoys or Hatfields, if their field's color is the same as the color of the lower box of their HUD. In the picture above, I am wearing the McCoys HUD and standing on the McCoys field which makes me a McCoy.

Whenever the HUDs, either the McCoys HUD or the Hatfields HUD, are worn, they go through a calibration procedure which makes both fields light up in order to indicate and show the player to which team they belong to.

Using a McCoys HUD on a Hatfields field is a violation of the game and a referee should prevent that.

All HUD scripts, except for the scripts responsible for flashing the fields, contain one settable parameter:

integer MAX_VELOCITY = 50;

Which represents the settable maximum velocity that may be attained by holding down the left mouse button a certain field component. This is important, because the velocity is also dependent on the angle. Intuitively, one challenge of the game is to induce enough velocity for the ball to cross the net. However, a player may induce more than that to confuse or make it harder for the opposing team to intercept the ball once it descends on the opposing field. This parameter limits the maximum velocity because too much of it may shoot the ball so high that it might be fairly impossible for the opposing team to intercept it.

The literal value here 50 represents $50\frac{m}{s}$ and assumes that all players may accelerate the ball to a maximum velocity of $50\frac{m}{s}$. This is a bit high and should be lowered and tweaked to your field dimension and players.

Also, since every single script in the HUD contains this parameter, you may exaggerate and (in case of teams), assign different max velocities to players (ie: John Doe has more muscle than his teammate Kelly). In case you do that, balancing as well as setting up regulations for players in a competition is entirely up to you.

Flashing HUD

In order to indicate to the player that they are currently building up velocity to shoot the ball into a certain field, both the blue and red boxes on the HUD may contain the following unnamed script [K] Velocity Buildup:

flashinghud.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
//
//                                            _  
//       \  / _  |  _   _ o _|_      |_| | | | \ 
//        \/ (/_ | (_) (_ |  |_ \/   | | |_| |_/ 
//                              /                
//
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
integer lock=0;
 
default
{
    link_message(integer sender, integer num, string str, key id)
    {
        if(lock) return;
        lock=1;
        llSetColor(<1,1,1>, ALL_SIDES);
        llSetTimerEvent(.01);
    }
    timer() {
        llSetColor(<0,0,1>, ALL_SIDES);
        llSetTimerEvent(0);
        lock=0;
    }
}

Hatfields Component of McCoys HUD

In the McCoys HUD (the one in the picture above), the (1.) Hatfields HUD Component, contains the named script [K] Volley - Hatfields which is responsible for communicating the predicted landing target to the ball and the field overlays, as well as the optional (yet, recommended) unnamed script [K] Velocity Buildup which flashes the HUD indicating buildup.

volley_mccoyshud.lsl
 ///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
//                 _                      _  
//        |\/|  _ /   _      _   |_| | | | \ 
//        |  | (_ \_ (_) \/ _>   | | |_| |_/ 
//                                                                        
///////////////////////////////////////////////////////////////////////////
//                            CONFIGURATION                              //
///////////////////////////////////////////////////////////////////////////
 
// Set this to the maximum possible velocity a player 
// can induce into the volleyball.
 
integer MAX_VELOCITY = 50;
 
///////////////////////////////////////////////////////////////////////////
//                          END CONFIGURATION                            //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
float Ox;
float Oy;
float Oz;
float Sx;
float Sy;
 
integer velocity=2;
integer comHandle;
integer comChannel;
 
init() {
    llSetColor(<0,0,0>, ALL_SIDES);
    list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    integer itra;
    integer ahash;
    for(itra=0, ahash=0; itra<llStringLength(llGetScriptName()); ++itra) {
        ahash += llListFindList(alpha, (list)llToLower(llGetSubString(llGetScriptName(), itra, itra)));
    }
    comChannel = ahash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
    comHandle = llListen(comChannel, "", "", "");
    llSetTimerEvent(2+llGetRegionTimeDilation());
}
 
default
{
    on_rez(integer start_param) {
        init();
    }
 
    attach(key id) {
        init();
    }
 
    state_entry() {
        init();
    }
 
    timer() {
        llRegionSay(comChannel, "@rc");
    }
 
    listen(integer channel, string name, key id, string message) {
        list calibData = llParseString2List(message, ["="], []);
        if(llList2String(calibData,0)!="@ac") return;
        llSetTimerEvent(0);
        llListenRemove(comHandle);
        calibData = llCSV2List(llList2String(calibData,1));
        Ox = llList2Float(calibData, 0);
        Oy = llList2Float(calibData, 1);
        Oz = llList2Float(calibData, 2);
        Sx = llList2Float(calibData, 3);
        Sy = llList2Float(calibData, 4);
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer mhash;
        string mfield = llList2String(llParseString2List(llGetScriptName(), [" - "], []),1);
        for(itra=0, mhash=0; itra<llStringLength(mfield); ++itra) {
            mhash += llListFindList(alpha, (list)llToLower(llGetSubString(mfield, itra, itra)));
        }
        comChannel = mhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llSetColor(<0,0,1>, ALL_SIDES);
    }
 
    touch(integer num_detected)
    {
        if(velocity<MAX_VELOCITY) ++velocity;
        llMessageLinked(LINK_THIS, 1, "", "");       
    }
 
    touch_end(integer num_detected) {
        vector tp = llDetectedTouchST(0);
        float dx = (Ox+(tp.x-.5)*200/100*Sx/-2);
        float dy = (Oy+(tp.y-.5)*200/100*Sy/-2);
        llRegionSay(comChannel+1, "@la=" + (string)((tp.y-.5)*2)+ "," + (string)((tp.x-.5)*-2));
        llWhisper(comChannel, (string)dx + "," + (string)dy + "," + (string)Oz + "," + (string)velocity);
        velocity=0;
    }
 
}

McCoys Component of McCoys HUD

Similarly, the lower box (4. in the picture above) on the McCoys HUD, contains the following named script [K] Volley - McCoys:

volley_mccoyshud.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////                                   
//                 _                      _  
//        |\/|  _ /   _      _   |_| | | | \ 
//        |  | (_ \_ (_) \/ _>   | | |_| |_/ 
//                       /                   
///////////////////////////////////////////////////////////////////////////
//                            CONFIGURATION                              //
///////////////////////////////////////////////////////////////////////////
 
// Set this to the maximum possible velocity a player 
// can induce into the volleyball.
 
integer MAX_VELOCITY = 50;
 
///////////////////////////////////////////////////////////////////////////
//                          END CONFIGURATION                            //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
float Ox;
float Oy;
float Oz;
float Sx;
float Sy;
 
integer velocity=2;
integer comHandle;
integer comChannel;
 
init() {
    llSetColor(<0,0,0>, ALL_SIDES);
    list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    integer itra;
    integer ahash;
    for(itra=0, ahash=0; itra<llStringLength(llGetScriptName()); ++itra) {
        ahash += llListFindList(alpha, (list)llToLower(llGetSubString(llGetScriptName(), itra, itra)));
    }
    comChannel = ahash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
    comHandle = llListen(comChannel, "", "", "");
    llSetTimerEvent(2+llGetRegionTimeDilation());
}
 
default
{
    on_rez(integer start_param) {
        init();
    }
 
    attach(key id) {
        init();
    }
 
    state_entry() {
        init();
    }
 
    timer() {
        llRegionSay(comChannel, "@rc");
    }
 
    listen(integer channel, string name, key id, string message) {
        list calibData = llParseString2List(message, ["="], []);
        if(llList2String(calibData,0)!="@ac") return;
        llSetTimerEvent(0);
        llListenRemove(comHandle);
        calibData = llCSV2List(llList2String(calibData,1));
        Ox = llList2Float(calibData, 0);
        Oy = llList2Float(calibData, 1);
        Oz = llList2Float(calibData, 2);
        Sx = llList2Float(calibData, 3);
        Sy = llList2Float(calibData, 4);
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer mhash;
        string mfield = llList2String(llParseString2List(llGetScriptName(), [" - "], []),1);
        for(itra=0, mhash=0; itra<llStringLength(mfield); ++itra) {
            mhash += llListFindList(alpha, (list)llToLower(llGetSubString(mfield, itra, itra)));
        }
        comChannel = mhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llSetColor(<1,0,0>, ALL_SIDES);
    }
 
    touch(integer num_detected)
    {
        if(velocity<MAX_VELOCITY) ++velocity;
        llMessageLinked(LINK_THIS, 1, "", "");       
    }
 
    touch_end(integer num_detected) {
        vector tp = llDetectedTouchST(0);
        float dx = (Ox+(tp.x-.5)*200/100*Sx/-2);
        float dy = (Oy+(tp.y-.5)*200/100*Sy/-2);
        llRegionSay(comChannel+1, "@la=" + (string)((tp.y-.5)*2)+ "," + (string)((tp.x-.5)*-2));
        llWhisper(comChannel, (string)dx + "," + (string)dy + "," + (string)Oz + "," + (string)velocity);
        velocity=0;
    }
 
}

Symmetrically, we have the scripts belonging to the Hatfields HUD. The Hatfields HUD will also, optionally, contain the generic unnamed script [K] Velocity Buildup which may be placed in both boxes on the HUD.

McCoys Component of Hatfields HUD

The McCoys component (red box on the HUD), contains the named script [K] Volley - McCoys:

volley_hatfieldshud.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////                                   
//               _                           _  
//     |_|  _. _|_ _|_ o  _  |  _|  _   |_| | | | \ 
//     | | (_|  |_  |  | (/_ | (_| _>   | | |_| |_/ 
//                                              
///////////////////////////////////////////////////////////////////////////
//                            CONFIGURATION                              //
///////////////////////////////////////////////////////////////////////////
 
// Set this to the maximum possible velocity a player 
// can induce into the volleyball.
 
integer MAX_VELOCITY = 50;
 
///////////////////////////////////////////////////////////////////////////
//                          END CONFIGURATION                            //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
float Ox;
float Oy;
float Oz;
float Sx;
float Sy;
 
integer velocity=2;
integer comHandle;
integer comChannel;
 
init() {
    llSetColor(<0,0,0>, ALL_SIDES);
    list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    integer itra;
    integer ahash;
    for(itra=0, ahash=0; itra<llStringLength(llGetScriptName()); ++itra) {
        ahash += llListFindList(alpha, (list)llToLower(llGetSubString(llGetScriptName(), itra, itra)));
    }
    comChannel = ahash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
    comHandle = llListen(comChannel, "", "", "");
    llSetTimerEvent(2+llGetRegionTimeDilation());
}
 
default
{
    on_rez(integer start_param) {
        init();
    }
 
    attach(key id) {
        init();
    }
 
    state_entry() {
        init();
    }
 
    timer() {
        llRegionSay(comChannel, "@rc");
    }
 
    listen(integer channel, string name, key id, string message) {
        list calibData = llParseString2List(message, ["="], []);
        if(llList2String(calibData,0)!="@ac") return;
        llSetTimerEvent(0);
        llListenRemove(comHandle);
        calibData = llCSV2List(llList2String(calibData,1));
        Ox = llList2Float(calibData, 0);
        Oy = llList2Float(calibData, 1);
        Oz = llList2Float(calibData, 2);
        Sx = llList2Float(calibData, 3);
        Sy = llList2Float(calibData, 4);
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer mhash;
        string mfield = llList2String(llParseString2List(llGetScriptName(), [" - "], []),1);
        for(itra=0, mhash=0; itra<llStringLength(mfield); ++itra) {
            mhash += llListFindList(alpha, (list)llToLower(llGetSubString(mfield, itra, itra)));
        }
        comChannel = mhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llSetColor(<1,0,0>, ALL_SIDES);
    }
 
    touch(integer num_detected)
    {
        if(velocity<MAX_VELOCITY) ++velocity;
        llMessageLinked(LINK_THIS, 1, "", "");       
    }
 
    touch_end(integer num_detected) {
        vector tp = llDetectedTouchST(0);
        float dx = (Ox+(tp.x-.5)*200/100*Sx/2);
        float dy = (Oy+(tp.y-.5)*200/100*Sy/2);
        llRegionSay(comChannel+1, "@la=" + (string)((tp.y-.5)*-2)+ "," + (string)((tp.x-.5)*2));
        llWhisper(comChannel, (string)dx + "," + (string)dy + "," + (string)Oz + "," + (string)velocity);
        velocity=0;
    }
 
}

Hatfields Component of Hatfields HUD

The lower blue box on the Hatfields HUD, contains the named script [K] Volley - Hatfields:

volley_hatfieldshud.lsl
 ///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
//               _                           _  
//     |_|  _. _|_ _|_ o  _  |  _|  _   |_| | | | \ 
//     | | (_|  |_  |  | (/_ | (_| _>   | | |_| |_/ 
//                                              
///////////////////////////////////////////////////////////////////////////
//                            CONFIGURATION                              //
///////////////////////////////////////////////////////////////////////////
 
// Set this to the maximum possible velocity a player 
// can induce into the volleyball.
 
integer MAX_VELOCITY = 50;
 
///////////////////////////////////////////////////////////////////////////
//                          END CONFIGURATION                            //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
float Ox;
float Oy;
float Oz;
float Sx;
float Sy;
 
integer velocity=2;
integer comHandle;
integer comChannel;
 
init() {
    llSetColor(<0,0,0>, ALL_SIDES);
    list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    integer itra;
    integer ahash;
    for(itra=0, ahash=0; itra<llStringLength(llGetScriptName()); ++itra) {
        ahash += llListFindList(alpha, (list)llToLower(llGetSubString(llGetScriptName(), itra, itra)));
    }
    comChannel = ahash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
    comHandle = llListen(comChannel, "", "", "");
    llSetTimerEvent(2+llGetRegionTimeDilation());
}
 
default
{
    on_rez(integer start_param) {
        init();
    }
 
    attach(key id) {
        init();
    }
 
    state_entry() {
        init();
    }
 
    timer() {
        llRegionSay(comChannel, "@rc");
    }
 
    listen(integer channel, string name, key id, string message) {
        list calibData = llParseString2List(message, ["="], []);
        if(llList2String(calibData,0)!="@ac") return;
        llSetTimerEvent(0);
        llListenRemove(comHandle);
        calibData = llCSV2List(llList2String(calibData,1));
        Ox = llList2Float(calibData, 0);
        Oy = llList2Float(calibData, 1);
        Oz = llList2Float(calibData, 2);
        Sx = llList2Float(calibData, 3);
        Sy = llList2Float(calibData, 4);
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer mhash;
        string mfield = llList2String(llParseString2List(llGetScriptName(), [" - "], []),1);
        for(itra=0, mhash=0; itra<llStringLength(mfield); ++itra) {
            mhash += llListFindList(alpha, (list)llToLower(llGetSubString(mfield, itra, itra)));
        }
        comChannel = mhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llSetColor(<0,0,1>, ALL_SIDES);
    }
 
    touch(integer num_detected)
    {
        if(velocity<MAX_VELOCITY) ++velocity;
        llMessageLinked(LINK_THIS, 1, "", "");       
    }
 
    touch_end(integer num_detected) {
        vector tp = llDetectedTouchST(0);
        float dx = (Ox+(tp.x-.5)*200/100*Sx/2);
        float dy = (Oy+(tp.y-.5)*200/100*Sy/2);
        llRegionSay(comChannel+1, "@la=" + (string)((tp.y-.5)*-2)+ "," + (string)((tp.x-.5)*2));
        llWhisper(comChannel, (string)dx + "," + (string)dy + "," + (string)Oz + "," + (string)velocity);
        velocity=0;
    }
 
}

Breakdown of the Ball

The ball is the mediator between the two teams and responsible for the trajectory calculations based on the coordinates that the HUDs send. The configuration section of the ball contains the parameter:

float HIT_DISTANCE = 2;

which is a settable parameter describing how far away an avatar must be in order to be able to hit the ball using the HUDs.

The volley ball, in the event that the volley ball field is in a public area, should be set to Allow anybody to move., as indicated in the picture above since it will allow both teams to drag it around and reposition it. However, in case of a tournament, this option should be turned off and preferably the referee should rez the ball.

The ball contains the named script McCoys vs Hatfields:

volley_ball.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
//                     _          
//                    |_)  _. | | 
//                    |_) (_| | | 
//             
//
///////////////////////////////////////////////////////////////////////////
//                            CONFIGURATION                              //
///////////////////////////////////////////////////////////////////////////
 
// This represents the distance from the ball that an 
// avatar may hit the ball. Keep in mind, this is 
// measured roughly from the avatar's groin. Be generous.
 
// This is also related to response time, a ball that 
// is hurling towards you may be hard to hit and you 
// would also have to take into account the region 
// dilation time which is implicit in this value and 
// not calculated on the fly.
float HIT_DISTANCE = 2;
 
///////////////////////////////////////////////////////////////////////////
//                          END CONFIGURATION                            //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
string bidM;
string bidH;
integer comChannelM;
integer comChannelH;
 
vector origin;
vector target;
float V0;
float dΔ;
float valSin;
list cDest;
 
default {
 
    state_entry() {
        bidM = llList2String(llParseString2List(llGetScriptName(), [" vs "], []), 0);
        bidH = llList2String(llParseString2List(llGetScriptName(), [" vs "], []), 1);
        list alpha = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
        integer itra;
        integer mhash;
        for(itra=0, mhash=0; itra<llStringLength(bidM); ++itra) {
            mhash += llListFindList(alpha, (list)llToLower(llGetSubString(bidM, itra, itra)));
        }
        integer hhash;
        for(itra=0, hhash=0; itra<llStringLength(bidH); ++itra) {
            hhash += llListFindList(alpha, (list)llToLower(llGetSubString(bidH, itra, itra)));
        }
        comChannelM = mhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        comChannelH = hhash + ((integer)("0x"+llGetSubString((string)llGetCreator(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llListen(comChannelM, "", "", "");
        llListen(comChannelH, "", "", "");
    }
 
    listen(integer channel, string name, key id, string message) {
        if(llVecDist(llList2Vector(llGetObjectDetails(id, [OBJECT_POS]), 0), origin=llGetPos()) > HIT_DISTANCE) return;
        cDest = llCSV2List(message);
        target.x = llList2Float(cDest, 0);
        target.y = llList2Float(cDest, 1);
        target.z = llList2Float(cDest, 2);
        V0=llList2Float(cDest,3);
        dΔ=llVecDist(<target.x,target.y,0>,<origin.x,origin.y,0>);
        valSin = 9.81*/llPow(V0, 2);
        if(valSin < -1 || valSin > 1) valSin = 9.81/llPow(V0, 2);
        if(valSin < -1 || valSin > 1) return;
        llSetVelocity(llVecNorm(<1,0,0>*llRotBetween(<1,0,0>,llVecNorm(<target.x-origin.x,target.y-origin.y, dΔ*llTan((90-RAD_TO_DEG*llAsin(valSin)/2) * DEG_TO_RAD) + llFabs(target.z-origin.z)>)))*V0, FALSE);
    }
}

Breakdown of Miscellaneous Components

The wind indicators show the players, at all times the direction of the wind (component 1), as well as the windspeed by rotating a light-emitting cylinder (2). The other helper components (3, 4 and 5) allow you to deploy the assembly so it may be used by the public.

Wind direction

The wind direction component (1) primitive contains the following unnamed script [K] Wind Direction:

volley_winddirection.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
default
{
    state_entry() {
        llSetTimerEvent(.5);
    }
    timer() {
        vector wind = llWind(ZERO_VECTOR);
        llLookAt(llGetPos() * llRotBetween(<1,0,0>, wind), .5, .2);
    }
}

Wind speed

The wind speed component (2) contains the following unnamed script [K] Wind Speed:

volley_windspeed.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
default
{
    state_entry() {
        llSetTimerEvent(.5);
    }
    timer() {
        llTargetOmega(<0,0,1>, llVecMag(llWind(ZERO_VECTOR)), .5);
    }
}

Volleyball Dispenser

The volleyball dispenser (3), contains the following unnamed script [K] Volley Ball Dispenser:

volley_balldispenser.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
default
{
    state_entry()
    {
        llSetText("Touch to dispense a volleyball.", <1, 1, 1>, 1);
    }
 
    touch_start(integer total_number)
    {
        if(llGetInventoryName(INVENTORY_OBJECT,0) == "") return;
 
        llRezObject(llGetInventoryName(INVENTORY_OBJECT,0), llGetPos() + <0, 0, 1>, <0,0,2>, ZERO_ROTATION, 0);
    }
}

Hatfields HUD Dispenser

The Hatfields HUD dispensers (4) contain the same unnamed script [K] Hatfields HUD Dispenser:

volley_hatfieldshuddispenser.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
default
{
    state_entry()
    {
        llSetText("Hatfields, touch for HUD.", <1, 1, 1>, 1);
    }
 
    touch_start(integer total_number)
    {
        if(llGetInventoryName(INVENTORY_OBJECT,0) == "") return;
 
        llGiveInventory(llDetectedKey(0), llGetInventoryName(INVENTORY_OBJECT,0));
    }
}

McCoys HUD Dispenser

The McCoys HUD dispenser (5), contains the following unnamed script [K] McCoys HUD Dispenser:

volley_mccoyshuddispenser.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
default
{
    state_entry()
    {
        llSetText("McCoys, touch for HUD.", <1, 1, 1>, 1);
    }
 
    touch_start(integer total_number)
    {
        if(llGetInventoryName(INVENTORY_OBJECT,0) == "") return;
 
        llGiveInventory(llDetectedKey(0), llGetInventoryName(INVENTORY_OBJECT,0));
    }
}

HUD-to-Field Calibration

In order to leave the scripts as they are, you will need to calibrate the field to the HUD axis. The whole assembly should have a world rotation of 0. If facing the McCoys field from the Hatfields field, an easy way to check, is that the right of the McCoys component of the Hatfields HUD will be in-line and parallel with the X axis of the field. Rotation can be taken into account but, I simply haven't bothered with it yet.

For contrast, the following picture (taken approximatively from the same perspective and angle) shows a bad calibration:

Tournament Considerations

For this specific assembly, the following considerations should be respected in case of a tournament.

All other rules, derived from Volleyball rules apply.

Public Considerations

In the event that this assembly will be provided to the public, the creator / builder of the assembly should be one and the same for all primitives contained in this assembly. The ball placed in the Volleyball dispenser should be set to Allow anybody to move. so that both teams may reposition the ball themselves. This will, of course, allow the teams to cheat. Besides, I am sure that in such cases the whole other team will make very vigilant referees to prevent the other team from cheating. Might have to turn off land damage to prevent bloody noses - or leave it on, it's only a paper moon.

Further work

Variations on Volleyball

One possible variation of this game would be to include the HUD controls implicitly in mouselook mode and abstract away from the calculated trajectories. That is, when clicking in mouselook, a force would be applied to the ball provided it is in front of the crosshair. I don't like it that way (personal choice, I don't consider it very reliable) so that is why I went with the one explained in this article.

Obtaining the Assembly

Feel free to mirror these files, provided the GNU/GPLv3 licensing is respected.

A Bit of Formalism

The ball is similar to the artillery cannon. However, the ball does not rez an object but rather calculates the trajectory and applies the velocity to itself. Since one cannot always hit a target (for example, when one doesn't have the necessary velocity), the difference between the artillery calculations and the script in the ball is that first, the angle required to hit the spot chosen by the player is calculated. If that is not possible, then the script calculates if the velocity is enough to lift the ball off the ground. If that is also not possible, then the ball is left as it is. Enumerating those events:

  1. The ball calculates the trajectory to the chosen point on the field using the "high" angle. If it is possible, the ball shoots towards that point. If it is not possible,
  2. the ball calculates the angle created by the velocity and shoots most probably into the net. If that is not possible,
  3. the ball stays where it is.

The HUD coordinates for both fields, are given by relative offsets ranging in the domain from [0,0] to [1,1]. The HUD, via synchronization, also knows the dimensions and position of the fields. The HUD sends the offset where the player clicked to the respective field, in order to update the target where the ball will land. Based on the dimensions of the field, the offset at which the player clicked, so that the HUD calculates the landing coordinates.

When a player clicks a position on field and holds down the mouse button, the script in the HUD component starts to linearly build up velocity. When the player releases the button, if the ball is within the HIT_DISTANCE of the player, then all the data above, including the built-up velocity is transferred to the ball which proceeds with pretty much the same calculations made in the artillery article.

The scripts use up to four, llListen filtered (non-colliding) channels and 2 (two) colliding channels. During gameplay, two non-colliding channels and two colliding channels are used, while the other two sleep.

Thanks