Previous

Drawing conclusions (and pictures)

Next

You can make a lot of games with just buttons and labels. For example, number and word oriented games look just fine with buttons and labels. We can even do a decent job of making a game look neat by putting images on the buttons.

A lot of nifty games need pictures that the program builds while it's running.

We made a Tchuka Ruma game in the last lesson with buttons and images. The image for 3 beans is always the same image. It would be cooler to make the game draw the images itself, and make them a little bit different each time.

We can easily make the Tchuka Ruma game look like this:

This game is built out of 5 canvas widgets, instead of 5 buttons. The canvas widget is a Tcl widget that we can draw stuff on.

We create a canvas widget just like we create all the other Tcl/Tk widgets. We call the canvas command to tell Tcl/Tk that we're going to make a canvas, then we give this canvas a unique name that starts with a period and a lowercase letter, and finally we add some option/value pairs to make the canvas look just like we want it to.

Here's the syntax for the canvas command.

Syntax: canvas name options

name The name for this canvas, starting with a "." and a lowercase letter.
options Key/value pairs such as: -height 200 -width 300

There are several options we can use with a canvas. The first two we'll need for this game are the height and width of the canvas.
-height The height of the canvas
-width The height of the canvas

We normally describe the height and width in terms of pixels.

The word pixel stands for PIcture ELement. I probably should have been a pictel, but pixel sounds cooler.

All computer screens and televisions show you images that are made up of many small dots. The dots can be red, green or blue, and they can be dark or light. Each of these dots is a pixel.

Normal computer screens are 1024 pixels wide by 768 pixels tall, but that can vary. Some are 1280 pixels wide by 1024 pixels tall, and some are even 1600 pixels wide by 1200 pixels tall. If you've got a wide-screen TV or computer monitor, it might be 1680 pixels by 1050 pixels tall.

There are ways for a computer program to find out how big the display screen is that a player is using, and then make itself just fit the screen.

For now, we'll just assume that a player has at least a 1024 pixel by 768 pixel display and make games that will fit on that comfortably.

If we make the canvases for the small bins 100 pixels wide, and make the large bin twice that wide, we end up with 600 pixels wide and about 80 pixels tall. That fits nicely on most monitors.

Take a look at the image below. This shows a canvas 100 pixels wide and 80 pixels tall.

