Picking a 3D model is the term for being able to click on a 3D model using your mouse. At first glance, this may seem like something quite simple. After all, you can stick a mouseUp handler on a sprite to capture a mouse click, right? If you simply wanted to click on the 3D sprite this would work fine, but being able to click on an individual model within the 3D world is another story.
First, consider the fact that your 3D world likely has more than just a single model in it. Second, you are looking at this 3D world through a camera, and clicking a point on the camera's projection plane is a 2D operation. Examine the following image:
This shows the camera's projection plane from two different views. The projection plane is the 2D plane displayed in the sprite. It is the view you see of the 3D world from a given camera. Because the projection plane is the view you see in Director, within a 3D sprite, this is also the plane that registers your mouse clicks. In order for you to be able to click on a model, the point you click on the 2D plane must be translated into 3D world space.
Fortunately, Director makes this easy to do by offering two different methods for you to use: modelUnderLoc and modelsUnderLoc. I'll explain the first, which we will use in the game. I'll then touch on the second, so you know what it can do should you want to use it for another project.
The modelUnderLoc method will return a model reference to the first model located under the specified point. Using modelsUnderLoc instead will return a list of models under the point. Optionally, modelsUnderLoc will also return a detailed list of information about the models it finds, including the distance from the point, the actual intersection location in the model, and more. For the memory game, all we need to know is which card the player clicked on, and for that, modelUnderLoc will work perfectly.
Open memory.dir from your project_four folder. If your local file isn't available, open memory_start.dir from the Lesson16 folder on the CD-ROM.
With the file open, you can begin adding the necessary Lingo that will allow you to click on the various cards.
Right-click the 3D sprite and select Script from the context menu. Add the property declaration for sp.
You'll use the sp property to hold a reference to the sprite, for quicker access to the sprite's properties.
Add the line of Lingo to the beginSprite handler to initialize the sp property. You can add it as the first line in the handler:
sp = sprite(me.spriteNum)
This, of course, stores the reference to the current sprite in sp.
This simply creates a reference to the sprite, making for less typing later.
Create the mouseUp handler, as shown here:
on mouseUp me myPoint = _mouse.mouseLoc - point(sp.left, sp.top) clickMod = sp.camera.modelUnderLoc(myPoint) trace(clickMod) end
The first line subtracts the point of the sprite's top left corner from the mouse location. This yields the click location within the sprite, instead of the absolute location on the Stage:
As you can see in the image, the mouse is at point (380, 280) on the Stage. The 3D sprite's top left corner is at point (300, 180). Subtracting this from the mouse position gives you a point within the sprite, point (80, 100). If you were to simply use the mouse positionpoint (380, 280)you'd be using a point that isn't even in the 3D sprite. This point is stored in myPoint and then used in the modelUnderLoc method, in the next line.
Close the script window, then rewind and play the movie. Open the Message window and click on several of the game cards.
As you click on the different cards, you will see the model references appear in the Message window. You'll also notice that you can click the cards' sides and bottom, and even the rods holding the cards. Note that if you click outside of the game board you will see <Void> appear. This is important, because later you will need to filter the models that are clicked so that you're only able to click the cards.
Now, let's look at the modelsUnderLoc method. We won't be using it in the memory game, but it's good to get an idea of the data it returns, and how it might be used.
Stop the movie and right-click the 3D sprite. Select Script from the context menu. Replace the current trace command, within the mouseUp handler, with the following trace:
trace(sp.camera.modelsUnderLoc(myPoint, 1, #detailed))
Instead of sending only the click point within the sprite to the modelsUnderLoc function, you also need to tell it how many models to report onin this case just one. To get all of the data possible from the function, the #detailed parameter is used. Without using #detailed, you will get a list of model references only, much like modelUnderLoc, except that you're able to get more than just one.
Rewind and play the movie, and click on any of the game cards.
Instead of getting a simple model reference, you now get all kinds of information returned. As an example, clicking on model "c1", the upper left card, might yield the following property list:
-- [[#model: model("c1"), #distance: 221.4752, #isectPosition: ¬ vector( -78.6384, 70.7824, 1.0000 ), #isectNormal: ¬ vector( 0.0000, 0.0000, 1.0000 ), #meshID: 1, #faceID: 1, #vertices: ¬ [vector( -95.4023, 79.0599, 1.0000 ), vector( -95.4023, 55.0398, ¬ 1.0000 ), vector( -65.5173, 79.0599, 1.0000 )], #uvCoord: ¬ [#u: 0.3446, #v: 0.5609]]]
As you can see, this is quite a detailed list of information, some of which can be quite useful. Take for instance the #distance property, which gives the distance from the camera to the point of intersection with the model. If you were creating a game where you had to shoot an arrow at a target, the #distance parameter would be vital.
Another, less obvious, example is that of drawing directly onto a 3D object. The #uvCoord property that is returned lists the coordinates within the model's texture map where the model was clicked. By extrapolating those coordinates into pixels, you can find the exact location within the 2D texture map where the mouse was clicked. Add a little imaging Lingo, and you've got yourself a 3D paint program.
Stop the movie before continuing.
Filtering the Results
Now, let's get back to our behavior, and the modelUnderLoc method. As it stands, when you click on one of the models in the 3D sprite, you get a reference to that model, which is stored in the clickMod variable. What you must do now is add a bit of filtering so that only cards can be clicked on. It would be pretty strange if the wooden box holding the game flipped when it was clicked.
Recall also that if you click out in space, away from any model, the modelUnderLoc function returns Void. Before you can check which model was clicked on, you first need to be sure that a model was actually clicked on.
Right-click the 3D sprite and select Script from the context menu. Add the following conditional test to the mouseUp handler. Replace the line used to trace the modelsUnderLoc data with the test:
if not(voidP(clickMod)) then modName = clickMod.name --step 2 code goes here end if
It's always a good idea to remove any trace commands you might have placed in your code for testing. Otherwise, they will slow your movie down when it is played. Lingo's voidP() function is used when you need to see if a variable is void. If the variable has no value, voidP() will return true. If the variable has a value, voidP() returns false:
True conditional executes
False conditional fails
True conditional executes
So, if a model is clicked on, the code within the if statement will be executed. This check is necessary because trying to get the name property of a void variable will result in a script error.
Now, once you know that a model has been clicked you need to further check to see if a card has been clicked.
Replace the --step 2 code goes here comment within the if statement with the following test:
if modName.char = "c" then delete modName.char modNumber = value(modName) trace(modNumber) end if
The char property of a string allows you to get a single, or multiple characters, from the string. Have a look at the following sample from the Message window:
p = "c12" trace(p.char) -- "c" trace(p.char[2..3]) -- "12"
So if the first character of modName is a "c" then the code within the if statement executes. And the first thing that's done is the "c" is deleted from the modName. This leaves modName as a string containing only the number of the card that was clicked on. If card "c12" was clicked on, then modName now contains the string "12".
You should realize that this only works because the models were appropriately named within 3ds max. When you're designing your own games, you will need to bear these kinds of things in mind.
The next line uses Lingo's value() function to turn the string version of the number into an actual integer, and stores it in the modNumber local variable.
Rewind and play the movie. Open the Message window and click the game cards, as well as the sides of the box.
When you click a card, its numberfrom 1 to 16is traced to the Message window. When you click the sides of the box, or click out in space, nothing happenswhich is exactly what you want.
Stop and save the movie to your project_four folder, before continuing.
Let's take a moment to discuss how this is working so far. In the previous lesson you created the assignTextures() method which, besides placing the textures onto the game cards, also creates the global variable solveList, that contains the randomized list of texture pairs. As an example let's assume that after assignTextures() is called, solveList contains the following:
solveList = [6, 2, 5, 7, 1, 3, 4, 8, 5, 7, 1, 3, 6, 8, 2, 4]
Using the model number that was clicked on as an index into solveList you can find out which texture is assigned to the clicked square. For instance, let's say model "c5" was clicked onthe first card in the second row. After the code you entered executes, you will have the number 5 stored in the modNumber variable. To see what texture is applied to the card you can use something like so:
whichTexture = solveList[modNumber]
Because the fifth item in solveList is the number 1, whichTexture becomes 1. To match this card, the player would need to click card "c11" because the eleventh item in solveList is the number 1. Look at the following image, which shows the cards along with the textures applied:
Now you should have some idea of how to determine if a match has been made. Of course there is more logic than just this. For instance, when two matching cards are found, they need to be removed from the game in play so they can no longer be clicked on. Also, when two cards are flipped and there is no match, they both must be flipped back so you can try again. But before we get to that, let's start by flipping the cards over when they are clicked on.