Showing posts with label modules. Show all posts
Showing posts with label modules. Show all posts

Thursday, June 4, 2009

User inputs

I had mentioned previously that players have two inputs to their computer, and thus to Metaplace -- the keyboard and the mouse. Fellow beta tester KStarfire pointed out that I missed the joystick/gamepad. Does Flash support a joystick? I'm not sure, but I'll concede the point to him.

Metaplace uses a single method, the MakeInput() function, to define all of the inputs that a world understands. These are parsed at compile time to ensure they're valid, and passed to the client as a bunch of tags to tell it which inputs it should bother to send to the server. Here's its definition:

MakeInput(input description, input code, input event, input modifier, command string)

Where input description is something like "press I to open the inventory", the input code is "i", the input event is "down", the input modifier is "control" and the command string is "inventory".

Keyboard

Whether you're going old-school RPG with arrow keys or WASD movement, or just need a few keys to support inventory and status windows, the keyboard is a must. MakeInput() lets us define an event - that is, send a Command - upon a specific key press (up or down) with a modifier (shift, alt, none) for every need we have. Some of the keys have bigger "codes" than the letter they represent, such as "left" for the left-arrow key. This means we can do one-step-at-a-time walking, by just listening for "down" events for our arrow keys, or we can do walk-until-I-let-go walking, where we start walking on "down" event, and stop on "up". This sounds like everything we could want, but there are some limitations.

There are a bunch of keys that aren't supported. I won't spell them out here, but there's a bug filed (by me) in the Metaplace forums for certain punctuation keys that you just aren't able to listen for. This is problematic for me in my Ultima Online world, where I want to emulate the speech system of UO, where there's no chat box in which you click to start typing. This means that my current implementation (which you can try out) lets you type letters, numbers and a space, but none of your punctuation comes through, which makes you type like a ... well, like most of the people on the internet.

My UO world also points out another issue: to get this working, I had to make 26 lines of MakeInput() to handle each letter; another 26 for when I press shift (to get a capital version); 10 more for the digits; space, enter... that's 64 right there. If the punctuation was working, I'd have even more. And what if I wanted to catch every keypress possible? UO allowed you to map keypresses to macros, such as "control-e" for the allnames macro (which happens to work in my UO world, if you want to try it.) This means four modifiers (none, shift, control, alt), two events (down and up), and ... 100+ keys? That's 800 MakeInput() lines! In my UO world, I only have half of them (I wasn't interested in key-up events). Rest assured that I wrote a program to write those all out for me. The problem that this points out: there's no way to say "any" for the "input code", to say "for any keypress, send this command" or "for any shift-keyrelease, send this command".

Another problem that arises is that a given code/event/modifier combination can only have one possible Command that it will send -- you can't have "i" both bring up your inventory and cast the Invisibility spell. If you define a MakeInput() more than once for the same combination, it used to be the case that the client used a random one. Now it looks like the last-defined one -- the one in the script loaded last -- is the one that wins.