We define a location on that canvas with two numbers, an X coordinate (how many pixels from the left border) and a Y coordinate (how many pixels down from the top. Tcl/Tk puts the 0, 0 is the upper left hand corner, instead of the lower left hand corner the way we commonly draw graphs. Having 0,0 in the upper left and counting Y as we go down the screen is common in computer graphics.

The blue rectangle on this canvas starts at position 10, 10 and extends to position 90, 70. These two locations are the opposite corners that define the rectangle. We could also describe this rectangle by saying that corners are 10, 70 and 90, 10.

The red oval is exactly covered by the blue rectangle. We call the blue rectangle the bounding rectangle or bounding box for the oval, because this rectangle just exactly covers the oval. If the box were any smaller, part of the oval would be on the outside, and if it were any larger, you'd be able to see part of the box around the outside edge of the oval.

The idea of a bounding box is common in graphic programming. Any time we need to describe an area on the canvas we use a bounding box. It can cover an oval, a circle, or even a bunch of different objects.

Lets look at drawing the rectangle, oval and lines.

The Tcl/Tk widgets are what computer folks call an object. That means it's really a command and data wrapped up into one thing.

When we create a new canvas with the canvas command, Tcl/Tk makes a new command with the name of the canvas we created. Like the sound commands, that new command has options that are commands we can use to control it.

When we create the canvas named .c, Tcl/Tk creates a command named .c also.

The main command for the canvas widget we created is create. This is how we draw on the canvas. We create lines, rectangles, ovals, text and so forth.

The create commands all follow the same pattern. From left to right we have:

  1. the name of the canvas object.
  2. the word create.
  3. the type of thing we're creating (rectangle, line, text, oval or arc).
  4. the location and size of the thing we're drawing.
  5. option/value pairs to set color, line thickness and stuff like that.

The location and size of the thing we're creating will be 1 or more X/Y pairs to define a location on the canvas. For things like an image or a word, we can use 1 X/Y pair, because the create command defines where the thing goes, but doesn't control how big it is.

If the create command can control how big the thing we're creating is, we need 2 X/Y pairs. Each pair will be the X, Y coordinates of a corner of a rectangle that would just cover the object we're creating. This is the bounding box we just looked at.

You can define a rectangle by giving the X/Y coordinates of any two opposite corners.

Here's some code that:

  1. creates a canvas named .c.
  2. draws a blue rectangle .
  3. draws a red oval inside the rectangle.
  4. draws a set of vertical and horizontal lines.

Like all the other Tcl/Tk widgets, you need to use the grid command to tell Tcl/Tk where to put the canvas.


   canvas .c -width 100 -height 80
   grid .c -row 1 -column 1

   .c create rectangle 10 10 90 70 -fill blue
   .c create oval 10 10 90 70 -outline red

   for {set i 0} {$i < 100} {incr i 10} {
	  .c create line $i 0 $i 80
	}
   for {set i 0} {$i < 80} {incr i 10} {
	  .c create line 0 $i 100 $i 
	}

Here's some of the things you can create on a canvas and some of the useful options for them:

type coordinates options
rectangle 2 pair -fill color, -outline color, -width number
oval 2 pair -fill color, -outline color, -width number
text 1 pair -text "string of words", -fill color
line 2 or more pairs -fill color, -width number

Type the code above (or copy and paste it) into Komodo and see what it does. Try changing the colors and X/Y locations and see what you get.

After you've change the colors, try changing the order in which the oval and rectangle are created. It will look like the oval wasn't created - but it really is there. Whatever you create in a canvas is drawn on top of what's already there. The rectangle completely covers (and hides) the oval.

Everything that we create on a canvas has a few things in common, whether it's a rectangle, oval or line. Everything gets a unique number when it is created, and everything can have a tag associated with it.

The unique number is how Tcl/Tk keeps track of the things on the canvas. Our program can use this number if we want it to. People (even computer programmers) prefer to think in words, rather than numbers.

A tag is a word that our program uses to reference something we've drawn on the screen. We can even give the same tag to several things we create on the canvas, so we can do something to all of them at once.

What sorts of thing would we want to do to more than one canvas element? Well, we might want to delete them.

In order to delete something from a canvas, we need to be able to tell the canvas what to delete. We do this by giving the canvas an identifier. The identifier could be the unique number the canvas assigned when we created the element, or it could be the tag that we told the canvas to associate with the things we've created.

One identifier that always exists is the word all. This means (can you guess?) to delete all the stuff in a canvas.

The command to delete things from a canvas is this:

canvasName delete identifier

Here's code like the previous example, but this time everything we create on the canvas has a tag. The buttons will delete things from the canvas when you click them.


   canvas .c -width 100 -height 80
   grid .c -row 1 -column 1

   .c create rectangle 10 10 90 70 -fill blue -tag bluebox
   .c create oval 10 10 90 70 -outline red -tag redoval

   for {set i 0} {$i < 100} {incr i 10} {
	  .c create line $i 0 $i 80 -tag yline
	}
   for {set i 0} {$i < 80} {incr i 10} {
	  .c create line 0 $i 100 $i -tag xline
	}
   button .b_blue -text Box -command ".c delete bluebox"
   grid .b_blue -row 2 -column 1
   button .b_red -text Oval -command ".c delete redoval"
   grid .b_red -row 3 -column 1
   button .b_yline -text "Vertical Lines" -command ".c delete yline"
   grid .b_yline -row 4 -column 1
   button .b_xline -text "Horizontal Lines" -command ".c delete xline"
   grid .b_xline -row 5 -column 1

Now for a tricky bit.

Whenever you move a mouse, hit a key or blink, the computer sees an event. OK, not when you blink. But lots of things cause events for the computer.

Events include:

You're already familiar with some of these events, you're just so used to them that you don't even think about them. When you move a mouse over a button, the button changes color to let you know it's ready to be clicked. When you push the mouse-button you see the button on the screen click down, and when you release the mouse-button it clicks back up, etc.

All of these things are done by giving the computer a list of events to watch for, and instructions for what to do when that event occurs. We do this for every single thing that gets drawn on the computer screen from a big window to a single button.

We call this binding an event to a window. Or just binding for short.

Luckily, most of these bindings are assigned automaticly without us needing to do anything about it. Buttons get the bindings for mouse-enter, mouse-leave, button-press and button-release without us needing to do anything.

Tcl/Tk gives you the power to put new bindings on the widgets we create. We can even put bindings on the things we draw on a canvas (but we'll get to that later).

For the Tchuka Ruma game, we want to be able to click on a bin and have it move the beans around, as if the canvas were a button.

We can do that by setting a binding on the canvas for the ButtonRelease event.

The Tcl/Tk command to assign bindings is bind. The bind command needs to know three things - the window to put a binding on, the event to watch for, and the commands to run when the event happens.

Here's the syntax for the bind command.


Syntax: bind  window event commands
window The name of the window to which these commands will be bound
event The event to use as a trigger for these commands
commands The commands to evaluate when the event occurs.

That's all the pieces we need to make a Tchuka Ruma game that looks like it's being played with bins and beans (if you don't look too close)

We can make a Tchuka Ruma game using bind and the canvas's create oval and delete command. We'll use a big oval for the bins, and smaller ovals for the beans.

Most of the code we wrote in the previous version of Tchuka Ruma will be the same, we'll just change it to use canvases instead of buttons.

Obviously, the big changes will be to the buildBoard and showBeans procedures. Instead of buttons, we'll be using canvases. Our code will need to draw the dark yellow ovals to be the bins and the dark brown ovals to be beans.

Here's the new buildBoard procedure. The only part we kept from the previous buildBoard is the loop, the grid commands and the procedure header comment. We replaced the button commands with canvas commands.

We added a bind command so that clicking on the canvases will call the moveBeans procedure. This line of code is what gets done for you when you use the -command option with the button command.

Finally, we draw an oval to use as the bin.


################################################################
# proc buildBoard --
#    Creates the GUI
# Arguments
#   NONE
#
# Results
#   Modifies the screen.  Creates widgets
#
proc buildBoard {} {
  global board

  # We have 4 canvases that act like buttons, and
  # one larger canvas to be the goal.
  
  # Build the 4 canvases that hold the beans when the game starts

  for {set i 0} {$i < 4} {incr i} {
    
    # Create and grid the canvas

    canvas .c_$i -width 100 -height 80
    grid .c_$i -row 1 -column $i

    # Bind the Left ButtonRelease Event to make this
    # canvas act like a button

    bind .c_$i <ButtonRelease-1> "moveBeans $i"
    
    # Create an oval to be the bin on this canvas
    .c_$i create oval 10 10 90 70 -fill gold
  }

  # Create and grid the goal canvas and put a larger
  # oval in it for the destination bin.
  
  canvas .c_goal -width 180 -height 80
  .c_goal create oval 10 10 170 70 -fill gold
  grid  .c_goal -row 1 -column 4 -sticky news
}

The moveBeans procedure won't need to be changed at all. That's the nice thing about this style of programming. The biggest part of the program is the code that knows how to play the game, and we can make a big visual change without touching that.

The showBeans procedure gets totally rewritten. As with the buildBoard procedure, we change almost everything except the header comment.

Here's the new procedure. Notice that we use the -tag option on the beans so that they all have the same tag, and we can delete the beans without deleting the big gold oval.


################################################################
# proc showBeans {}--
#    Make the GUI reflect the contents of the board array
# Arguments
#   None   
#   
# Results
#   Updates the GUI
#   
proc showBeans {} {
  global board
  
  # Update all the canvases to reflact the number of beans
  # in their bin.
  
  for {set i 0} {$i < 4} {incr i} {
    .c_$i delete beans
    for {set j 0} {$j < $board($i)} {incr j} {
       # Calculate a position for the upper left corner of this
       #  oval (bean).
       set x [expr 20 + ($j*14)]
       set y 36
       .c_$i create oval $x $y [expr $x + 12] [expr $y + 8] -fill brown \
         -tag beans
    }
  }  
    .c_goal delete beans
    
    for {set j 0} {$j < $board(goal)} {incr j} {
       set x [expr  20 + ($j*20)]
       set y 36
       .c_goal create oval $x $y [expr $x + 12] [expr $y + 8] -fill brown \
         -tag beans
    }
     
}    

Look at the calculation for where to put the beans in the previous code. The beans will always be put in a row. The first bean is always in the same column, the next bean in another and so forth. That's OK, but it's a little bit boring. The display looks like this.

We can use some random numbers to spice this up. The code below varies the X and Y position of the beans just a little bit so they end up scattered across the bin, but are still mostly in a row, and mostly in columns. We need to be careful with random numbers like this to make sure we don't put beans exactly on top of each other, or outside the bin.


################################################################
# proc showBeans {}--
#    Make the GUI reflect the contents of the board array
# Arguments
#   None
#
# Results
#   Updates the GUI
#
proc showBeans {} {
  global board

  # Update all the canvases to reflact the number of beans
  # in their bin.

  for {set i 0} {$i < 4} {incr i} {
    .c_$i delete beans
    for {set j 0} {$j < $board($i)} {incr j} {
       # Calculate a position for the upper left corner of this
       #  oval (bean).
       set x1 [expr int (rand()*8) + 20 + ($j*14)]
       set y1 [expr int(rand()*20 + 22)]
       set x2 [expr $x1 + 12]
       set y2 [expr $y1 + 8]
       .c_$i create oval $x1 $y1 $x2 $y2 -fill brown -tag beans
    }
  }
    .c_goal delete beans

    for {set j 0} {$j < $board(goal)} {incr j} {
       set x1 [expr int(rand()*10 + 20 + ($j*20))]
       set y1 [expr int(rand()*20 + 22)]
       set x2 [expr $x1 + 12]
       set y2 [expr $y1 + 8]
       .c_goal create oval $x1 $y1 $x2 $y2 -fill brown -tag beans
    }
}

Here's the complete code for this game.


################################################################
# proc moveBeans {binNumber}--
#    moves beans from one bin to successive bins.
#    If there are N beans in a bin, One bean will be placed
#    into each of N bins after the start bin.
#    Each time a bean goes into bin 4, it goes out of play.
#    If there are no beans in play (8 beans are in bin 4), 
#    the player wins.
#    If the last bean goes into an empty bin, the player loses.
# Arguments
#   binNumber	The number of the bin to take beans from.
# 
# Results
#   The global variable bins is modified.  
#   The buttons -text option is configured to reflect the number of
#   beans in the bins.

proc moveBeans {binNumber} {
  global board
  
  # Save the number of beans we'll be moving.
  set beanCount $board($binNumber)

  # Empty this bin
  set board($binNumber) 0

  # Put the beans into the bins after this bin.

  for {set i $beanCount} {$i > 0} {incr i -1} {
    incr binNumber 
    
    # If we've reached the end of the board, 
    # go back to the beginning.
    if {$binNumber > 4} {
      set binNumber 0
    }

    # If this is the last bin, update the "goal" bin
    #  Check to see if the player has won.

    if {$binNumber == 4} {
      incr board(goal)
      if {$board(goal) == 8} {
        tk_messageBox -type ok -message "You just Won!"
	exit
      }
    } else {
      # Last bean can't go into an empty bin
      if {($i == 1) && ($board($binNumber) == 0)} {
        tk_messageBox -type ok -message "You just lost"
	exit
      }
      # Put this bean in a bin.
      incr board($binNumber)
    }
  }
  showBeans
}

################################################################
# proc showBeans {}--
#    Make the GUI reflect the contents of the board array
# Arguments
#   None
# 
# Results
#   Updates the GUI
# 
proc showBeans {} {
  global board

  # Update all the canvases to reflact the number of beans
  # in their bin.

  for {set i 0} {$i < 4} {incr i} {
    .c_$i delete beans
    for {set j 0} {$j < $board($i)} {incr j} {
       # Calculate a position for the upper left corner of this
       #  oval (bean).  
       set x1 [expr int (rand()*8) + 20 + ($j*14)]
       set y1 [expr int(rand()*20 + 22)]
       set x2 [expr $x1 + 12]
       set y2 [expr $y1 + 8]
       .c_$i create oval $x1 $y1 $x2 $y2 -fill brown -tag beans
    }
  }
    .c_goal delete beans

    for {set j 0} {$j < $board(goal)} {incr j} {
       set x1 [expr int(rand()*10 + 20 + ($j*20))]
       set y1 [expr int(rand()*20 + 22)]
       set x2 [expr $x1 + 12]
       set y2 [expr $y1 + 8]
       .c_goal create oval $x1 $y1 $x2 $y2 -fill brown -tag beans
    }
}

################################################################
# proc initializeGame {}--
#    initializes the game variables.
# Arguments
#   NONE
# 
# Results
#   Modifies the global variable "board"
# 
proc initializeGame {} {
  global board
  # Put beans into the first 4 bins.

  for {set i 0} {$i < 4} {incr i} {
    set board($i) 2
  }

  # Make sure there are no beans in the last bin
  set board(goal) 0
}

################################################################
# proc buildBoard --
#    Creates the GUI
# Arguments
#   NONE
# 
# Results
#   Modifies the screen.  Creates widgets
# 
proc buildBoard {} {
  global board
  
  # We have 4 canvases that act like buttons, and
  # one larger canvas to be the goal.

  # Build the 4 canvases that hold the beans when the game starts

  for {set i 0} {$i < 4} {incr i} {

    # Create and grid the canvas

    canvas .c_$i -width 100 -height 80
    grid .c_$i -row 1 -column $i 

    # Bind the Left ButtonRelease Event to make this
    # canvas act like a button

    bind .c_$i <ButtonRelease-1> "moveBeans $i"
    
    # Create an oval to be the bin on this canvas
    .c_$i create oval 10 10 90 70 -fill gold
  }
  
  # Create and grid the goal canvas and put a larger
  # oval in it for the destination bin.

  canvas .c_goal -width 180 -height 80
  .c_goal create oval 10 10 170 70 -fill gold
  grid  .c_goal -row 1 -column 4 -sticky news 
}

initializeGame
buildBoard
showBeans


Type or Copy/Paste the code into Komodo and try running it.

Try modifying the colors of the big bins and beans.

You can make differently shaped beans by changing the command that sets x2 and y2. Try adding some randomness to the bean size and see how the game looks.

This is trickier. Try reworking the showBeans procedure so that it takes an argument. That argument will be the index for the bin to show. Instead of showing all the beans in all the bins, only show the beans in the bin described by the argument. Here's how the new procedure will start.


################################################################
# proc showBeans {id}--
#    Make the GUI reflect the contents of the board array
# Arguments  
#   id  an identifier - the board array index and a canvas suffix.
#
# Results
#   Updates the GUI
# 
proc showBeans {id} {
  global board

  # Update a canvas to reflact the number of beans
  # in the bin.

  .c_$id delete beans
  # Add the rest of the code here
}

You'll be able to get rid of the loop and the lines of code that fill the goal bin.

Now modify the moveBeans procedure to call showBeans with the $binNumber when it changes the number of beans in a bin.

Finally change the calls to showBeans in buildBoard to pass the number of the bin that's being made.

The advantage of this way of updating the board is that we can add a sound effect for dropping the beans into the bins.

You can get a plink sound effect here

Add the commands to load the plink sound to initializeGame, and then add plink play to showBeans. You'll need to also add update idle to showBeans to show the beans as they are being dropped into the bin.


Important points are:

There is a lot of stuff we can do with a canvas. We'll start looking at another game with pictures in the next lesson.



Previous

Next


Copyright 2007 Clif Flynt