Gamesman - The "Shall We Play a Game?" Project
     
 
   
 
   
 
   
 
   
>>
     GUI Procedures
   Advanced Graphics
   Sound and Animation

 

 
   
 
Extra Credit > Advanced Graphics

Advanced Graphics

Getting Started With gdraw

Gdraw is a graphics library you can use to design the simple and gui graphics for the gamesman final project. Although gdraw was designed to work specifically with Gamesman, you don't actually have to be running Gamesman to use it. This means that you can design your graphics in real time, without having to resolve your game in Gamesman to see if your functions are working. However, there are a few steps you should take to get gdraw working for you. These steps are important if, for example, you wish to add gifs to your scene.

First, load the gamesman.scm file into your interpreter. In the labs just type (load "gamesman.scm") as usual, and at your home STk you can just use the load command in the menus above.

Then load gdraw.scm the same way.

Following this, call the function (set-gamesman-path). This should just return okay. What set-gamesman-path does is find out what machine type you are running on and sets the appropriate Gamesman directory structure. This comes in handy when you want to add gif's to your scene, so make sure that you call this function beforehand. Simply put all your images in ~/ucwise/gamesman/images/ and you'll be set to go.

Finally, call (init-graphics). This opens up the root window and displays the default canvas on it. Remember not to close this root window without saving your work. It will close STk with it.

Using IDs

Every thing you draw on the canvas is a separate object. When you type (draw-line 100 100 200 200) you're not just drawing a line on the screen. You're making a Tk object, and each object has a unique ID. ID's are assigned sequentially, in the order that objects are created. So if you made a call to draw-line right after initializing the graphics you'd probably get a return value of 1. One is the unique ID for that line. You can now use that ID to change properties of the line. You can say (set-width! 1 20) and the width of the line becomes 20 pixels. Now, ID's keep increasing even if you call (clear-graphics!) and draw the same line. This line would probably get the ID 2 and so forth.

So this is all pretty sweet, huh? Well, no... it isn't. Why not? As it turns out, if you wanted to use ID's in your procedures, you'd probably have a HARD time, simply because you can't exactly store these ID's anywhere. And since ID's keep increasing, you'll never really know exactly what ID your objects will be given at any time during Gamesman. So what do you do now? Well read on...

Using Tags