For a user creating their world completely from scratch, this shouldn't be a problem - they know what they want each key to do, and aren't likely to re-define the same combination again for another purpose (though I'll come back to this). The problem is more likely to appear when the Marketplace becomes involved, when a world-builder buys off-the-shelf modules that define MakeInput()s. As more and more content becomes available, the chances of these modules colliding is going to grow.

This has already occurred. KStarfire had a world where the spacebar was the "throw" command; after he had created that functionality, the avatar module -- probably the most ubiquitous module in Metaplace -- added a jump action to the avatars. And what key did they choose for that? That's right, the spacebar.

KStarfire, at that point, had two choices: either edit the avatar module, change the MakeInput() line where the jump was defined, and have to do this every time the module updated; or change his own code. But what if the "throw" command was one that he had purchased, instead of written himself? Another solution would be to allow the keys for every module to be defined by the user or world-builder, to have the MakeInput() commands pull their values from a user or template property. Unfortunately, the MakeInput() command doesn't allow this (future blog post).

So is there a solution? I think so. I think that a module needs to be developed that defines every single combination in MakeInput() and sends a single command from the client to the server when it happens. This Command would then fire a Trigger to the the user object, and at that point, any interested parties could listen for the Trigger, and based on configuration, decide if that matters to them. So the avatar system could allow the user, or world admin, to say that "control-spacebar is jump" and "spacebar is throw", and each module would handle things the way they should. Additionally, modules could, if they wished, share the same keypress.

I believe in this solution so much, in fact, that I've implemented it. Also, I was able to work around the limited environment of the Define Commands() block to come up with this little gem to save myself from typing an enormous list of MakeInput()s:


keys={"numpad0","numpad1","numpad2","numpad3","numpad4","numpad5","numpad6","numpad7","numpad8","numpad9",
"f1","f2","f3","f4","f5","f6","f7","f8","f9","f10","f11","f12",
"backspace","tab","return","pause","capslock","escape","space","pageup","pagedown",
"end","home","left","up","right","down","print","printscrn","insert","delete","help","numlock","scroll",
'-','=','\\','.','/','0','1','2','3','4','5','6','7','8','9','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',
"shift","control","alt","lshift","rshift","lcontrol","rcontrol"}
states={"down","up"}
modifiers={"none","shift","control","alt"}

for x=1,#keys do
key=keys[x]
for y=1,#states do
state=states[y]
for z=1,#modifiers do
modifier=modifiers[z]
MakeInput('keypress '..modifier..'-'..key..' '..state,key,state,modifier,(state=='up' and 'un' or '')..'key '..modifier..' '..key)
end
end
end


This generates every possible MakeInput() that Metaplace supports right now (with all of the missing punctation). It fires a Trigger key(modifier,code) for key down, and unkey(modifier,code) for key up. Now I can have code such as


Trigger key(modifier,code)
if code==self.jumpkey and code==self.jumpmodifier then
SendTo(self,"jump")
end
end


or even have a way to encode the modifier and code into a single value so there aren't two values to store and compare, and have a comparison function:


Trigger key(modifier,code)
if self.jumpkey==keypair(modifier,code) then
SendTo(self,"jump")
end
end


I really think that this approach is going to be inevitable, because the colliding modules is going to happen more and more, and the need for customizable keypresses, either to prevent collisions or to allow flexible worldbuilding (not to mention, to be able to support key macros) is going to be in demand. And, as I hinted at before, we might want to change the meaning of a key halfway through gameplay; for instance, I might want "i" to open the inventory in a non-combat mode, but when I'm in combat mode, "i" might cast the Invisibility spell. This could be one Trigger that handles the keypress, that understands both modes, or two separate Triggers (in different modules) that each handle a specific case, unaware (and uncaring) that the other exists.

I have one last thing that I'd like to see support for -- held-down keys generating repeated key-presses, much as we're used to seeing on computers when we hold down a key in our word processor. Some world might need this functionality (such as my UO speech), but others probably won't; once I add this as an option, I'll publish the above code as a small module for universal key handling.

One last thing about the keyboard: even with the Flash client in focus, some browsers out there rudely capture keypresses instead of passing them on to Metaplace. Specifically, the "control-c" in UO is meant to refresh your mouse cursor (there's a bug there I have yet to file), but in Internet Explorer 8, this brings up a menu or something. I've seen this on some of the other browsers, with certain keypresses, whether it toggles bookmarks or who-knows-what. This is an unfortunate issue that world- and module-builders will have to keep in mind -- the browser compatibility issues reach further than the web page!

Mouse

The mouse is a must in today's computing, giving us near-instant pointing and selection. Very few worlds are likely to get by without its use, largely in part to the graphical nature of the worlds (plain worlds such as Plain Old Chat.)

Unlike the keyboard, there are only a few different input codes available for the mouse:


  • mouse-terrain -- clicking on the ground (on a tile)
  • mouse-object -- clicking on an object


"click" and "double-click" are the only two events supported. Embarrassingly, I've never tried the "double-click" event, since the only one of my worlds that needs it (UO) was created before this event was added, and I ended up writing my own (in a similar manner to how I'll write the repeating key code). All of the modifiers are supported (none, shift, control, alt).

There are lots of things missing here. There's no way to distinguish where the mouse-click went down, and where it was released (I believe the location of release is what is sent to the server). This is one that I think could add some extensibility to a world's interface, and wouldn't increase the traffic at all.

Having the mouse location, either while holding down the mouse button or not, would of course be valuable. This would let us have draggind-and-dropping, of handling mouse-over/hover (future blog post) very easily, and to have the mouse cursor itself act as an agent in the game, as the avatar itself. We're told that the traffic between the client and server would be too large to allow this feature. I can certainly see that there *could* be a lot of traffic, depending on how often you sent the mouse data. But I propose that this is a setting we should be able to send to the client,

SetMouseLocationUpdateTime()

as well as provide a way to enable and disable the flow of this data, so worlds that need it can request it:

StartMouseLocationUpdates()
StopMouseLocationUpdates()

And of course, the client could have a hardcoded maximum of tags being sent in.


And last but not least, there's the fact that all mouse events currently represent the left-mouse-button only. No right click (nor middle click, nor sideclick?). This is a Flash problem, as I mentioned in the post on movement, but indeed a problem. As I had said, Ultima Online used right-mouse-hold (or right-mouse-doubleclick) for movement, and everyone associates right-clicking with a context menu. Other clients (a future blog post) could implement this support, of course, but I think it unlikely that we're going to see support in the Flash client. And, of course, even if other clients COULD support it, there's no way for the world to state that it's interested, because it's not an option in MakeInput().

So for now, workarounds need to be made. Luckily (perhaps oddly), double-left-click on terrain had no meaning in Ultima Online, so I've used that as my walk-to command (I use single-left-click as a "look" command.) As for workarounds for other missing things, I use ctrl-left-click as pickup/drop (instead of drag-and-drop). Though I also have a little hack for this, too, which I'll mention ... in a future post. Shift-left-click is, I believe, used to bring up the Behavior Tool, which acts in place of a right-click for a context menu, and the current avatar behaviour seems to go with single-left-click for bringing up a context menu on other avatars (for meeping, sending friend requests, offering gameplay, etc.)

I can also see a need for a mouse equivalent to the "every key" module I mentioned above, letting world owners define the mouse meaning they desire to various actions: if I want single click to be "look", and double click to be use, I should be able to set that instead of be forced to use whatever the module-writer decided.


Overall, I'm a bit disappointed with the mouse support, because it's really the primary interface for many. The modifiers can help us for now, but I really hope that, once all of the bigger pieces of Metaplace get done, the mouse support can be revisited.

Joystick?

Is this so far-fetched? Not really; I have no idea if Flash can support joysticks, or gamepads, but custom clients sure can, but we still hit the problem of having no support for them in the MakeInput() call. And this brings up something that has stuck in craw for a bit: the special nature of MakeInput().

I had planned on talking about this in a future post, but I really don't understand why MakeInput() must be in the Define Commands() block. Or at least, why it must ONLY be in that block. I can see a desire to precompile this, to ensure that the definition is done correctly. Or can I? Every other API call checks this at runtime, not at compile time, and lets the world owner know in the Debug or Log window, and lets the user know by just not working. Why can't MakeInput do the same thing? Why can't I call MakeInput() later in my script, after asking the user to define some keys, of after the user's preferences have loaded? Why can't we call DeleteInput() -- there's a tag ([W_DELINPUT]) that exists for when a script is unloaded, which removes a defined input, so doing so after the world is running, and a user is connected, is certainly possible. Why can't we add inputs after we're up and running?

Maybe there's a good reason, maybe it goes against the design of the system, or maybe this was just oversight. Some day I hope to know! Until then, though, I hope my module will be a viable workaround, or that the collision of inputs doesn't become too big of a problem, too soon.

Wednesday, June 3, 2009

Worldspace versus Screenspace

In Metaplace, you have two canvasses on which to work: the "worldspace", where all of your objects sit, and the "screenspace", which is where the UI elements sit. There are a few key differences to note about them: worldspace is seen by everyone, where screenspace may or may not be shared; worldspace is affected by zoom, where screenspace is not; and screenspace is always on top of worldspace.

These three differences can be utilized to make all sorts of interesting effects that aren't immediately obvious to world builders. The fact that screenspace can be on a per-user basis means that it can help implement worlds where not everyone has the same knowledge -- fog-of-war and different kinds of "vision" (infravision, see-invisibility) can be supported through interesting uses of screenspace instead of worldspace. Basic menus and dialog boxes inherently "take advantage" of the fact that the UI doesn't zoom while the worldspace does, although the opposite can also be true -- zoom way out in a world (that supports it) with nametags on the avatar: the nametag soon dwarfs the puny little figure.

There might also be times when you want to have an effect between two objects: my Masses and Springs world needed to draw a line between two objects, but a line is in screenspace while the objects were in worldspace (if you go see the world, it currently suffers from a bug, which I've just reported -- it works okay if you click down-and-right of the face. Also, it only really works if you get out of full-screen mode, for reasons mentioned below).

So how do you go about matching up screenspace to worldspace?


There are four methods to attach UI (our screenspace objects) in Metaplace, two that are seen by everyone, and two that are per-user. UiAttachWorld() is attached to the viewport itself, treating the top-left corner as the origin, and is seen by everyone. UiAttachUser() is the per-user equivalent. UiAttachObject() attached UI to a specific worldspace object, based on the origin of that object (this is important later), and is seen by anyone who has that object on-screen. UiAttachUserObject() is the per-user equivalent.

Each of these functions has their use: UiAttachWorld() is good for a high score board, or a population monitor; UiAttachUser() is good for a HUD with user-specific values; UiAttachObject() is good for nametags; and UiAttachUserObject() is good for context menus. For the latter two, the UI "follows" the object as it moves (or as you move away), which is what you likely want, and is all nicely left up to the client; if the object moves off-screen, you don't see the UI, and that's probably a good design. Any UI attached to an object will hopefully be an appropriate size, such as a context menu that is reasonably sized to fit menu choices, or a nametag that is readable but not covering up half of the screen.

But is the nametag an appropriate size? What about when you zoom way out, like I mentioned before? And in the case of UiAttachWorld() and UiAttachUser(), the screenspace objects don't follow a specific object, but sit on-screen until removed, taking up real estate -- but how much?

The scaling and positioning of screenspace objects to worldspace objects is a two-part problem: zoom and screen size. The zoom can be read with GetPlace().zoom , but that only retrieves the "set" value of zoom, and cannot take into account any mouse-wheeling done by the user. This means that any screenspace-to-worldspace mapping requires the world to enable zoom lock. There's no way to set zoom from script (anymore - we used to be able to fake it with OutputToUser()), so we can't even force a zoom level on users if they change it, or if we want to have different zooms for different cases. Screensize is even worse: there's no function to request a user's screen size (which could vary like the zoom could), and there's no support for the undocumented P_VIEWPORT tag to force a client to a specific size.

Why is this such a problem? To do any sort of mapping between screenspace and worldspace, whether it's position or sizing, we need one or both of these values. Sizing, of course, only requires knowing zoom, so provided we enable zoom lock, we could ensure that or nametags are scaled appropriately with our avatars by using GetPlace().zoom to resize them. But since we have no way of changing zoom, I suppose this is really a one-time calculation anyway. Not too useful. But for positioning, and especially for screenspace items that might span between two worldspace objects, both position and size matter.

To map position between screenspace and worldspace, we need to know at least one point that maps between the two. Metaplace has the idea of a "camera", which is where the center of the viewport is focused; for most worlds, this is usually centered on the player's avatar, but Metaplace also allows the camera to be focused on a specific point in the world. The important thing to note is that this value -- either the player's position in the world or the camera's focal point -- is a worldspace coordinate that we know, and it maps to the center of the viewport. The fact that this is the center is important because we don't know how tall or wide our viewport is, which means that we still have no way of determining where in the worldspace the top-left of our viewport maps to. This means that we must always attach the screenspace items based on the center... but we don't know the center of the viewport, either!

So what do we do? If the camera is focused on the player, then we just use them as our center, using UiAttachObject() or UiAttachUserObject(); and if the camera is focused on a specific worldspace coordinate, we have to figure out how far away a known object is from that worldspace location (the player, or perhaps a more stationary object), convert this distance to screenspace (which we still haven't figured out how to do), and then attach to that object.

Sounds easy, right? Let me make it worse: the view type will also affect your calculations! Metaplace currently supports a bunch of views: top-down and side-view (which thankfully use the same scaling); isomorphic, stepped and sloped isomorphic; and rotated (or "UO") view. Also, as we hinted at earlier, the origin of the object to which we're attaching can matter, if our screenspace effect is related to the object itself.

Let's try to make sense of all this. First, let's think about scale. A zoom of 1.0 in Metaplace means that a tile is 64 pixels wide, regardless of which view we're in. Hurray for small blessings! This means that for every 64 pixels on the x-axis that we move something in screenspace, it will shift the equivalent of one tile over in worldspace, at zoom 1.0. A zoom of 2.0 makes everything twice as big, which means that one tile is now 128 pixels wide, and so our general formula is

screenspacewidth = (number_of_tiles_wide)*64*zoom

or

number_of_tiles_wide = screenspacewidth / (64*zoom)

The height of a tile depends on which view we're in: top-down and side-view use square tiles, so the size is also 64 pixels high, at zoom 1.0; rotated view's tiles are diamonds, but are proportional, so they, too, are 64-pixels high; and the isomorphic views are nicely at an angle where the tiles are half the height of their width, so 32 pixels high, at zoom 1.0. Given this, we now know how to scale screenspace elements to their worldspace counterparts; we know that if we want a UI window, button, etc. to be two tiles wide and two tiles high, we could write a function like this:

function tiles_to_pixels(user,w,h)
local tilewidth=64
local tileheight=64

local view=user.place.view
if view==2 or view==3 or view==4 then -- isomorphic
tileheight=32
end

local zoom=user.place.zoom

return w*zoom*tilewidth,h*zoom*tileheight
end

and then might use this function in such a way:

w,h=tiles_to_pixels(2,2)
local win=UiRect(0,"blank window",w/2,h/2,w,h)
UiAttachUserObject(self,self,win)

to draw a big square on top of the player. Big deal? It will be the same relative size, no matter what you set the Place's zoom level to. That's the big deal.


But what about position? This is all fine and good if we want scaled screenspace items that are a given distance from the player, but what if we don't know that distance all the time, because the player can move about?

Let's say that the player is at (10,10) in our world, but we want a screenspace item to appear at (4,6), because, perhaps, they have a treasure map marking X, and the player's Treasure Spotting skill is high enough that a glowing X should appear in the distance. One way would be to just drop a glowing X object into the world and be done with it, but other players would then be able to see it, even if they don't have the Treasure Spotting skill. Alternately, we could drop an invisible object in that location, and then attach a glowing X to it using UiAttachUserObject(), so only the Treasure Spotting player sees it; this would work for most cases, but some people will point out that the object information is still being sent to everyone else's client, and thus they can still know that the object is there, even without a glowing X. If that's not enough of a concern for you, let's say instead that we want a glowing line drawn from the player to the location of the treasure -- there's no UI function to attach a line from one object to another, so we're back to going through all of this nonsense anyway.

It sounds easy, that we can take our current position (10,10), and the desired position (4,6), find the difference (-6,-4), turn those into pixels (-384,-256) or (-384,-128) depending on view (at zoom 1.0), and voila, we have our offsets from our player's position in screenspace. And that's true -- if we're in top-down or side-view.

But this is not the case for any of the other views:

rotated (UO) view


iso view


See how going from (10,10) to (9,10), while it sounds like we're moving one tile "left", we're actually moving left and up. This is the "rotated" part of the view, where the X and Y axes of the worldspace don't align with the X/Y axes of screenspace. This means even more figuring, to determine how much each of our tiles in worldspace slide along these diagonals in screenspace.

Let's look at the rotated view first. I think it's pretty clear from the diagram that as we move along the X axis (from (9,10) to (10,10)), we're moving half-a-tile to the right, and half a tile down, or (32,32) pixels in screenspace (at zoom 1.0) for every increase along the X axis in worldspace. And, as we move along the Y axis (from (10,9) to (10,10)), we move half-a-tile to the left, and half-a-tile down, or (-32,32) pixels in screenspace for every increase along the Y axis in worldspace.

This means that our previous attempt, where we determined that our offset (our difference) was (-6,-4), can use these offsets to figure out where we're going: since the difference was -6 on the X axis, we can multiply that by the (32,32) we figured out and get (-192,-192); the difference on the Y axis was -4, multiplied by our (-32,32) gives us (128,-128). Add these two together, and we get (-64,-320) instead of the (-384,-256) we originally (naively) came up with. If you're still not convinced, try it again going from our (10,10) to (6,6) -- looking at the diagrams above, you should realize that if we step back on both the X and Y at the same time ((10,10) to (9,9)) the actual screen movement is straight upwards. Your result should have zero for the X, and a negative number for Y.

And what about the isomorphic view? Well, it's still the same formula of "half-a-tile right" and "half-a-tile down", but remember the height of the tile, in screenspace, is the only thing that changes from the rotated view to the isomorphic view, so if we figure things out based on "tiles left" and "tiles down", we can convert that using our function above.


The calculations above let us figure out the offset of the glowing X from the player, on the player's screenspace. But the player is likely moving. If we didn't take that into account, that red X would stay the same distance away, "moving" along the ground as the player did. Using this approach, we'd have to re-adjust (using UiPosition()) the X as the player moved, hooking into path_begin() and/or path_end(), or on a tick-based timer.

Alternately, we could attach it to a stationary object; the X-marks-the-spot code might find a tree, rock or seashell near the (4,6) location, and use that as the attach point. The drawback is that the code would have to do a search for an object, instead of just lazily using the player, but the savings would be in having to redraw the X. As long as the code can determine that an object isn't likely to move, this might be worthwhile.

So where's the final code? Err, well, I had plans on posting the two functions that I use often -- worldSpaceToScreenSpace() and screenSpaceToWorldSpace() -- but realized as I was writing this post that I don't handle all views (the "rotated" view had been removed when I wrote it, so I don't support it), nor do I handle the camera not being attached to the user (the code makes a bunch of assumptions in this regard). Once I add this extra support, I'll post the code to the Marketplace.

I didn't talk about how to handle that -- the camera being fixed on a location instead of the player. It's really just one more offset to be calculated, though; if we already know how to figure out how to get a screenspace object to appear at a certain worldspace location, if the player is at the center, so as long as we can convert the player's location from the camera location, we can still figure out the correct offset. I realize as I type this that diagrams might be helpful, but trust me when I tell you that drawing those two pictures above taxed my artistic skill.

The last thing I glossed over is the origin of an object. Metaplace allows the origin to be defined anywhere on the sprite that represents the object - three "fixed" modes (top-left, center and bottom-center), as well as a free-form "percentage" system. While this doesn't matter if you're just trying to find another worldspace location -- the player is at (10,10), regardless of how the sprite is drawn) -- there are times where you might want to know the exact screenspace coordinates at which an object's sprite is drawn. This can depend on the scale of the sprite, and the zoom as well. One example for this might be to draw a box around a sprite; knowing the height and width isn't enough, if you don't know where to start drawing. I have a function for this, too, which returns the top-left, center and bottom-middle screenspace coordinates of any object, which are part of my same screenspace/worldspace library.

I'm surprised how often my world ideas require mapping values between the two spaces; my UO world needed it for dropping items in-hand, and that's where I first wrote it. But things like the nametags should scale in size and location, regardless of zoom, as should speech bubbles. And attaching UI effects to objects (damage numbers, highlighting or selector graphics) could also take advantage of this to good effect.

Tuesday, June 2, 2009

Blog content -> Metaplace content

My first few posts have been mainly about Metaplace from a scripting point-of-view, and I can't promise than many of the future ones won't be along the same theme.

That being said, I hope the non-programmers reading the blog don't stop reading; even if you're sure you'll never decide to try your hand at scripting, reading what's possible in Metaplace can help you design your worlds and try to find folks in the Metaplace community that are willing to implement some of the ideas you might come up with after reading this blog, perhaps in exchange for some design ideas, some writing/dialogue, or some art, depending on where your talents lie.

Also, if anyone finds modules or worlds that demonstrate any of the ideas that I put forth, and I fail to mention them in the blog itself, please let me know, either in a comment or in Metaplace mail, and I'll try to keep such information up-to-date for future readers. While this might seem like the Metaplace Marketplace's job, that does require authors to properly tag their creations, and if Metaplace becomes as successful as I expect, the Marketplace is going to become very full indeed, perhaps causing some worthwhile modules to be missed.

Of course, I'd love to be the one implementing all of the ideas that I come up with here, but family life comes first, which means that Metaplace, unfortunately, doesn't get the time that it used to. Still, I do have my ever-growing list of ideas and projects, and SOME day I'll get through it... perhaps when my grandchildren start helping.