This page discusses the patience game at greater length. The annotated code is a detailed description of how the code works, whilst the discussion the more general issues of preprocessing and properties. The last section will continually show recent modifications and variations to the program to make it more efficient or more readable.
The patience game lives the Games Room world.
Standard defines for Dive files. Dive files
are preprocessed through the C
preprocessor for macro substitution
#include "dive.vh"
A few defines for the dimensions of objects
#define BOARD_SIZE (1.0) #define BOARD_DEPTH (BOARD_SIZE/10) #define BALL_SIZE (BOARD_SIZE/15) #define BALL_SPACING (BOARD_SIZE/7.5) #define BALL_FUDGE (3*BALL_SPACING)
A macro with parameters what will be used to
define the individual
balls.
#define ball(MAT, SIZE, X, Y)\ name "Ball X Y"\ translation v ((X*BALL_SPACING) - BALL_FUDGE)\ ((Y*BALL_SPACING)-BALL_FUDGE) (-0.48 * BOARD_DEPTH)\ nograsp on \ realobj on\ material MAT\ texture "http://www.cs.ucl.ac.uk/research/vr/Dive/textures/n030.rgb"\ view {SPHERE SIZE SIZE SIZE}\ inline.tcl "patienceball.tcl"
This is where the game object resides.
object {
Give it a name (this allows other objects to search for the game board).
name "Game"
This component actually has geometry of grey text.
material "GREY_M" view { CTEXT 0.075 "Reset" "ccp" }
First sub object - the button on the back of the board. It has a name, a standard dive colour. You can't grasp it though, but it is real - this means that if you grab the object you effectively grab it's parent (in this case the whole game).
subs object {
name "Button" material "RED_NEON_M" translation v 0 0 (-0.25*BOARD_DEPTH) nograsp on realobj on
Start of the tcl behaviour.
begin.tcl
This is a new tcl function that:
proc on_reset {type vid stype origin src_id x y z} { set father [dive_find_super [dive_self]] set board [dive_find_sub_byname $father "Board"] dive_send $board "reset_balls" }
This tcl function tells dive to call the function
on_reset whenever the
current object ([dive_self]) isselected. The parameters then passed to
on_reset are different depending on which type of event it is attached
to.
dive_register INTERACTION_SIGNAL DIVE_IA_SELECT [dive_self] "" on_reset
End of the tcl behaviour.
end.tcl
The geometry of the button is a cube.
view { RBOX v -(0.2*BOARD_SIZE) -(0.2*BOARD_SIZE) (0.2*BOARD_DEPTH) v (0.2*BOARD_SIZE) (0.2*BOARD_SIZE) 0 }
}
The "Board" subobject.
subs object { name "Board" translation v 0 0 (-0.65*BOARD_DEPTH) material "BROWN_M"
This object has a texture applied specified by a URL (otherwise must be on DIVEPATH).
texture "http://www.cs.ucl.ac.uk/research/vr/Dive/textures/n029.rgb"
The view is two intersecting boxes.
view { BOXVECTOR 2 RBOX v -(0.3*BOARD_SIZE) -(0.5*BOARD_SIZE) -(0.5*BOARD_DEPTH) v (0.3*BOARD_SIZE) (0.5*BOARD_SIZE) (0.5*BOARD_DEPTH) RBOX v -(0.5*BOARD_SIZE) -(0.3*BOARD_SIZE) -(0.5*BOARD_DEPTH) v (0.5*BOARD_SIZE) (0.3*BOARD_SIZE) (0.5*BOARD_DEPTH) }
Start of the tcl behaviour
begin.tcl
A procedure to set an object solid and red.
proc set_red {id} { /* puts "Red ID: $id"*/ dive_material $id "RED_NEON_M" dive_wireframe $id FALSE }
A procedure to set an object solid and blue (er actually its yellow)
proc set_blue {id} { /* puts "Blue ID: $id"*/ dive_material $id "YELLOW_M" dive_wireframe $id FALSE }
A procedure to set an object to wireframe and camoflage it brown.
proc set_off {id} { dive_wireframe $id TRUE dive_material $id "BROWN_M" }
The procedure reset_balls (which can be called when the button object is selected) does three things:
proc reset_balls {} { global first_x global first_y global first global first_fromid set first_x -1 set first_y -1 set first 1 set first_fromid 0 dive_for_all_descendants [dive_self] ENTITY set_blue set me [dive_self] set center [dive_find_sub_byname $me "Ball 3 3"] set_off $center }
The procedure test jump does the final test for the validity of a jump and then does the jump. The jump
The final test that must be performed is that the ball in the middle is really there (i.e. whether it is wireframe or not).
proc test_jump {x1 y1 x2 y2 id1 id2} { global first_x global first_y global first set me [dive_self] set u [expr ($x1 + $x2) / 2 ] set v [expr ($y1 + $y2) / 2 ] set center [dive_find_sub_byname $me "Ball $u $v"] dive_entity_info $center info set wire $info(wireframe) if {$wire==0} { set_off $center set_off $id1 set_blue $id2 set first_x -1 set first_y -1 set first 1 } }
This is the procedure called by the ball objects. It provides the core logic for the application.
proc done {fromid} {
The global varibles store the position of the highlighted ball and identity. The flag first simply stores whether or not there is a highlighted ball.
global first_x global first_y global first global first_fromid set id [dive_self]
The procedure starts off by getting the coordinates of the ball from the NAME of the ball object.
dive_entity_info $fromid info set name $info(name) scan $name "Ball %d %d" x y set wire $info(wireframe)
There are two main cases:
First there is no selected ball
if {$first == 1} {
So if that ball is there, highlight it.
if {$wire==0} { set_red $fromid set first_x $x set first_y $y set first 0 set first_fromid $fromid }
Second there is a selected ball
} else {
There are several cases here. First we have clicked on the same ball again so we remove the highlight .
if {($first_x==$x)&&($first_y==$y)} { if {$wire==0} { set_blue $fromid set first_x -1 set first_y -1 set first 1 } } else {
Second we have clicked on another ball so we switch the highlighting
if {$wire==0} { set_blue $first_fromid set_red $fromid set first_x $x set first_y $y set first_fromid $fromid } else {
Third we have clicked on a space. This space is a candidate for jumping into so we check four case corresponding to the four positions we can possible jump into. We call test jump above if the relevant positions are OK.
if {($x == $first_x +2) && ($y == $first_y)} { test_jump $x $y $first_x $first_y $first_fromid $fromid } if {($x == $first_x -2) && ($y == $first_y)} { test_jump $x $y $first_x $first_y $first_fromid $fromid } if {($x == $first_x ) && ($y == $first_y+2)} { test_jump $x $y $first_x $first_y $first_fromid $fromid } if {($x == $first_x ) && ($y == $first_y-2)} { test_jump $x $y $first_x $first_y $first_fromid $fromid } } } } }
Here we link the application variables to properties. This is necessary since we want the state the be the same on each evaluation node. See the example discussion below.
dive_property_link [dive_self] "first_x" first_x dive_property_link [dive_self] "first_y" first_y dive_property_link [dive_self] "first" first dive_property_link [dive_self] "first_fromid" first_fromid
We call reset_balls to put us in the default state with all but the center ball showing
reset_balls
End of the tcl behaviour
end.tcl
The rest of the file is devoted to the 33 balls that are placed on the board. See the discussion about the preprocessing below.
Each ball has a material a size and an x-y position. The positions are used in the name of the ball so that the board can tell which ball was pressed simply by extracting the name of the object.
subs object { ball("YELLOW_M", BALL_SIZE,0,2) } subs object { ball("YELLOW_M", BALL_SIZE,0,3) } subs object { ball("YELLOW_M", BALL_SIZE,0,4) } subs object { ball("YELLOW_M", BALL_SIZE,1,2) } subs object { ball("YELLOW_M", BALL_SIZE,1,3) } subs object { ball("YELLOW_M", BALL_SIZE,1,4) } subs object { ball("YELLOW_M", BALL_SIZE,2,0) } subs object { ball("YELLOW_M", BALL_SIZE,2,1) } subs object { ball("YELLOW_M", BALL_SIZE,2,2) } subs object { ball("YELLOW_M", BALL_SIZE,2,3) } subs object { ball("YELLOW_M", BALL_SIZE,2,4) } subs object { ball("YELLOW_M", BALL_SIZE,2,5) } subs object { ball("YELLOW_M", BALL_SIZE,2,6) } subs object { ball("YELLOW_M", BALL_SIZE,3,0) } subs object { ball("YELLOW_M", BALL_SIZE,3,1) } subs object { ball("YELLOW_M", BALL_SIZE,3,2) } subs object { ball("YELLOW_M", BALL_SIZE,3,3) } subs object { ball("YELLOW_M", BALL_SIZE,3,4) } subs object { ball("YELLOW_M", BALL_SIZE,3,5) } subs object { ball("YELLOW_M", BALL_SIZE,3,6) } subs object { ball("YELLOW_M", BALL_SIZE,4,0) } subs object { ball("YELLOW_M", BALL_SIZE,4,1) } subs object { ball("YELLOW_M", BALL_SIZE,4,2) } subs object { ball("YELLOW_M", BALL_SIZE,4,3) } subs object { ball("YELLOW_M", BALL_SIZE,4,4) } subs object { ball("YELLOW_M", BALL_SIZE,4,5) } subs object { ball("YELLOW_M", BALL_SIZE,4,6) } subs object { ball("YELLOW_M", BALL_SIZE,5,2) } subs object { ball("YELLOW_M", BALL_SIZE,5,3) } subs object { ball("YELLOW_M", BALL_SIZE,5,4) } subs object { ball("YELLOW_M", BALL_SIZE,6,2) } subs object { ball("YELLOW_M", BALL_SIZE,6,3) } subs object { ball("YELLOW_M", BALL_SIZE,6,4) } } }
The ball shows several stages of preprocessing. Firstly the whole file is passed through the C preprocessor to expand each of the:
ball("YELLOW_M", BALL_SIZE,6,3)
macros. The definition of a ball thus comes out like this:
name "Ball 6 4" translation v ((6*((1.0)/7.5)) - (3*((1.0)/7.5))) ((4*((1.0)/7.5))- (3*((1.0)/7.5))) (-0.48 * ((1.0)/10)) nograsp on realobj on material "YELLOW_M" texture "http://www.cs.ucl.ac.uk/research/vr/Dive/textures/n030.rgb" view {SPHERE ((1.0)/15) ((1.0)/15) ((1.0)/15)} inline.tcl "patienceball.tcl"
The macro subsitution allows you to do some powerful things, and it is used a lot in the standard dive objects. The next stage is to replace the
inline.tcl "patienceball.tcl"
With the actual script:
proc on_drop {type vid stype origin src_id x y z} { set father [dive_find_super [dive_self]] dive_send $father "done [dive_self]" } dive_register INTERACTION_SIGNAL DIVE_IA_SELECT [dive_self] "" on_drop
This is wrapped in a pair of begin.tcl and end.tcl statements .
Next any Dive default definitions are replaced. In our case this means that things like default material:
material "YELLOW_M"
is replaced by the actual definition which is:
material { diffuse 0.4 0.4 0.05 ambient 0.8 0.8 0.05 specular 0 0 0 spec_power 0 transparency 1 }
If you then look at the source of the ball inside vishnu you'll see that
the complete definition looks like:
object { name "Ball 6 2" prop "Class" "string" "Tcl:Dive" 2 material { diffuse 0.4 0.4 0.05 ambient 0.8 0.8 0.05 specular 0 0 0 spec_power 0 transparency 1 } nograsp on realobj on texture "http://www-dept.cs.ucl.ac.uk/research/vr/Dive/textures/n030.rgb" translation v 0.4 -0.133333 -0.048 rotation v 1 0 0 v 0 1 0 v 0 0 1 begin.tcl proc on_drop {type vid stype origin src_id x y z} { set father [dive_find_super [dive_self]] dive_send $father "done 6 2 [dive_self]" } dive_register 4 1 [dive_self] "" on_drop end.tcl view { SPHERE 0.0666667 0.0666667 0.0666667 } }
Now of course if you make any changes you will see them within vishnu, but if you save your file out then you'll lose all your nice macros and be left with a long winded file that is difficult to understand.
NOTE1: The reason that the behaviour is inlined is that the cpp macro substution doesn't pay attention to line break and puts everything on one line. This is fine for the Dive definition, but tcl scripts get mangled since their structure is derived in part from their file layour.
NOTE2: You can't use any TCL comments in your .vr files since cpp doesn't like them
NOTE3: the ball's dive_register lost the nice names of the signal (INTERACTION_SIGNAL DIVE_IA_SELECT) and replaced it with their integer definitions (4 1). You can look this sort of thing up in the file dive.vh in the main Dive data directory
First thing to try is what happens if you take out the lines:
dive_property_link [dive_self] "first_x" first_x
dive_property_link [dive_self] "first_y" first_y
dive_property_link [dive_self] "first" first
dive_property_link [dive_self] "first_fromid" first_fromid
If you run a multiuser system, you'll see that each person can highlight a different ball so you have two highlighted balls. Only is really highlighted on the current system and this leads to confusion since the collaboraters could see and do different things.
What has gone wrong is that there are two tcl interpreters running, one on each machine. The interpreters access a common database, but also have local state (first_x, first_y etc..) This state isn't shared so the interpreters can be in a different state. This might not be a problem but it usually is.
What is needed is some way of specifying some higher level information that says that only one ball can be highlighted at a time. We need this information to be shared between both machines, and to be updated in a timely fashion.
This is what properties do. A property specifies a way of wrapping arbitrary data from interpreters into dive types so that they can be propogated through the dive database.Their use is quite simple, but there is a small performance penalty to pay. You have to bear in mind that latency is a problem and it is still possible to get the interpreters to diverge.
Astute readers (always wanted to say that) will have noticed that the use of properties in this example is actually redundant. This is because all the information to determine which object is highlighted is actually in the dive database already. Specifically we coloured it red to give feedback about when it had been selected. So instead of storing (first_x, first_y, etc) as global variables in the tcl script and then exporting them as properties, we can search all the sub objects and look for one that is red.
This makes the tcl code longer, but the main drawback is that it is quite slow to check the colour of each ball each time we click on something. There is obviously a trade-off here:
You can get rid of properties if the state of the application manifests itself in the presentation of it's objects in the database.
It might be quite slow to reverse engineer the state.
Anthony Steed, A.Steed@cs.ucl.ac.uk