There is no spoon
Legacy:Spark/Mouse Pointer
This page is supposed to document my attempts to create a RTS-like HUD, including (but not limited to) a mouse pointer.
If everything works well, maybe parts of this can be integrated into the "core" Wiki later.
Contents
Showing a Mouse Pointer
I'm going to use a subclassed HUD for this. It seems like a clean choice to me and I don't think there is any disadvantage to an interaction other than that I need a gametype for this.
First I simply want a pointer to show up, this part is pretty easy. I start by defining a var for the cursor (var SpriteWidget Cursor) and adding a texture to it in the defaultproperties (Cursor=(WidgetTexture=Material'Crosshairs.Hud.Crosshair_Cross1',TextureCoords= .. and so on)). Later I will probably do my own texture or even make it context sensitive.
The next step is to draw this cursor. For this I made a simple function (so I can easily extend this later):
simulated function DrawCursor (Canvas C) { DrawSpriteWidget (C, Cursor); }
Just like a crosshair, just incredible simple. I call this function from the end of DrawHudPassC, just like a crosshair (but I don't render a usual crosshair of course, this wouldn't make sense. Besides of this, my HUD is completely empty.
Next this mouse pointer needs to be moved to it's right location, this shouldn't be difficult.
We can simply use the WindowsMouseX and WindowsMouseY values of the Player class which lives in PlayerController. To place our SpriteWidget at this location, we can use this simply code:
Cursor.PosX = PlayerOwner.Player.WindowsMouseX / C.ClipX; Cursor.PosY = PlayerOwner.Player.WindowsMouseY / C.ClipY;
I divide by canvas size because the SpriteWidget struct obviously expects a scaled float value between 0 and 1.
There is a small bug it seems, as both values stop a bit too early (at least in 1024x768 it only goes until 1015x740). Once I change a random setting in the menu and click on "Apply changes", this is fixed so it looks like an engine bug.
Besides of this, it works very well. However, I'm not really satisfied as this somehow creates a "second cursor" to the menu cursor (which uses a different sensitivity and doesn't share the same location). It would be favorable to have a common cursor for both the menu and the game. Another way is to use a blank GUI page.
Using a GUIPage
Using the GUI system to draw the mouse (and control the game) has a few advantages, but unfortunately a few disadvantages as well. The advantages are:
- You can use MouseX and MouseY of the GUIController. It shares the same position and sensitivity with the menu mouse pointer.
- Standard input and exec functions aren't active (can't be executed), this might come in handy if you want to allow both first person controls and RTS controls without key binding headaches for the user.
- All GUI elements are available to be drawn and used on the screen, so you don't have to create your own buttons and similar controls.
The disadvantages (that I know about) are:
- Standard input and exec functions aren't active...
- While you can catch almost every possible key event with the "OnKeyEvent" delegate, it could be a PITA because the key and state is passed in as a byte, not a convenient enumeration. So if you want to check for the "Mousewheel Up pressed" event, you have to check for key "0xEC" and state "1". Have fun doing this for 50 keys. :) Making those keys all configurable would be even more of a PITA. This might all not be too much of a problem though, considering that RTS games are usually almost completely controlled by the mouse anyway. Implementing a few keys and shortcuts might be sufficient. It would be more convenient though if we could use an interaction for this task.
- The GUI code is to a large part engine controlled, so there might be unforseen troubles (I hope not...).
Enough rambling, here is how to do it (straight and to the point):
Create a blank GUI page
Subclass GUIPage and set the following default properties (some of them are redundant):
bAcceptsInput=true bCaptureMouse=true bRequire640x480=false bAllowedAsLast=true bPauseIfPossible=false
Make sure that you open this GUI page when going into strategy view (for example with ClientOpenMenu in your PlayerController).
Work around to get mouse input
Unfortunately I couldn't make my GUIPage really capture mouse events (when clicking anywhere on the screen). As a workaround, I create one hugeass transparent control all over the screen. I use this solely for the purpose of catching mouse events. To do this, add the following to the defaultprops of your GUIPage:
Begin Object Class=GUIButton Name=InputControl WinTop = -2; // Ugly hack to make the control always catch mouse events WinLeft = -2; WinHeight = 9999; WinWidth = 9999; StyleName="NoBackground" OnMousePressed=InternalOnMousePressed OnMouseRelease=InternalOnMouseRelease End Object Controls(0)=GUIButton'InputControl'
The weird seizes are because of another problem. If I use 0.0 for top and left and 1.0 for height and width (which should mean all of the screen), then the outermost pixels don't react to mouse events. Setting the borders out of the screen works (until we have monitors rendering more than tenthousand pixels).
OnMousePressed and OnMouseRelease are both delegates which will call the appropriate function if this event occurs. InternalOnMousePressed and InternalOnMouseRelease are my functions to handle these events. Note that both functions are defined in our GUIPage, as the button control is inline.
Handling key events
To get key events (which actually includes axis moving like moving the mouse and mouse buttons other than the first), we use the "OnKeyEvent" delegates, but this time we use the one of the GUIPage because it will capture all key input. So add the following to your defaultprops: OnKeyEvent=InternalOnKeyEvent and don't forget to implement this function. To see how all those three functions are declared (your headers have to match for this to work), I suggest a look at GUIComponent. To get more informationen about delegates, the UDN can help.
Implementing mouse scrolling
A common feature of every RTS game is, that you can scroll the view by moving the mouse pointer to an edge of the screen. This was really easy to reimplement. When using the GUIPage method, just check for "Player.GUIController.MouseX" (and .MouseY respectively) in your PlayerMove function and make the player move accordingly. The only interesting thing is, that there doesn't seem to be a simple way to get the current screen resolution (to check weither the mouse has hit an edge). I found the easiest way to work around this to copy the "Canvas.ClipX" and "Canvas.ClipY" values of the HUD to variables in the PlayerController.
Implementing a selection rect
This will not tell you how to select units inside of a certain rectangle but how to draw it. Everyone knows this feature from Windows, etc. Implementing this is fairly straightforward again.
First I created three new variables in my PlayerController, those are "bMousePressed", "SelectionRectStartX" and "SelectionRectStartY". Now in the InternalOnMousePressed function, I set bMousePressed to true and SelectionRectStartX/Y to the current mouse position, but only if bMousePressed wasn't true before (because this event is executes repeatedly while holding down the mousebutton).
In InternalOnMouseRelease I simply set bMousePressed to false again. This would be the place to fire a "SelectRectUnits()" function or something like that.
In my HUD code, I have a function to draw the selection rectangle. This simply checks weither bMousePressed is true and weither the current mouseposition differs from th starting positions. If yes, then I use the different drawing functions of the HUD (the Canvas to be exact) to render a simple rectangel from the start positions to the current position.
Comments
DJPaul: The mouse the HUD uses is rendered by something like Player.Viewport.bShowMouseCursor, but you won't find the Viewport class normally (cos it's a native 'un). UEd won't let me view it either. This sodding wiki's Viewport page is a load of bollocks, too >:O
Also, i'm not sure this page is the correct place for this. Perhaps a Developer's Journal, or the Help Desk might be more appropiate. When you've the details knocked out, feel free to add pertinate information somewhere relevant (the HudBase page?).
Spark: But you are reading my Developer's Journal right now. :) You just can't make subpages from Spark/Developer Journal, so it's Spark/Mouse Pointer while in fact it would be more like Developer Journals/Spark/Mouse Pointer. :) It's linked from my journal page.
And trying to access anything of Player.Viewport results in a "unrecognized member" error. Viewport(Player) works though but there is no bShowMouseCursor and no bShowMousePointer. There is a bShowWindowsMouse in the Player class, but this one never had any effect for me.
DJPaul: I think that is waht I mean. I have not tried using it on 2003, but it works with old UT in-game.
Spark: Ok, I go for the blank GUIPage for now because it works fairly well and hope that it won't be the wrong decision in the long run. :) I will try to keep everything clean and as abstract as possible, just in case. Now I can finally work on the fun part, selecting and commanding units.
NickR: One problem I have is that I need to be able to use the console at the same time as using a mouse pointer. I want to be able to bind a key to go from the regular control scheme to then using a mouse and not have control over my player via my regular keys. But could that be the reason why the console doesn't work? Because all input events don't get called when a GUI Page is present?
BTW bShowMouseCursor is declared in the Player Class. So you would use Player.bShowMouseCursor not Player.Viewport.bShowMouseCursor
I believe Viewport is a tiny mini class (for lack of better term). It calls the players viewport to bShowMouseCursor instead of just the player. Or at least... that's how I think it works O_o... it could just be the same thing...Player.bShowMouseCursor and Player.Viewport.bShowMouseCursor ;)
Foxpaw: Interesting thing about viewport, it isn't really a class at all, but it's interpreted as one natively. "Viewport" is a "subclass" of Player, and refers to a local player. "NetConnection" is also a "subclass" of Player, and refers to a remote player. However, neither is truly a class, but the compiler recognizes it as a class, even though it isn't really one. :rolleyes: The distinction is handled natively by the replication code.
Pingz: A little late, but if your trying to get the screen resolution without having access to a Canvas.
Screen.x = myHUD.ResScaleX * 640; Screen.y = myHUD.ResScaleY * 480;
ResScaleX/Y is set in Hud.PostRender().
porkmanii: You could also use the GetCurrentRes console command. The GUI uses this in a couple places. It returns a string like "1280x1024".
CurrentRes = PlayerOwner().ConsoleCommand("GETCURRENTRES");
EricBlade: Here's a UDN page on this very topic: http://udn.epicgames.com/Two/MouseCursorInterface .. I've plugged it's code in to try it out, and it works fantastically, but I can't think of any use for it for me. :D