Overview
I'm going to talk about the clicking feature I just implemented using Unity as the first step in creating my game. Also code will be made available on my bitbucket account as soon as I get it cleaned up and this project under version control.
[UPDATE]
The source code has been pushed and tagged. You can find the relevant code to this post at: My Bitbucket Repository.
I've also went back through the post and linked to code and added a screen shot of my Logging Manager.
[UPDATE]
The source code has been pushed and tagged. You can find the relevant code to this post at: My Bitbucket Repository.
I've also went back through the post and linked to code and added a screen shot of my Logging Manager.
The Design
The Common Approach
Generally the process for capturing clicks and selecting units is described in the following Sequence Diagram (Note: Yes it is not proper UML but you'll get the idea and I've also included some custom annotations):
Figure 1: Common Selection Implementation |
Classes we need to code:
- ClickController:
- This class inherits from MonoBehaviour. This allows it to make use of the base class's Start, Update, Awake, OnEventMethods, etc. methods which tie us into the main process loop and events in the game. See its scripting reference.
- Note that Unity does not allow us to instantiate any class extending MonoBehaviour in code. We have to attach them to GameObjects and Unity will handle their initialization. So to keep things simple I would suggest you create a single empty game object to hold all of your instanced MonoBehaviour classes (I call mine ~InstanceManager). This way you don't have scripts attached to random objects (such as your main camera) just because you have to in order to use it.
- UnitManager:
- The UnitManager is created using a singleton design pattern so that we don't have to initialize it ourselves and that there is only ever one instance. (See Wikipedia: Singleton Pattern).
- Unit
- This is the MonoBehaviour that will be attached to all of our units. This allows us to add additional capabilities (via methods) and properties (via members) to the Unit such as movement, speed, hitpoints, etc.
Following the sequence
- The user first clicks some where on the screen. The click controller is watching the input on the mouse button and if it is clicked ever clicked...
- It uses Unity's SendMessage capabilities to...
- Call a handler defined in the Unit class (LeftMouseButton())
- The LeftMouseButton method gets and instance of the UnitManager
- Calls the SelectUnit method on the instance which adds the unit to a list of selected units
Thoughts
Although the concept behind the process is sound I do find that this approach has a few flaws:
- It's does not Separate Concerns well.
- In order to make project organized, scalable, modular and maintainable having good Separation of Concerns is crucial.
- When implementing future features, such as multi-select, deselection, group move commands, patrol commands the code will be messy and you'll have to hunt for where exactly a specific feature is implemented since it could be the Unit handling it or the UnitManager.
- Performance
- This method uses the unit it self to pass along the message that it was clicked and is called using a message system instead of a direct call. The compiler may be smart enough to see the 1:1 mapping to another call in the Unit but the use of a SendMessage method is worrisome. Since them method name is passed as a string Reflection will have to be used which is very expensive.
- (Ties into the Separation of Concerns issue) why does the Unit have to be involved with the selection process? All a unit should be doing is unit-esq stuff such as moving, shooting an dying. It's the responsibility of the unit manage to manage the units so don't waste the CPU cycles calling the unit or confusion of code location.
- Potential for duplication of code
- More then just units will be clicked on and not just units will move so why not have a base type to implement the shared features and just extend it.
- Coupling between the Unity Engine and User code.
- To make code more readable and, even though it's moot in this case, more modular between platforms. An abstraction layer should be at least partially implemented which maps unity and system methods/properties (such as buttons and keycodes) to actions. You should never see methods named LeftClick() or references to KeyCode outside of the mapping or interactions directly with Unity.
- Magic Numbers
- Although its not shown in the diagram I found an excessive use of constants inside the code, specially with mouse buttons (0=left, 1=right, 2=middle).
My Approach
Magic Numbers and Mapping
I defined two enumerated types to deal with the magic number issues as well as for mapping purposes:
- MouseButton
- Values: Left, Right, Middle
- Action - Maps actions to mouse buttons
- Values: Select, Interact, Manipulate
Explanation: To begin with the user is going to be able to do three things with units: Select them, Interact with them, and Manipulate them. Now using a mapping method I can define what mouse click will execute which action.
You can see how I handle the mapping in my UnitManager.cs file. The main entry point is at the ClickNotification() method. Following the logic there you can see how the mapping is done with the enumerations which occurs in the MapMouseButtonToActionHandler() method.
You can see how I handle the mapping in my UnitManager.cs file. The main entry point is at the ClickNotification() method. Following the logic there you can see how the mapping is done with the enumerations which occurs in the MapMouseButtonToActionHandler() method.
Logging
Figure 2: Log Manager Script in the Inspector |
Figure 3 to the right my Log Manager script attached to my ~Managers object. This allows me to adjust the default log level at run time as well as turn logging on and off for specific classes.
Base Mobile class
Any object that acts like a mobile (can moved, die, etc...) will be derived from this class. Currently this has no impact but will be utilized when I add unit movement.New Sequence Diagram
Figure 3: My Select Implantation |
You'll notice that work is being confined to the UnitManager and the ClickController is just notifying the UnitManager that a click occurs and passes along all pertinent information such as the GameObject clicked, location in global space and any key modifiers pressed. Then it's the UnitManager's responsibility to do what it needs to do. This time it checks its mapping for the mouse button pressed (which maps to select) and calls the SelectActionHandler which in turn decides that we're selecting a unit and calls SelectUnit.
At this point I have multi-select, individual selection and deselection and single selection working. But for all of these you'll note that the additional functionality doesn't change the sequence of events, only the actions called at the end. For instance if you are holding Ctrl and click a selected unit the SelectionActionHandler would call DeselectUnit instead of SelectUnit.
Next Steps
Get my code under version control and share it! Then I plan to implement movement and box selections.
No comments :
Post a Comment