Tutorial 2: Custom-Viewer
From WiCWiki
|
by Insane Buzzstards
This is a tutorial about the Custom Viewer !
Questions and Annotations
This tutorial can be discussed in this thread. Please help us making this tutorial as perfect as it can be, by giving hints and writing comments!
Introduction
This is one half tutorial and one half tool-presentation. So what is this Custom Viewer??? Did you already take a look at WiC's Debug Viewer??? The Custom-Viewer is almost the same. It is a game-overlay with the help of which you can execute Python-Code whenever you like. The difference to the original debug-viewer is, that it is build for easier extendability, so that you can easily and efficiently add your python-code to the viewer. The viewer has almost no options in the beginning, but you can later create your own modules and entries, which you can easily share with all other modders via the WiC-Wiki. If such a module has a bunch of different options and entries, we will call this an editor. One example for an editor, could be the AreaEditor, with the help of which you can create and visualize areas in the landscape.
Here is a screenshot of how a very simple custom-viewer could look like:
Look here for a list of already existing Custom-Viewer-editors.
In this tutorial, we won't teach you how to write your own custom-viewer, but explain how to extend our Custom-Viewer to fit to your needs. We will give you an overview of the structure and the methods you will have to use.
Resource downloading
You can download the py's of the custom-viewer here: [1]
Reading: Structure
So ... this chapter is titled "Reading: Structure" ... do what it says: just read and don't try anything. This is mostly just background. We will do something practical later, in the tutorial-part!
What is in this package?
cviewer.py # your customized viewer cust_viewer/Entry.py # sources for the viewer cust_viewer/misc.py # some other stuff that might be needed input_handler/InputHandler.py # code for handling user-input input_handler/keys_scancodes.py # variables for most keys (for user-interaction)
Place the "cviewer.py" and the "cust_viewer" and "input_handler"-folder in a folder that is included by the WiC-Python-Includepaths, f. ex. in "python" or in your modfolder. To use the Custom-Viewer, you have to import cviewer and call cviewer.myViewer.activate() or cviewer.myViewer.toggleViewer(). To make things easier, you can add it to your "wicautoexec.txt" (in the WiC-folder of "My Documents" ... create it if it doesn't exist) and toggle the viewer via F2:
py import cviewer bind F2 py cviewer.myViewer.toggleViewer()
Usage
Select entries with your left-mouse or navigate with the up- and down-arrow-key. To expand an entry or make it do what it says press "space".
Entries (module: "Entry" in package "cust_viewer")
Members
The Custom-Viewer is based on entries. An entry is simply a node in the list. An entry has the following members (some of them are not important to you):
m_text = the main-text of the entry (string) m_preText = additional text before main-text (string) m_postText = additional text after main-text (string) m_childList = child-entries (List<Entry>) m_childMap = child-entries (Dictionary<String,Entry>), the key is the m_text of the child m_isExpanded = is the entry expanded (boolean) m_isVisible = is the entry visible (boolean) m_color = the current color of the entry (int 0x) m_actions = the actions of an Entry (Dictionary<String, List<Action>>), you can create groups of actions m_parent = the parent-Entry (Entry), None for root-entries m_positionInParentList = the position in the parents entry list (int) m_tooltip = a tooltip that will be shown to give information (String)
Creating Entries
When you create an entry, you mostly give the constructor 2 values: the text of the entry and its parent.
child = Entry("I am a child of my parent", parentEntry) childChild = Entry("I am a child of child", child)
When you create an entry, the entry adds itself to the children of its parent. To be honest: it adds itself to a list of childs ("m_childList"), which is important for the order, in which the entries are printed, and a dictionary of childs ("m_childMap"), which is better for accessing the entries. In the dictionary, the key for an entry is its "m_text"-attribute (without pre- or post-text).
Instead of giving "child" as the parent of "childChild" you could also do the following:
child = Entry("I am a child of my parent", parentEntry) childChild = Entry("I am a child of child", parentEntry["I am a child of my parent"])
This is helpful, when you create parents and children in different methods.
The Custom-Viewer itself is a class. Every Custom-Viewer has a root-entry, that is automatically created, which is myViewer.m_rootEntry. So the first entry you will add to a viewer will be a child of the rootEntry:
child = Entry("I am a child of the root-entry", myViewer.m_rootEntry) childChild = Entry("I am a child of child", myViewer.m_rootEntry["I am a child of the root-entry"])
Note that myViewer is an instance of a custom-viewer. Such an instance will already be created for you in the cviewer-module.
Actions
You can add actions to entries and execute these actions whenever you want. What is an action? Actions are provided by WiC and are nothing but a way to store methods and execute those whenever you want. You create an action in the following way:
a = Action(nameOfMethod, para1, para2, para3)
Now you have stored an action and whenever you call "a.execute()", the action executes the method "nameOfMethod" with the parameters "para1", "para2" and "para3". You may think that this is pretty senseless, because you can call the method on your own, but in the end this is very useful, when having a list of methods you want to execute. You just have to iterate over the list and call action.execute() for every action.
As we said, you can add actions to entries, what is useful for reacting to user-events. The actions are stored in groups of lists. This makes it possible to react to different user-events. You could for example make a group of actions, that shall be executed when the user presses "g" and another when the user makes a double-click on the entry.
The method for adding an action to an entry takes 3 parameters: the entry, the action should be added to. The group, the action should belong to and the action itself:
entryAddAction(child, "use", Action(entryToggleExpand, child))
As you see, we add the method "entryToggleExpand" to the group "use" of the entry's actions. This method expands the entry if it is not already expanded and verse visa. We can execute all actions of a group by calling the Entry-method "executeActions(group)":
child.executeActions("use")
Making this call will expand or unexpand "child". Later we will teach you, when you will execute an entry's actions.
There are two actions that an entry will automatically create for itself, namely toggling its color when it is selected or deselected. So the two groups "select" and "deselect" already exist.
NOTE: entryToggleExpand is already deprecated, but it serves as a good example! If you want to create an expandable entry, use the entries "makeExpandable(isExpanded)"-method instead, this will automatically add entryToggleExpand to the "use"-group.
The CustomViewer (module: "CustomViewer" in package "cust_viewer")
The viewer itself just has a few methods:
activate() # activates the viewer deactivate() # deactivates the viewer toggleViewer() # toggles the activation-state of the viewer update() # the update-method initViewer() # initialize the viewer by creating the entries printViewer() # prints the viewer and its entries setSelectedEntry(entry) # sets which entry should be selected getSelectedEntry() # gets the selected entry setPreviousSelectedEntry(entry) # sets which entry was previously selected - you won't need this one getPreviousSelectedEntry() # gets the previously selected entry selectEntryByPosition(xPos, yPos) # selects an entry by given coordinates (used for selecting entries by mouse) selectNextEntry() # selects the next entry (goes downwards in the list) selectPreviousEntry() # selects the previous entry (goes upwards in the list)
Those should be clear.
Your customized Viewer (module "cviewer")
update()
The update-method is called for every frame. It checks the user-input and redaws the viewer and everything else you want to draw.
user-interaction
There are already parts, f.ex. for navigating with the arrow-up and arrow-down-key and handling left-mouse-clicks and left-mouse-double-clicks, as well as handling the "space"-key. The logic is quite simple. You can press a key (keyboard or mouse) in three different ways: you press it once and want a single action. you press it twice for another action (f.ex. mouse-double-click) or you hold it to repeat an action until you release the key. So there are three different methods given in the "InputHandler", which you will use.
# checking if a key was pressed and execute an action just once def checkSinglePress(key, action) # key = the key, that shall be checked # action = the action, that shall be executed
# checking if a key was pressed, executing an action just once and check if double-press and execute second action once def checkDoublePress(key, singleAction, doubleAction, dblPressTime=0.2) # key = the key, that shall be checked # singleAction = the action, that shall be executed on a single press, give "None" if you just want double-click-action # doubleAction = the action, that shall be executed on a double press # dblPressTime = how much time can be between the first and second press to execute the double-press-action, default is 0.2s
# checking if the user holds a key and execute it def checkHoldingKey(key, action, timeFirst=0.4, timeNext=0.03) # key = the key, that shall be checked # action = the action, that shall be executed # timeFirst = how much time should pass, until the second execution can follow, default is 0.4s # timeNext = how much time should pass, until the next execution can follow (except the second), default is 0.03s
There are also methods that do the same as those, just for multiple keys at the same time. For an overview over all keyboard-keys, look into the "keys_scancodes.py" . Mouse-keys are in the "InputHandler.py"
Keyboard-Key-Example
As you can see we have to give an action to these methods. So when you want to add a key, you have to write a method that describes what the key should do. Here is an example for the space-key. You can create every other key in the same way:
# the method that will be called when "space" is pressed def key_act_SPACE_single(): selectedEntry = myViewer.getSelectedEntry() selectedEntry.executeActions("use")
And in the "update()"-method:
checkSinglePress(KEY_SPACE, Action(key_act_SPACE_single))
In this example we check if the key was pressed once and we want to have a single-action. Always declare the methods in this format "key_act_IDENTIFIER_TYPE". An identifier is the name of the key, f. ex. "KEY_SPACE", just without "KEY", so just "SPACE". The TYPE is the way in which the key is pressed. Use "single" for single presses, "double" for double-presses and "hold" for holding the key. The format is important, as different editors might use the same key for different actions for their entries (what is perfectly possible).
As you can see, we check whether the user presses space and if so we get the selectedEntry and execute all its "use"-actions, f.ex. expanding it. This is the place for executing the entries' actions. But you can do whatever you want in these methods - there's no need that it has something to do with the custom-viewer or the entries.
NEVER use two of the check-methods together for one key. that makes no sense, except you are able to control the behaviour. So if you want to have a single and a double-press, just use the double-press-method.
Mouse-Key-Example
Another thing: if you are writing a method for your mouse-keys, the method needs to have 2 Parameters for the mouse-coordinates, even if you don't use those. Otherwise the game will crash. Here is an example for the mouse-left-key:
# this method will be called when the user clicks once with the left-mouse-button def key_act_MOUSE_LEFT_single(posX, posY): # do stuff here
# this method will be called, when the user makes a double-click def key_act_MOUSE_LEFT_double(posX, posY): selectedEntry = myViewer.getSelectedEntry() selectedEntry.executeActions("use")
and in the "update()"-method:
# check for left-mouse-button-interaction, different actions for single- and double-click checkDoublePress(KEY_MOUSE_LEFT, Action(key_act_MOUSE_LEFT_single, 0, 0), Action(key_act_MOUSE_LEFT_double, 0, 0))
When declaring the actions, you have to give the mouse-coordinates as zeros or whatever you like. Those will be filled with the correct coordinates later!
Printing/Drawing
At the end of the update-method you will also do all the drawing, so this method is also the place to execute "myViewer.printViewer()"
myViewer
The cviewer.py is also the place, where the Custom-Viewer is created. We simply declare a variable:
myViewer = CustomViewer(update)
It is important to give the viewer a function-pointer to the "update"-method that it will use. So simply give the name.
Tutorial: Creating a simple Area-Editor
After having read all this boring stuff, we want to do something productive. We will create an Area-Editor. This is an entry named "Area-Editor", having a child named "Add Area" and when you add an area, it will be visible as a child of the "Area-Editor". We won't just add those areas visually - we will add real areas, that you can use for other stuff later.
First of all you have to create a new file (python-module) in "cust_viewer" named "areaeditor.py".
Because we need to have access to our Custom-Viewer, we will create a viewer-variable and initialize it with None.
theViewer = None
We will begin by creating the "initializeAreaEditor"-Method. This one will create the first entries. When calling it, we will give it the Custom-Viewer, so that the method knows, to which viewer it should add the entries.
def initializeAreaEditor(aViewer): # set the viewer global theViewer theViewer = aViewer
# create the root-Entry eAreaEditor = Entry("Area-Editor", theViewer.m_rootEntry) # make it expandable eAreaEditor.makeExpandable(False) # False means not expanded in the beginning # create the add-area-entry eAddArea = Entry("Add Area", eAreaEditor) # call the "addArea" method when someone "uses" this entry (hitting space) entryAddAction(eAddArea, "use", Action(addArea))
I think, the comments should explain everything. "makeExpandable(isExpanded)" internally executes "entryAddAction(eAreaEditor, "use", Action(entryToggleExpand, eAreaEditor))". So, as you can see both entries have different "use"-actions (when the user hits space, the "use"-actions of the currently selected entry will be executed.). The root-entry has a use-action to expand and unexpand it and the "addArea"-Entry has an action to call the method "addArea".
Let's create the "addArea"-Method!
We will have a Dictionary for our added areas and we will save the number of areas, that we have:
# the areas that are added using the viewer (Dictionary<String, Area>) addedAreas = {} # the number of added areas numAddedAreas = 0
And here is the method.
def addArea(): """ Adds an ara """ global numAddedAreas global currentArea # the name has to be unique, so just add the num of areas areaName = "Area" + str(numAddedAreas) # create the area at the mouse-position pos = getMouseToScreen() # create the area area = Area(pos, 10) # add it to the Dictionary and increase the num of areas addedAreas[areaName] = area numAddedAreas = numAddedAreas + 1 # create an entry for the area and its actions entry = Entry(areaName, theViewer.m_rootEntry["Area-Editor"]) entry.makeExpandable(False)
As you see, we create a unique name for the entry/area that will be added. We will create the area at the current mouse-position. This is a method from the "misc"-module. If the mouse-position is invalid, than the area will be created at the camera-lookat. After that we add the area to our dictionary, so that we are able to access it from outside (f.ex. for creating units in this area or something). Last but not least: we create the entry for the area. Note that the parent is the "Area-Editor"-entry. We also add an action for expanding (which is still quite useless, because the entry has not childs).
Well ... we still missed some imports before that method, otherwise you will get errors:
from cust_viewer.misc import getMouseToScreen from cust_viewer.misc import drawCircle from cust_viewer.Entry import Entry from cust_viewer.Entry import entryAddAction from area import Area from reaction.action import Action import wicp import serverimports as si
When you look at the imports, you will see that we use "cust_viewer" right before the module we want to load. You can do this, because the folder "cust_viewer" is treated as a python-package.
Last but not least, we will write a method to print the areas in the world:
#the color areas should be drawn in COLOR_AREA = 0x00ff00 def printAreas(): """ Prints the Areas in the world. """ for ar in addedAreas: # get the screen-position of the name worldPosName = addedAreas[ar].myPosition screenPosName = wicp.World2Screen( worldPosName ) # if is visible, draw it if not screenPosName is None: si.ClientCommand( 'PrintText', ar, screenPosName[0], screenPosName[1], COLOR_AREA ) # draw a circle to visualize the area drawCircle(addedAreas[ar].myPos, addedAreas[ar].myRadius, COLOR_AREA)
For all areas in our dictionary, we get the position of that area, calculate the according screenposition, draw the name and draw a circle. The drawCircle-method is from the "misc"-module.
Now our areaeditor-module is ready. There are still two things that we have to do.
We have to initialize the AreaEditor in the cviewer's "initViewer()"-method. Therefore we have to import the necessary methods first:
from cust_viewer.areaeditor import initializeAreaEditor from cust_viewer.areaeditor import printAreas
Then add "initializeAreaEditor(myViewer)" to your cviewer's "initViewer()"-method.
Also, we want to print out the areas every frame. So you have to add "printAreas()" at the end of the cviewer's "update()"-method.
That's it! you successfully created a simple Area-Editor!!! Now you should know, how to work with the Custom-Viewer to make your modder-life easier.
Sharing Editors
Of course we encourage you to share your editors. Therefore make a page for your Editor and add this page to the Editor-List of the Custom Viewer
On your editor's page, add a download-link to your Python-Script and explain how the "cviewer.py" has to be changed, so that it uses your editor.
Look the AreaEditor's page for an example.