Tags are great. Use tags often!!! A tag is just a word that you can assign to an object. The tag can be unique, or you can assign the same tag to many objects. It's up to you. And you can use tags exactly like ID's. Let's create our line again but this time with a tag:

    (draw-line 100 100 200 200 'tag 'myline)
    (set-width! 'myline 20)

Excellent! Now I've got a line with a width of 20, just like before, and I didn't have to know anything about the ID.

Now, let's say I called (clear-graphics!) and I created two lines that crossed each other and I gave each a different tag:

    (draw-line 100 100 200 200 'tag 'line1)
    (draw-line 200 100 100 200 'tag 'line2)

I can manipulate each line individually by using its unique tag. Now I'm going to add the same tag to each of them. Since they make an x I'm going to call the tag 'x.

    (addtag 'line1 'x)
    (addtag 'line2 'x)

And now I can change both of their widths at the same time:

    (set-width! 'x 20)

However, I can still manipulate them individually by using the other tags.

Tags don't get overwritten, so you can have as many tags on an item as you like. If you ever want to see how many tags a particular item has, call the get-tags function (remember to give it a unique tag or ID otherwise you might get something screwy):

    (get-tags 'line1) => (line1 x)
    (get-tags 'line2) => (line2 x)

Using Bindings (GUI only)

Bindings are the core of the Gamesman GUI. They allow for user interaction directly with the canvas art and allow for the creation of moves based on user input. Here's a bit of how it works.

First, Gamesman calls your gui-init-canvas function where you set up all of your geometry (lines, rectangles, ovals, curves etc.). Then Gamesman pauses, allowing the user to make use of any kind of interaction you've set up. Here's where the human player clicks, drags, and clicks some more, until you've gathered enough information to create the move.

Once the move is made, you must make a call to gui-return-from-human-move. This function only takes one argument, a move. Once you make this call, Gamesman will check to see if the move is valid. If the move is not allowed, then Gamesman merely does nothing, and continues to wait until you call gui-remove-from-human-move again. Hopefully this is more of a convenience for you than a hassle. It saves you the time of having to error check your moves. This way, only the valid moves you make will be accepted.

So how do you get your geometry to accept user input? Bindings, that's how. A binding is a procedure that is invoked when the user clicks on that particular piece of geometry. To make a binding, simply call the set-binding! procedure with an ID or a tag and a procedure to invoke. The procedure must be a lambda of no arguments. Remember how Scheme evaluates all its arguments before evaluating the procedure? Well, the same thing applies here. Consider the example below:

    (draw-rectangle 100 100 200 200 'tag 'rect)
    (set-binding! 'rect (set-fill-color! 'rect "blue")

I expect that when I click on the rectangle that it will change it's color to blue. However, right after making this binding the color changed on me. Why? Well, the arguments to set-binding! are 'rect and (set-fill-color! 'rect "blue"). Since Scheme evaluates its arguments first, the expression (set-fill-color! 'rect "blue") gets evaluated immediately. Consequently, the color changes to blue even though I never clicked on the rectangle. Here's what the function call should have looked like:

    (set-binding! 'rect (lambda () (set-fill-color! 'rect "blue"))

And that's how you can make simple bindings. Another thing to note is that you can have more than one binding per object. This gets pretty complicated, especially when combined with non-unique tags. If you want to remove all bindings from an object, just provide the following call (set-binding! 'rect "") . We have found a few glitches when trying to make a binding using a shared tag when objects have multiple tags. If you have any problems post to the newsgroup.

The Click the Arrow to Move Method

Okay, so in Tic-Tac-Toe bindings are easy. I just bound the entire board, which was a 600 x 600 picture, with a procedure that figured out which slot was clicked on based on the mouse coordinates and called gui-return-from-human-move with that move. But your games aren't quite that simple. They require you to move a piece from one location to the other. So how should you represent those kind of moves? One way to do this is with arrows. A call to gui-show-all-moves might put a few arrows up on the screen going from certain pieces into the appropriate slots they can move to. Then you can just bind those arrows with a procedure that calls gui-return-from-human-move with the right move. You can bind those arrows the same way you bind any other object. Just tag it appropriately, then call set-binding! and inside the lambda make sure there's a call to gui-return-from-human-move. That's all there is to it.

One trick I used in Tic-Tac-Toe was to make all the pieces during gui-init-canvas and then lowered them under the board. You might do the same with your arrows. You could create all of them first, bind them all with procedures, and then lower them below the board until gui-show-all-moves is called. You could then color them differently when either gui-show-value-moves or gui-show-safe-moves is called.

In case some of you are curious how to make arrows, you can put arrowheads on your lines by adding the option 'arrow followed by one of 'first ,'last , or 'both. Here are a few examples.

    (draw-line 200 200 300 200 'arrow 'first)
    (draw-line 150 100 150 200 'arrow 'last)
    (draw-line 10 10 50 50 'arrow 'both)

(Extra for Experts: There are lots of other options you can use for all sorts of Tk objects, but we can't tell you everything. What fun would that be? That's why, if you're reading this, we called you an expert up front. Besides, we want you to WOW us at the presentation. But here's a hint: get-properties and set-property! are a good place to start experimenting)

Here's an example of some code that uses arrows to move a piece around.

;; Begin example
(clear-graphics!)
 
(set-canvas-size 100 300) ;; height, width
 
(draw-line  90 50 140 50 'fill 'grey 'tag 'arrow1-2 'arrow 'last  'width 20 'arrowshape '(25 25 12))
(draw-line  60 50 110 50 'fill 'grey 'tag 'arrow2-1 'arrow 'first 'width 20 'arrowshape '(25 25 12))
(draw-line 190 50 240 50 'fill 'grey 'tag 'arrow2-3 'arrow 'last  'width 20 'arrowshape '(25 25 12))
(draw-line 160 50 210 50 'fill 'grey 'tag 'arrow3-2 'arrow 'first 'width 20 'arrowshape '(25 25 12))
 
(addtag 'arrow1-2 'arrows)
(addtag 'arrow2-1 'arrows)
(addtag 'arrow2-3 'arrows)
(addtag 'arrow3-2 'arrows)
 
(draw-rectangle   0 0 100 100 'fill "bisque" 'tag 'slot1)
(draw-rectangle 100 0 200 100 'fill "bisque" 'tag 'slot2)
(draw-rectangle 200 0 300 100 'fill "bisque" 'tag 'slot3)
 
(draw-oval 10 10 90 90 'fill 'red 'tag 'mypiece)
 
(define (set-mypiece-coords num)
   (set-coords! 'mypiece 
                (list (+ 10 (* num 100)) 
                      10
                      (+ 90 (* num 100))
                      90)))
 
(define (handle-move move)
   (set-mypiece-coords (- move 1))
   (unselect-piece))
    
(define (return-from-move move)
   (handle-move move))
 
(define (update-arrows)
  (lower! 'arrows)
  (let ((coords (get-coords 'mypiece)))
    (cond ((= (first coords) 10)
         (raise! 'arrow1-2))
        ((= (first coords) 110)
         (raise! 'arrow2-1)
         (raise! 'arrow2-3))
        (else (raise! 'arrow3-2)))))
 
(set-binding! 'arrow1-2 (lambda () (return-from-move 2)))
(set-binding! 'arrow2-1 (lambda () (return-from-move 1)))
(set-binding! 'arrow2-3 (lambda () (return-from-move 3)))
(set-binding! 'arrow3-2 (lambda () (return-from-move 2)))
 
 (define (select-piece)
   (set-binding! 'mypiece unselect-piece)
     (update-arrows))
     
(define (unselect-piece)
  (set-binding! 'mypiece select-piece)
  (update-arrows))
 
(set-binding! 'mypiece select-piece)
(update-arrows)
;; End example

In this example, all bindings occur on the arrows, and they are set up only once. The function update-arrows is the core of the program. First, it lowers all the arrows. Then, it raises the appropriate pieces based upon the position of the piece. In this way we only raise arrows for moves that actually exist.

The Click a Piece, Then a Slot to Move Method

Some of you may be curious how to set up more complicated bindings, ones where you can click on a piece and then give it a place to go. To do such a thing you have to set up a binding which in turn sets up other bindings. It's not as complicated as it may sound. Here's an example.

;; Begin Example
(clear-graphics!)
 
(set-canvas-size 100 300)
 
(draw-rectangle 0 0 100 100 'fill "bisque" 'tag 'slot1)
(draw-rectangle 100 0 200 100 'fill "bisque" 'tag 'slot2)
(draw-rectangle 200 0 300 100 'fill "bisque" 'tag 'slot3)
 
(draw-oval 10 10 90 90 'fill 'red 'tag 'mypiece)
 
(define (set-mypiece-coords num)
   (set-coords! 'mypiece 
                (list (+ 10 (* num 100)) 
                      10
                      (+ 90 (* num 100))
                      90)))
         
(define (remove-slot-bindings)
   (for-each (lambda (num)
             (set-binding! (word 'slot num) ""))
             '(1 2 3)))
 
(define (handle-move move)
   (set-mypiece-coords (- move 1))
   (unselect-piece))
    
(define (return-from-move move)
   (handle-move move))
 
(define (select-piece)
   (set-width! 'mypiece 10)
   (for-each (lambda (slot-num)
                (set-binding! (word 'slot slot-num) 
                              (lambda ()
                                 (return-from-move slot-num))))
             '(1 2 3))
   (set-binding! 'mypiece unselect-piece))
   
(define (unselect-piece)
   (set-width! 'mypiece 1)
   (remove-slot-bindings)
   (set-binding! 'mypiece select-piece))
 
(set-binding! 'mypiece select-piece)
;; End Example

Don't get intimidated. What's going on here is actually pretty simple. First, the auxiliary functions. The function set-mypiece-coords just takes in a number corresponding to the slot the piece is moving to (minus 1 actually). The math in there is just setting up the coordinates for the circle based on that new slot. The function remove-rect-bindings removes all the bindings from all the rectangles. (I tried doing this giving them all a shared tag and then removing that binding, but it didn't work as expected. So I recommend using what I've used above in your projects.)

The return-from-move functions simulates Gamesman a bit, acting as the intermediate function between your user input and how you handle the move. The piece is physically moved in handle-move. This is where the piece's coordinates get changed and also where its binding is reset. It's in this function (gui-handle-move) that your game might include animation.

Now, the bindings are where this gets tricky. The piece is initially bound to the procedure select-piece. Now remember, this binding procedure won't actually get invoked until you click on the piece. When this occurs, the procedure select-piece does three things. First, it changes the width of the piece so that it appears selected. Then it creates a binding on each of the three slots. It binds them with a procedure that just calls return-from-move given with a number specific to that slot. So slot1 will call (return-from-move 1) when invoked, slot2 (return-from-move 2) when invoked, and slot3 with (return-from-move 3) when invoked. Then the procedure resets the binding for the piece to the procedure unselect-piece; unselect-piece is much simpler. It just resets the width to 1 so that the piece appears deselected, then it removes the slot bindings, and then rebinds the piece to select-piece.

This might seem a little circular, but that's the idea. You see, when we click on the piece, we want to change its binding so that if we click on it again, then it will be deselected. To do this, we have each binding set up the other binding when it is invoked. Click around on the example a bit to see what I mean. The most important thing to remember when you're doing your GUI is that each piece should set up bindings unique to itself. So if you click on one piece it shouldn't set up bindings that might be confused with another. And make sure to call gui-return-from-human-move somewhere in your bindings. That's the core of the GUI and your link to Gamesman.

Remember, any and all questions are welcome on the CS3 newsgroup!

 

[Dan Garcia icon] [Department of Computer Science] [Official Website for the University of California, Berkeley]


Gamesman ©2003 Dan Garcia. All rights reserved.
Site design by Steven Chan. Site maintained by Hesam Samimi.