Jump to content

UI programming guideline


Recommended Posts

  • Premium

Hi there devs,

 

Its been a while since my last release and probably this will not change, but I had this guide in my head for like a year now. In my last ~2 years while I was working in the AE team various devs came and go, and this kind of guideline would have been a huge help and could prevent lots of errors.

 

I have been in the dev scene for more than 10 years now and (as some of you may already noticed) one of my main interest has always been making more and more user friendly and complex UI objects/interfaces and along the road gathered tons of experience. You won't end up with another shiny system that you can use in your server this time by reading this article, but instead maybe I can prevent you from making windows that stays in the memory forever, opens infinite times, prevents other window's destruction, and many more. Who knows, maybe you will leave with some brand new never ever seen tricks that you can use in your future UIs.

But first of all lets talk about some good2know stuff.
 

UI layers

Layers are root "windows", their purpose to hold all kind of interface windows while managing the z order of the windows (z order = depth level of the windows, which one is "above" the other). By default there are 5 layers (from bottom to top):

  1. "GAME": this is only used for the game window (game.py), and when you click on it the game will translate the click position to the map
  2. "UI_BOTTOM": this is used for shop titles
  3. "UI": this is the default layer, we would like to put most of our windows into this layer, like inventory, character window, messenger, pms, etc, so they can overlap each other
  4. "TOP_MOST": every window, that should always be in front of the player, so other windows would not overlap them from the lower layers, so for example the taskbar is placed in this layer
  5. "CURTAIN": this is for the curtain window, which is actually a black as a pit window, that is used to smooth the change from different introXX windows (like between login and charselect) This is the outest layer and goes before the other 4.

 

So when the render happens, the 1st layer and its childs will be rendered first, then the 2nd layer, then the 3rd, etc, so by this we will get our comfy usual client layout. In the other hand, when we click, everything goes in reverse order, first the client will try to "pick" from the curtain layer's child windows (also in reverse order), then the top_most layer, etc. Ofc there is no layers beyond the game layer, so usually the "pick" will succeed there, and we will end up clicking on the map.

 

UI windows

Now lets talk a little bit about the parts of an UI window. It has a python object part and a c++ object part. When you create a basic Window (from the ui.py) basically 2 things happen: a python Window object is created first, then through the wndMgr.Register call a c++ CWindow object is created. These 2 objects are linked, in python the handle for the CWindow is saved in the self.hWnd variable, while in the CWindow the python object handle would be stored in the m_poHandler.

 

What are these handles for? If you want to call a method on a window from python, you have to specify which window you want to perform that action on. Remember, the python part is just a "frontend" for the UI, the actual magic happens in the cpp part. Without python, its possible to create windows, but without the cpp part there is no windows at all. On the cpp part, we need the python object handle to perform callbacks, like notifying the python class when we press a key, or pressing our left mouse button.

By default the newly created window will take a seat in one of the layers (if provided through the register method, otherwise it goes to the UI layer). In a healthy client we only put useful windows directly into a layer. For example you want to put your brand new won exchange window into the UI layer, but you don't want to put all parts of a window into the UI layer (for example putting the base window into the root layer then putting the buttons on it to the root layer too, then somehow using global coordinates to make it work).

 

Instead, you want to "group" your objects, using the SetParent function. You may say that "yeah yeah who doesn't know this?", but do you know what actually happens in the background? The current window will be removed from its current parent's child list (which is almost never null, cus when you create it its in the UI layer, so the parent is the UI layer) and will be put into the end of the new parent window's child list.

Why is it important, that it will be put into the end? Because it will determine the Z order of the childs of the window. For example if I create 2 ImageBox with the same size of image on the same position and then I set ImageBox1's parent before ImageBox2's parent, then I will only see ImageBox2, because that will be rendered after ImageBox1, because its position in the childList is lower than ImageBox2's. For normal window elements (like buttons) its very important, because after you set the parent of a window, you can't alter the z order (or rather the position in the childList) unless you use the SetParent again. No, you can't use SetTop, because its only for windows with "float" flags on it, which you only want to use on the base of your window (the root window that you put your stuff on it and use it as parent for most of the time).

 

Window picking

"Picking" is performed when we move the cursor. This is an important method, because this will determine the result of various events, for example the OnMouseOverIn, OnMouseOverOut, OnMouseRightButtonDown, etc. To fully understand it, you must keep in mind that every window is a square.  Do you have a perfect circle image for a button? No you don't, its a square. Do you have the most abstract future window board design with full star wars going on the background? No, you DON'T. ITS A SQUARE. By default, a window is picked if:

  • the mouse is over the window AND
  • the window is visible (Shown) AND
  • the window doesn't have "not_pick" flag set AND
  • the window is "inside its parent's square" on the current mouse position, which means if the parent's position is 0,0 and it has a size of 10x10 and the window's position is 11, 0, the window is outside of its parent's square. This is really important to understand, lots of UI has fully bugged part because of ignoring this fact. I think every one of you already experienced it on some bad illumina design implementation, when you click on a picture, and your character will start to run around like a madman, because the game says that A-a-aaaaa! There is NO WINDOW ON THAT POSITION ;)

 

It is useful to use the not_pick flag whenever you can, for example on pure design elements, like lines and flowers and ofc the spaceships on the background. Lets say you have a size of 10x10 image that has a tooltip, and you have a window on it that has a size of 5x5. When the mouse is over the image, the tooltip will be shown, but if its over the 5x5 window, the tooltip won't appear, unless you set it to the 5x5 window too. But if you use the not_pick flag on the 5x5 window, the 5x5 window won't be picked and the tooltip would be shown even if the mouse is over the 5x5 window.

 

Window deletion, reference counting, garbage collector, proxying

The window deletion procedure starts on the python side, first the destructor of the python object will be called, then it will call the wndMgr.Destroy that deletes the c++ object. By default, we have to store our python object, or this time our python window, to make sour it doesn't vanish. Usually we do this via the interface module, like "self.wndRandomThing = myModule.RandomWindow()". But what is this? What is in the background?

Python objects has reference count. Let me present it with the following example:

a = ui.Window() # a new python object is created, and its reference count is 1
b = a # no new python object is created, but now b is refering to the same object as 'a', so that object has a refence count of 2
del b # b is no longer exists, so its no longer referencing to the newly created window object, so its reference count will be 1
del a # the newly created window object's reference count now 0, so it will be deleted, calling the __del__ method

To be more accurate, del-ing something doesn't mean that it will be deleted immediately. If the reference count hits 0 the garbage collector (btw there is garbage collector in python if you didn't know) will delete it, and that moment the __del__ will be called. It sounds very handy isn't it? Yeeeeah its easyyyy the coder don't have to manage object deletion its sooo simple.... Yeah... But lets do the following:

class stuff(object):
	def __del__(self):
		print "del"
	def doStuff(self):
		self.something = self.__del__ # here we could just simply do self.something = self too, doesnt matter

a = stuff()
a.doStuff() # and now you just cut your leg
del a #you are expecting the "del" print, but that will never happen

You may say " ? oh please who tf does something stupid like this? It SO OBVIOUS that its wrong whaaaaaaat????" But in reality, I could count on only one of my hand how many devs don't make this mistake. Even my codes from the past decade are bad according to this aspect, since I only realized this problem about a year ago, when I was working on the AE wiki. Even the yimir codes contain tons of this kind of errors, however there was definitely a smart man, who implemented the __mem_func__. Okay, I see that you still don't understand how is this possible to make this kind of mistake, so let me show you a real example:

class myBoard(ui.Board):
	def __init__(self):
		super(myBoard, self).__init__()
		self.makeItRain()
    
	def __del__(self):
		super(myBoard, self).__del__()
		print "I want to break free. I want to breeaaaaaak freeeeeeeeeeee"
    
	def doNothing(self):
		pass
  
	def makeItRain(self):
		self.btn = ui.Button()
		self.btn.SetParent(self)
		self.btn.SetEvent(self.doNothing) #boom

a = myBoard()
del a
# but where is the print?

Thats not that obvious now right? What happens here? We create a myBoard object, which in the __init__ calls to the makeItRain func, which stores an ui.Button object, and sets the button to call for the myBoard class's doNothing function with the self object pointer in the OnLeftMouseButtonDown function, which means that the reference count will never be zero, because the btn referenced in the myBoard but myBoard is referenced in the btn but the btn is referenced in the.... so you know, its a spiral of death, that our best friend garbage collector can't resolve.

 

Okay, but how to do it correctly? Lets talk about proxying. In python, proxies are weak references, which means that they don't increase reference count of an object, which is exactly what we need.

#let me show this time the console output too
class stuff(object):
	def __del__(self):
		print "del"

>>> from weakref import proxy
>>> a = stuff() #newly created object
>>> b = proxy(a) #create weak reference to the new object (note that the weak reference object is also an object that stores the weak reference)
>>> b #what is b?
<weakproxy at 02DB0F60 to stuff at 02DBFB50> # b is a weakproxy object that refers to a which is a "stuff object"
>>> del b # what if we delete b?
# no "del" message, the stuff object is not deleted, because its reference count is still 1, because its stored in a
>>> b = proxy(a) # lets recreate b
>>> del a # now we delete the only one reference of the stuff object
del # and here we go, we got the del message from __del__
>>> b # okay but whats up with b?
<weakproxy at 02DB0F60 to NoneType at 7C2DFB7C> # because b is a weakproxy object, it won't be deleted out of nowhere, but it refers to a NoneType right now, because the stuff object is deleted (also note here that NoneType is also a python object :D)
>>> if b: #what if I want to use b?
...     print "a"
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ReferenceError: weakly-referenced object no longer exists # in normal cases no need to trycatch this exception, because if we do everything right, we will never run into a deleted weakly-referenced object

But how do we proxy something like self.doNothing? Actually, what is self.doNothing? self.doNothing has 3 parts:

  1. The first part is not that obvious, it has a base class pointer, which is points to myBoard.
  2. It has a class object pointer, that is basically "self". It refers to the current instance of myBoard.
  3. It has a function object pointer, called doNothing, which is located inside the myBoard class.

 

And now we can understand ymir's ui.__mem_func__ class, which is exactly meant to be used for proxying class member functions:

# allow me to reverse the order of the definitions inside the __mem_func__ so it will be more understandable
class __mem_func__:
	def __init__(self, mfunc): #this happens when we write ui.__mem_func__(self.doSomething)
		if mfunc.im_func.func_code.co_argcount>1: # if the doSomething takes more than one argument (which is not the case right now, because it only needs the class obj pointer (which is the 'self' in the 'self.doSomething'))
			self.call=__mem_func__.__arg_call__(mfunc.im_class, mfunc.im_self, mfunc.im_func) #lets unfold the python object to the mentioned 3 parts
		else:
			self.call=__mem_func__.__noarg_call__(mfunc.im_class, mfunc.im_self, mfunc.im_func) #this will be called for the 'self.doSomething'

	def __call__(self, *arg): # this line runs whenever we call apply(storedfunc, args) or storedfunc()
		return self.call(*arg)
      
	
	class __noarg_call__: # this class is used whever the python object we want to proxy only takes one argument
		def __init__(self, cls, obj, func):
			self.cls=cls # this one I don't really get, we won't need the class object later also its not proxied, so its probably here to prevent delete the base class in rare cases
			self.obj=proxy(obj) # here we proxy the object pointer which is what really matters
			self.func=proxy(func) # and then we proxy the class func pointer

		def __call__(self, *arg):
			return self.func(self.obj) # here we just simply call the class function with the object pointer

	class __arg_call__:
		def __init__(self, cls, obj, func):
			self.cls=cls
			self.obj=proxy(obj)
			self.func=proxy(func)

		def __call__(self, *arg):
			return self.func(self.obj, *arg) # here we just simply call the class function with the object pointer and additional arguments

Pros, cons, when to, when not to, why?

Now you may ask that "Okay okay that sounds good, but why? What can this cause, how big the impact could be, is it worth to even bother with it?" First of all, you must understand that this is kinda equal to a memory leak. I would actually call these non-deleted windows as "leaking windows". Every time you warp, new and new instances will be generated from those objects, and ofc, one instance of a window means like about +50/100 instances, depending on the window complexity, how much childs it has.

 

Usually these windows are at least remain in a Hide state, and hidden windows don't affect update and render time, but they will remain in the root layer, and may stack up depending on how much time the player warp. Still, the number of leaking root windows / warp is around 50-100, which is really not much for a linked list (the type of the childList). However, the memory consumption is much greater. One year ago AE had 10k leaking windows growth / warp. One base class (CWindow)'s size is 140 bytes, which means the memory growth is at least 1,3MB/warp and it does not contain the size of the python objects size for those windows, the linked_list headers and other necessary stuffs. After some hour of playing this can easily reach 100+MB of leaking memory.

 

On worse cases the old windows are not even hidden, and on rewarp players may see multiple instances of the mentioned windows, like double inventory, double messenger window, etc. In this cases those windows can affect the render and update times too.

 

Pros:

  • your code will now work correctly regarding to this topic
  • you may gain some performance boost
  • you may find stuff in your client that even you don't know about
  • you may find enough kebab for a whole week
  • you can save kittens by removing leaking windows and proxying objects
  • you can build my respect towards you and your code and you can even calculate the actual number using the following formula: totalRespect = proxysUsed * 2 + ui.__mem_func__sUsed - proxysMisused * 3.511769 - ui.__mem_func__sMisused * pi - 666 * ui.__mem_func__sNotUsed

Cons:

  • depending on how bad the situation is and how skilled you are, the time needed to find and fix everything could be vary, from few hours to several days
  • if you are satisfied with your client as it is right now there is not that huge benefit regarding how much time it could take to fix all the windows

 

Detecting leaking windows

Spoiler

 

So now that you decided to take care of these nasty windows, you have to keep in mind that this won't be easy. It will include lots of checking, rechecking, rerechecking, rererechecking, swearing, thinking, and by the end of the day you will know all the name of those developers you will never ever want to hear about again.

 

So the basic concept is to count the currently __init__ed and __del__ed window objects. Since every window is derived from the ui.Window class it will automatically apply to all other derived classes. If you just want to check a single window, like the one you are currently developing, its very easy, just put a dbg.LogBox into the __del__ method, and if it appears during teleports "hoooraaay", your window is not leaking. But checking the whole interface it much harder. I wrote a little tool for it, but it only gives you small pinpoints, like xynamed class is not deleted properly, also it gives an exact number of how much window remained undeleted after teleporting. First lets implement it and in the meantime I will introduce its mechanism.



# put this somewhere in the constInfo.py
DETECT_LEAKING_WINDOWS = True # turne this to false to disable leaking windows checking
if DETECT_LEAKING_WINDOWS:
	WINDOW_COUNT_OBJ = False # we only want to check leaking while we are in the game phase
	WINDOW_OBJ_COUNT = 0 # number of leaking window (only counting this if window_count_obj is true)
	WINDOW_OBJ_LIST = {} # here we store the init-ed but not del-ed (so currently allocated) windows
	WINDOW_OBJ_TRACE = [] # we store the curent stackstrace here
	WINDOW_TOTAL_OBJ_COUNT = 0 # number of total allocated windows
 


# in networkmodule.py import constInfo and in def __ChangePhaseWindow(self): replace this:
if oldPhaseWindow:
	oldPhaseWindow.Close()
    
#with this: 
		if constInfo.DETECT_LEAKING_WINDOWS:
			#dbg.LogBox("total window obj count: "+ str(constInfo.WINDOW_TOTAL_OBJ_COUNT))
			import game, gc, os
			if isinstance(newPhaseWindow, game.GameWindow): #going from something else (introloading) to gameWindow
				constInfo.WINDOW_COUNT_OBJ = False # stop object counting while we removing the old phase window
				if oldPhaseWindow:
					oldPhaseWindow.Close() 
				del oldPhaseWindow # try to remove the old phasewindow
				gc.collect() # force garbage collector to remove 0 referenced object
				constInfo.WINDOW_COUNT_OBJ = True # start counting window objects

			elif isinstance(oldPhaseWindow, game.GameWindow): # from gamewindow to something else (intrologin, introselect, introloading)
				constInfo.WINDOW_COUNT_OBJ = True # start counting window objects
				if oldPhaseWindow:
					oldPhaseWindow.Close()
				del oldPhaseWindow # try to remove old gamewindow
				gc.collect() # force garbage collector to collect 0 referenced objects
				constInfo.WINDOW_COUNT_OBJ = False # stop counting window objects
				if constInfo.WINDOW_OBJ_COUNT > 3: # there are static classes, whose are allocated only once on the first login, like the candidatewindow
					# file saving stuff
					dbg.LogBox("!ATTENTION! WINDOW_MEMORY_LEAK DETECTED\n LEAKING WINDOW COUNT: "+ str(constInfo.WINDOW_OBJ_COUNT))
					if not os.path.isdir("memory_leak"):
						os.mkdir("memory_leak")
					leakReport = 0
					while os.path.isfile("memory_leak/window_memory_leak%i.txt" % leakReport):
						leakReport += 1
					opFile = open("memory_leak/window_memory_leak%i.txt"%leakReport, "w+")
					opRootFile = open("memory_leak/window_memory_leak_root%i.txt"%leakReport, "w+")
					for i, v in constInfo.WINDOW_OBJ_LIST.iteritems():
						opFile.write(v.typeStr + " parent type: " + v.strParent + "\n")
						for j in v.traceBack:
							opFile.write("\t" + j + "\n")
						if v.strParent == "":
							opRootFile.write(v.typeStr + "\n")
					opRootFile.flush()
					opRootFile.close()
					opFile.flush()
					opFile.close()

			else:
				if oldPhaseWindow:
					oldPhaseWindow.Close()

		else:
			if oldPhaseWindow:
				oldPhaseWindow.Close()
                
##############################################################################################

#still in the networkmodule, all under all SetXXXPhase function except SetGamePhase add this: 
		if constInfo.DETECT_LEAKING_WINDOWS:
			constInfo.WINDOW_COUNT_OBJ = False
#before the self.SetPhaseWindow(XXX)

#for SetGamePhase add this: 
			if constInfo.DETECT_LEAKING_WINDOWS: # reset leaking window list since the last teleport
				constInfo.WINDOW_COUNT_OBJ = True
				constInfo.WINDOW_OBJ_COUNT = 0
				constInfo.WINDOW_OBJ_LIST = {}

#before self.SetPhaseWindow(game.GameWindow(self))

If you want to check something else than the gamephase, just replace the part before setphasewindow(game.GameWindow) with the phase window that you want to use this check on's WINDOW_COUNT_OBJ = False. Also replace both game.GameWindow in the __changephasewindow method.



# in ui.py add this before __mem_func__ class:
if constInfo.DETECT_LEAKING_WINDOWS:
	import weakref
	import sys
	def trace_calls_and_returns(frame, event, arg): #as the name (somewhat) implies build trace of calls and remove trace on returns
		co = frame.f_code
		func_name = co.co_name
		line_no = frame.f_lineno
		filename = co.co_filename
		if event == 'call' or event == 'c_call':
			constInfo.WINDOW_OBJ_TRACE.append('Call to %s on line %s of %s' % (func_name, line_no, filename))
			return trace_calls_and_returns
		elif (event == 'return' or event == 'c_return') and len(constInfo.WINDOW_OBJ_TRACE):
			constInfo.WINDOW_OBJ_TRACE.pop(-1)
		return

	sys.settrace(trace_calls_and_returns)

	class ExtendedRef(weakref.ref): # extended weakref object to store the backtrace, type of actual object and the parent name, if any
		def __init__(self, ob, callback=None):
			super(ExtendedRef, self).__init__(ob, callback)
			self.typeStr = str(ob)
			self.strParent = ""
			self.traceBack = constInfo.WINDOW_OBJ_TRACE[:] # just deepcopy the current trace

###############################################################################################

#still in ui.py in the Window class, add the following to the beginning of def __init__:

		if constInfo.DETECT_LEAKING_WINDOWS:
			constInfo.WINDOW_TOTAL_OBJ_COUNT += 1 # increase total obj count
			if constInfo.WINDOW_COUNT_OBJ: # if we are counting window objects increase the normal obj count and save the traceback to know where the window creation have been called from
				constInfo.WINDOW_OBJ_COUNT += 1
				constInfo.WINDOW_OBJ_LIST[id(self)] = ExtendedRef(self) # save trace and other data

# then add the following to the beginning of def __del__:

		if constInfo.DETECT_LEAKING_WINDOWS: #looks like this window is not leaking cus __del__ has been called, remove it from the active window list
			constInfo.WINDOW_TOTAL_OBJ_COUNT -= 1
			if constInfo.WINDOW_COUNT_OBJ and id(self) in constInfo.WINDOW_OBJ_LIST:
				constInfo.WINDOW_OBJ_COUNT -= 1
				constInfo.WINDOW_OBJ_LIST.pop(id(self))

# now lets save the parent window name, so (still in the Window class) add the following after ALL!! wndMgr.SetParent(self.hWnd, parent.hWnd)

				if constInfo.DETECT_LEAKING_WINDOWS: # find our window in the saved obj list and save its parent address
					if constInfo.WINDOW_COUNT_OBJ and id(self) in constInfo.WINDOW_OBJ_LIST:
						constInfo.WINDOW_OBJ_LIST[id(self)].strParent = str(parent)

And thats it! The tool is up and running. You must disable cython to fully utilize it. But how does it work? If you didn't change anything in the code, you just have to log into a char, then warp somewhere. After the warp you will receive a dbg.LogBox if you have leaking windows, and the tool will generate a window_memory_leak_rootXX.txt and a window_memory_leakXX.txt into the memory_leak folder that you can find in your client's root dir. IMPORTANT: Some windows only created after attempting to open it for the first time! This means that to make sure a window is not leaking you must open it at least once.

Now lets talk about analysing reports. So basically you will have two txts. The _rootXX.txt will contain windows placed directly into layers, so windows like taskbar, GameWindow, inventoryWindow, etc will be placed here. In the _leakXX.txt you will find the call stack for all leaking window, including non-root windows, so here you can mostly find various classes from ui.py. Again, remember that if a window is leaking, all of its childs, childs of childs, childs of childs of childs, etc... will be leaking. You can find the leaking root window's childs by searching for its address from the _rootXX.txt. Sometimes you will find non-root leaking windows whose parents are not leaking. This means that "element classes" in ui.py like BoardWithTitleBar can be leaking too. For example in BoardWithTitleBar if you don't use the SetCloseEvent, it will be leaking by default, because of the self.SetCloseEvent(self.Hide) line.


A walkthrough

Fortunately I have a client with full of leaking windows (wasn't hard to find one :D ) so I will demonstrate a repair of a basic window. Its much easier to repair leaking root windows, and I recommend you to start with these, so after fixing them you will have way less results in the _leakXX.txt and easier to work with them after. So I have these bad boys:



<ui.EmptyCandidateWindow object at 0x05D8BD50>
<ui.EmptyCandidateWindow object at 0x061D3030>
<ui.EmptyCandidateWindow object at 0x06602370>
<uiToolTip.ToolTip object at 0x066B7BF0>
<uiToolTip.ItemToolTip object at 0x05D90750>
<uiCube.CubeWindow object at 0x39DC3F70>
<uiPrivateShopBuilder.PrivateShopBuilder object at 0x066B76B0>
<uiRules.Rules object at 0x05E2C550>
<uiExchange.ExchangeDialog object at 0x063CBDD0>
<mb_ingameWiki.InGameWiki object at 0x05D90A90>     ->>>>>>>>>>>>>>>>>>> okay I will never trust again masodikbela, his 4 years old wiki is full of leaks :@@@@@@@ -1 dev
<switchbot.CheckBox object at 0x05E2D3B0>
<uiDragonSoul.DragonSoulWindow object at 0x05D8AEF0>
<ui.ReadingWnd object at 0x05E2C910>
<switchbot.Bot object at 0x061BCD10>
<uipetsystem.TextToolTip object at 0x39EFCAF0>
<ui.EmptyCandidateWindow object at 0x061D3CB0>
<ui.EmptyCandidateWindow object at 0x061D3A90>
<ui.EmptyCandidateWindow object at 0x062D09F0>
<ui.EmptyCandidateWindow object at 0x39EFD270>
<ui.EmptyCandidateWindow object at 0x063CB630>
<uiRefine.RefineDialogNew object at 0x066B76D0>
<ui.EmptyCandidateWindow object at 0x066B7290>
<uiCommon.PopupDialog object at 0x062C88B0>
<ui.EmptyCandidateWindow object at 0x06602B70>
<ui.EmptyCandidateWindow object at 0x05D8BC50>
<uiInventory.BonusPageWindow object at 0x05D90A30>
<ui.EmptyCandidateWindow object at 0x061D31F0>
<ui.Bar object at 0x39EFF130>
<ui.EmptyCandidateWindow object at 0x062D0C50>
<uiToolTip.ToolTip object at 0x05D8DE10>
<ui.ImageBox object at 0x05E2D230>
<uisash.AbsorbWindow object at 0x066D9CD0>
<uiInventory.PotionWindow object at 0x05E2CD90>
<uiCommon.QuestionDialog2 object at 0x062C8810>
<uiAttachMetin.AttachMetinDialog object at 0x05D8BAF0>
<uipetsystem.TextToolTip object at 0x39EFCB30>
<ui.Bar object at 0x06330D10>
<switchbot.CheckBox object at 0x05E2D150>
<ui.EmptyCandidateWindow object at 0x061D3690>
<uipetfeed.PetFeedWindow object at 0x39EFC2B0>
<ui.ImageBox object at 0x05E2D2F0>
<uiShop.ShopDialog object at 0x063E5450>
<uisash.CombineWindow object at 0x066D9BD0>
<ui.EmptyCandidateWindow object at 0x05E2CA70>
<ui.EmptyCandidateWindow object at 0x05D8F030>
<ui.ReadingWnd object at 0x05D8F050>
<ui.EmptyCandidateWindow object at 0x063CB170>
<ui.EmptyCandidateWindow object at 0x062A5990>
<switchbot.CheckBox object at 0x05E2D310>
<ui.ImageBox object at 0x05E2D390>
<ui.EmptyCandidateWindow object at 0x061D3E30>
<ui.ImageBox object at 0x05E2D410>
<uiDragonSoul.DragonSoulRefineWindow object at 0x05D90B10>
<uiToolTip.ItemToolTip object at 0x05E2CFF0>
<ui.ReadingWnd object at 0x062A59B0>
<ui.ImageBox object at 0x05E2D110>
<ui.EmptyCandidateWindow object at 0x06464AD0>
<uiInventory.InventoryWindow object at 0x05D8AE90>
<ui.EmptyCandidateWindow object at 0x062D0910>
<ui.EmptyCandidateWindow object at 0x05D8B870>
<uipetsystem.TextToolTip object at 0x39EFCB90>
<uiToolTip.ItemToolTip object at 0x066B7430>
<ui.EmptyCandidateWindow object at 0x061D39D0>
<ui.EmptyCandidateWindow object at 0x063CB0B0>
<uichangelook.Window object at 0x39DC3EB0>
<uiCommon.PopupDialog object at 0x062A5F30>
<uipetsystem.TextToolTip object at 0x39EFCCB0>
<ui.EmptyCandidateWindow object at 0x061D1F30>
<uiQuest.QuestCurtain object at 0x39EFF0F0>
<ui.EmptyCandidateWindow object at 0x06624970>
<switchbot.CheckBox object at 0x05E2D250>
<uiCharacter.CharacterWindow object at 0x061D14F0>
<ui.EmptyCandidateWindow object at 0x061D3D70>
<uiToolTip.ToolTip object at 0x39EFC330>
<switchbot.CheckBox object at 0x06330E10>
<ui.Bar object at 0x39EFF150>
<ui.EmptyCandidateWindow object at 0x06624A50>
<ui.EmptyCandidateWindow object at 0x061D3830>
<uipetsystem.PetSystemMain object at 0x39E78350>
<uiToolTip.ToolTip object at 0x05D8DC90>
<uiToolTip.ToolTip object at 0x05D8DFD0>

I will chose uiCube, because it contains a not-that-obvious unproxied part, and only that kind of "leak" is not already present in this article. After highlighting all "self." text and walking through the whole file, we will notice some odd lambda expressions:



			row = 0
			for materialRow in self.materialSlots:
				j = 0
				for material in materialRow:
					material.SetOverInItemEvent(lambda trash = 0, rowIndex = row,  col = j: self.__OverInMaterialSlot(trash, rowIndex, col))
					material.SetSelectItemSlotEvent(lambda trash = 0, rowIndex = row,  col = j: self.__OnSelectMaterialSlot(trash, rowIndex, col))
					material.SetOverOutItemEvent(lambda : self.__OverOutMaterialSlot())
					j = j + 1
				row = row + 1

			row = 0
			for resultSlot in self.resultSlots:
				resultSlot.SetOverInItemEvent(lambda trash = 0, rowIndex = row: self.__OverInCubeResultSlot(trash, rowIndex))
				resultSlot.SetOverOutItemEvent(lambda : self.__OverOutMaterialSlot())
				row = row + 1

This may looks okay, but in reality they are just as bad as an un-ui.__mem_func__-ed class member function. What lambda functions do for us is kinda the same like writing a custom function every time we want to call it with a new combination of arguments. Be aware that doing ui.__mem_func__ inside the lambda is also useless, because that code part will not be called before the actual event happens for example in the material's OnMouseOverIn call, so it will still produce leaking windows. Instead of this what I did was adding *args to the SetOverInItemEvent and unfolded the lambda expressions to this:



			row = 0
			for materialRow in self.materialSlots:
				j = 0
				for material in materialRow:
					material.SetOverInItemEvent(ui.__mem_func__(self.__OverInMaterialSlot), 0, row, j)
					material.SetSelectItemSlotEvent(ui.__mem_func__(self.__OnSelectMaterialSlot), 0, row, j)
					material.SetOverOutItemEvent(ui.__mem_func__(self.__OverOutMaterialSlot))
					j = j + 1
				row = row + 1

			row = 0
			for resultSlot in self.resultSlots:
				resultSlot.SetOverInItemEvent(ui.__mem_func__(self.__OverInCubeResultSlot), 0, row)
				resultSlot.SetOverOutItemEvent(ui.__mem_func__(self.__OverOutMaterialSlot))
				row = row + 1

And thats it! The uiCube is no longer leaking. Only 351236235234246345764567e+45 other classes left! Good luck with them :P

 

 

DON'T BLOCK THE BACKEND!!

At first glance python code looks like you are invincible, you can do whatever you want, you are not responsible for the performance because python has a bottomless bucket full of update/render time and if the game freeze sometimes its definitely not your fault, the game is just BAD. Lets talk about OnUpdate and OnRender, whats the difference, when they are called, what to and what not to put in there.

 

So as their name implies, they are called from the binary every time it performs an Update or Render action. In Update, the binary performs non-rendering actions, like updating the positions of walking characters, updating window positions, and every kind of calculation that is necessary for a render action, to make every kind of data up to date, reducing the cpu operations required for render as much as possible while keeping track of the time.

 

In Render, the binary performs non-updating actions, calling only directx device rendering methods that builds up a picture of the current world, including the UI, using the current, up to date positions.
 

Spoiler

 

The basic concept is that the number of updates must be static in time, and the number of renders can be vary depending on the computer's spec. It means that if your pc can only procedure 1 fps (one call to render / sec (and lets leave vsync out of this for the shake of this example)) for 5 seconds, the movement speed of the characters wont slow down and they will be exactly there where they should be after 5 seconds. To ensure that the speed of time doesn't change, the number of updates must be fixed. For example in our beloved game its 60 / sec.

 

But what if our pc is just simply can't follow this forced schedule? Of course its possible that update fps drops as well as render fps. Don't worry! If this happens, we will just produce more in the next second! The number of updates must be static in time, but we didn't say under how much time. It can be 1 sec, 10 sec, 100 sec, 1 hour, doesn't matter. If we can produce 120 update under 2 sec the theory is correct in practice too. If we can't produce 120 under 2 secs, lets try to do 600 under 10 secs.

 

You know its like when you are preparing for an exam, and 2 weeks before the exam you calculate that if you learn one thesis per day, you will end up knowing all of them by the day of the exam. And then life comes, you miss one day and you say that okay okay I will do 2 next day. And then life comes again and again, and you will end up learning only half of your thesises by the day of the exam and you will just try to yolo it. This can happen with our programs too! But instead of yoloing it, our program will just enter an unrecoverable state (cycle of death), which we experience as black screen that we can only interrupt by closing our client in taskmgr.

I really like to talk about this topic too, since my other favourite field is optimisations, but that is too far from our topic already, so if you are interested in this "time step" topic, I can recommend you this article: https://gafferongames.com/post/fix_your_timestep/ (by reading it you will notice how bad is the timestep in our game and you may end up fixing the cycle of death problem which even the great myself didn't have the chance to work on so far)

 

 

If you try to count the number of stars in our galaxy to accurately simulate your star wars going on in the background of your inventory window, no matter where you do it, (render or update) you will start hurting the game. For example if you have an ui with tons of elements, generating all the elements under one tick can cause HUGE client lag. In my AE wiki, I only load one or two entity for the current page under an update tick, so it will still load quickly, but won't block the game.

 

Notice that doing something like this is an UPDATE operation. Lets be nice and don't interrupt our rendering whit this kind of stuff. You should only use calls to render functions inside the OnRender, for example like in the ui.ListBox class, where we actually ask the binary to render a bar into our screen.

 

Around 20% of time spent in an Update tick was consumed by the UI update (calling to a python object through the api is kinda slow by default) in the AE client one year ago. Removing the rendering and updating of the gui for testing purposes actually gave a huge boost to the client, so who knows, maybe one day someone will make a great client that runs smooth on low end pcs too.

 

Answer the api calls if expected!

Some calls from the binary like OnPressEscapeKey expects a return value. Of course if no return value is provided the binary won't crash, but can lead to weird problems and malfunctions. For example, the OnPressEscapeKey expects non-false (non-null) if the call was successful on the window, so it will stop the iteration from calling to lower level windows. So if you expect from the game to close only one window per esc push, you have to return True (or at least not False or nothing).

 

There was a crash problem related to this in one of my friend's client recently. In his ui.EditLine class the OnPressEscapeKey looked something like this:

	def OnPressEscapeKey(self):
		if self.eventEscape():
			return True
		return False

In the original version it just return True unrelated to the eventEscape's return value. This looks fine at first glance, but if for some reason the self.eventEscape doesn't return anything and if (like the garbage collector decides to run or its disabled) the layer's or the parent's child lists changes in the binary because one or more windows are destroyed under the OnPressEscapeKey procedure and the iteration trough the child list is not interrupted, the binary will start to call invalid addresses thinking that they are existing windows, calling OnPressEscapeKey on them, resulting hardly backtraceable crashes.

 

Closing words

I can't highlight out enough times how much I like and enjoy creating new kind of UI stuff, (like animating windows that you may saw on my profile recently (click for the gif)) and because of this if you want to discuss a theory about new UI stuffs or mechanics of already existing UI stuffs feel free to do it in the comment section instead of writing me a pm to help others.

Also this time (unlike for my other topics) I would like to keep this guideline up to date and maybe adding new paragraphs if I find out another common mistake/misunderstanding.
 

Spoiler

 

Unrelated to the topic: And last but not least I'd like to ask everyone who are thinking about "hiring" me or asking me to do stuff for them to save your time. For me its not about money, its about knowledge and fun (which is usually generated through gathering knowledge and making new stuffs). Working on a (usually) bad client full of kebab is neither of those but at least not convenient at all. If I need your money you will know about it. I have my own projects. The reason behind me not creating new topics that frequently is that I don't think that releasing ready to use systems moves this scene forward, instead in 99% of the time it just fills the pocket of those that doesn't even care about the stuff or about improving, just interested about money.

 

Some says that knowledge is power and power equals money, but if you can't use knowledge you are just where the shore breaks. So my commission to this community in the future will always remains in the form of knowledge and experience that somehow benefits (or at least not impair) both you and me.
 

 

DEATH TO ALL LEAKING WINDOW!!!!!4444four

Edited by Metin2 Dev
Core X - External 2 Internal
  • Metin2 Dev 6
  • Confused 1
  • Good 2
  • Love 78

The one and only UI programming guideline

Link to comment
Share on other sites

  • Premium
14 minutes ago, Johnny69 said:

Very usefull if you don't want memory leak (at least from python side).

Also you can mention that objects created with no parent need to be deleted manually. (I saw some people creating buttons and stuff directly from game.py and never setting a parent or deleting them).

Good job. ?

This is not a problem, because if GameWindow is not leaking (and also the button class itself is not leaking) the button will be deleted after the deletion of GameWindow, so there is no need to do xy = None or del xy in the __del__ method. Its generally not necessary at all, the reference number won't increase by setting parent of a window, it increases when you save them into a variable of a class or into a global var.

Here is an example from the python command line:

>>> class stuff1(object):
...     def __del__(self):
...             print "del1"
...
>>> class stuff2(object):
...     def __del__(self):
...             print "del2"
...     def __init__(self):
...             self.something = stuff1()
...
>>> a = stuff2()
>>> del a
del2
del1
>>>

 

Edited by masodikbela
added example

The one and only UI programming guideline

Link to comment
Share on other sites

  • Bronze
31 minutes ago, masodikbela said:

This is not a problem, because if GameWindow is not leaking (and also the button class itself is not leaking) the button will be deleted after the deletion of GameWindow, so there is no need to do xy = None or del xy in the __del__ method. Its generally not necessary at all, the reference number won't increase by setting parent of a window, it increases when you save them into a variable of a class or into a global var.

Here is an example from the python command line:

>>> class stuff1(object): ... def __del__(self): ... print "del1" ... >>> class stuff2(object): ... def __del__(self): ... print "del2" ... def __init__(self): ... self.something = stuff1() ... >>> a = stuff2() >>> del a del2 del1 >>>


>>> class stuff1(object):
...     def __del__(self):
...             print "del1"
...
>>> class stuff2(object):
...     def __del__(self):
...             print "del2"
...     def __init__(self):
...             self.something = stuff1()
...
>>> a = stuff2()
>>> del a
del2
del1
>>>

 

 

Yeah, you are right. I saw that shit that I said with the buttons at some server and I thought they are created in game.py.

It was something like: "import test" in game.py

and test.py with this content like this:

spacer.png

 

Edited by Metin2 Dev
Core X - External 2 Internal
Link to comment
Share on other sites

  • Premium
1 minute ago, Johnny69 said:

 

Yeah, you are right. I saw that shit that I said with the buttons at some server and I thought they are created in game.py.

It was something like: "import test" in game.py

and test.py with this content like this:

spacer.png

 

Thats not a problem either, its called static object. It won't be created more than once (or more times than it gets deleted). There are 3 windows by default that are static as I mentioned already in the guide. So in the terms of "leaking" windows its not leaking, but of course it doesn't mean that using this will produce the expected result. After the first warp the number of leaking windows will contain the static windows, so to get the correct result you have to teleport 2 times, and the 2nd result will be the accurate one.

Edited by Metin2 Dev
Core X - External 2 Internal

The one and only UI programming guideline

Link to comment
Share on other sites

  • Honorable Member

Thank you,

For Source I created a little counter:
common/ClassCounter.h:

Spoiler

#pragma once

template <typename T> class ClassCounter
{
public:
	ClassCounter() { counter++; }
	virtual ~ClassCounter() { counter--; }
	static int GetCount() { return counter; }
private:
	static int counter;
};
template <typename T> int ClassCounter <T>::counter = 0;

 

 

Example Class usage:
 

Spoiler

class ExampleClassName : public ClassCounter<ExampleClassName>
{
public:
	ExampleClassName() {}
	~ExampleClassName() {}
};

 

Usage:

Spoiler

	std::cout << ClassCounter<ExampleClassName>::GetCount() << std::endl; // 0
	auto a = new ExampleClassName[10];

	std::cout << ClassCounter<ExampleClassName>::GetCount() << std::endl; // 10
	delete[] a;

	std::cout << ClassCounter<ExampleClassName>::GetCount() << std::endl; // 0

 

 

 

Link to comment
Share on other sites

  • 2 weeks later...
  • Bronze

On a side note

You actually throw away pythons garbage collector by even having destructors in most cases

 

 

bool PyTuple_GetWindow(PyObject* poArgs, int pos, UI::CWindow** ppRetWindow) {
	PyObject* iHandle;
	if (!PyTuple_GetObject(poArgs, pos, &iHandle))
		return false;

	if (!iHandle)
		return false;

	if (!PyCapsule_CheckExact(iHandle))
		return false;

	if (auto* ptr = PyCapsule_GetPointer(iHandle, nullptr); ptr) {
		*ppRetWindow = static_cast<UI::CWindow*>(ptr);

		return true;
	}

	return false;
}
          
// Usage
auto win     = UI::CWindowManager::Instance().RegisterXYWindow(po, szLayer);
auto capsule = PyCapsule_New(win, nullptr, CapsuleDestroyer);

return capsule;
          

void CapsuleDestroyer(PyObject* capsule) {
	auto rawPtr = static_cast<UI::CWindow*>(PyCapsule_GetPointer(capsule, nullptr));
	UI::CWindowManager::instance().DestroyWindow(rawPtr);
}
          

 

Edited by Alpha
  • Love 4
Link to comment
Share on other sites

  • 2 months later...
Quote

	row = 0
			for materialRow in self.materialSlots:
				j = 0
				for material in materialRow:
					material.SetOverInItemEvent(lambda trash = 0, rowIndex = row,  col = j: self.__OverInMaterialSlot(trash, rowIndex, col))
					material.SetSelectItemSlotEvent(lambda trash = 0, rowIndex = row,  col = j: self.__OnSelectMaterialSlot(trash, rowIndex, col))
					material.SetOverOutItemEvent(lambda : self.__OverOutMaterialSlot())
					j = j + 1
				row = row + 1

			row = 0
			for resultSlot in self.resultSlots:
				resultSlot.SetOverInItemEvent(lambda trash = 0, rowIndex = row: self.__OverInCubeResultSlot(trash, rowIndex))
				resultSlot.SetOverOutItemEvent(lambda : self.__OverOutMaterialSlot())
				row = row + 1

Is It possible to use a weakref to self not to unroll lambda expressions?
for example like this

from _weakref import ref
for materialRow in self.materialSlots:
				j = 0
				for material in materialRow:
					material.SetOverInItemEvent(lambda  i= ref(self), trash = 0, rowIndex = row,  col = j: i.__OverInMaterialSlot(trash, rowIndex, col))
					

Or would the garbage collector still find +1 references? 

Link to comment
Share on other sites

  • Premium
3 hours ago, OtherChoice said:

Is It possible to use a weakref to self not to unroll lambda expressions?
for example like this


from _weakref import ref
for materialRow in self.materialSlots:
				j = 0
				for material in materialRow:
					material.SetOverInItemEvent(lambda  i= ref(self), trash = 0, rowIndex = row,  col = j: i.__OverInMaterialSlot(trash, rowIndex, col))
					

Or would the garbage collector still find +1 references? 

I've tried various ways to keep the lambdas somehow, but always ended up having extra references to self because of it. Never really liked lambdas in python anyway so I gave up on making them work easily so didn't dig much deeper than that.
 

But of course this doesn't mean that its not possible to keep them so the best way to do some tests, try out some ideas and come to a conclusion.

  • Love 1

The one and only UI programming guideline

Link to comment
Share on other sites

Is possible to reload interface with a single button.

 

  

import sys
def ReloadModule(moduleName):
	if moduleName in sys.modules:
		del sys.modules[moduleName]
		del globals()[moduleName]
		retModule = __import__(moduleName)
		globals()[moduleName] = retModule
	def ReloadInterface(self):
		if self.wndGemWindow:
			self.wndGemWindow.Hide()
		del self.wndGemWindow	
		ReloadModule("uiGemShop")

		self.wndGemWindow = uiGemShop.GemShopSystem()
		self.wndGemWindow.Open()

 

Link to comment
Share on other sites

  • 7 months later...
0209 02:06:14416 ::   File "networkModule.py", line 239, in SetGamePhase

0209 02:06:14417 ::   File "game.py", line 102, in __init__

0209 02:06:14417 ::   File "interfaceModule.py", line 559, in MakeInterface

0209 02:06:14417 ::   File "interfaceModule.py", line 491, in __MakeDialogs

0209 02:06:14417 ::   File "uiRefine.py", line 468, in SetInven

0209 02:06:14417 :: TypeError
0209 02:06:14417 :: : 
0209 02:06:14417 :: cannot create weak reference to 'weakproxy' object
0209 02:06:14417 :: 

 

did u know how to fix this error

Lets Go Yes GIF by The Lonely Island

Link to comment
Share on other sites

  • 3 months later...
  • 7 months later...
On 4/18/2020 at 12:53 AM, masodikbela said:

Instead of this what I did was adding *args to the SetOverInItemEvent and unfolded the lambda expressions to this:

			row = 0
			for materialRow in self.materialSlots:
				j = 0
				for material in materialRow:
					material.SetOverInItemEvent(ui.__mem_func__(self.__OverInMaterialSlot), 0, row, j)
					material.SetSelectItemSlotEvent(ui.__mem_func__(self.__OnSelectMaterialSlot), 0, row, j)
					material.SetOverOutItemEvent(ui.__mem_func__(self.__OverOutMaterialSlot))
					j = j + 1
				row = row + 1

			row = 0
			for resultSlot in self.resultSlots:
				resultSlot.SetOverInItemEvent(ui.__mem_func__(self.__OverInCubeResultSlot), 0, row)
				resultSlot.SetOverOutItemEvent(ui.__mem_func__(self.__OverOutMaterialSlot))
				row = row + 1

 

Apologies if it's a stupid question, python is not my thing. But when I refactor it to *args, how do I then pass those arguments to the function? 0, row, j  and captured by *args, but then what? should I just save them to a variable in SetOverInItemEvent and unpack them when the event is triggered? Or is there an easier way of doing it?

Link to comment
Share on other sites

  • Premium
23 hours ago, 3bd0 said:

Apologies if it's a stupid question, python is not my thing. But when I refactor it to *args, how do I then pass those arguments to the function? 0, row, j  and captured by *args, but then what? should I just save them to a variable in SetOverInItemEvent and unpack them when the event is triggered? Or is there an easier way of doing it?

https://book.pythontips.com/en/latest/args_and_kwargs.html Its explained really well here, basically its when you want to pass different amount of variables you can just use *args as last argument in the function definition catching any remaining variables. It will be a tuple, either with 0 or more than 0 elements.

The one and only UI programming guideline

Link to comment
Share on other sites

32 minutes ago, masodikbela said:

https://book.pythontips.com/en/latest/args_and_kwargs.html Its explained really well here, basically its when you want to pass different amount of variables you can just use *args as last argument in the function definition catching any remaining variables. It will be a tuple, either with 0 or more than 0 elements.

Sorry maybe I didn't explain myself clearly. I was asking about how to pass the information inside *args back to the callback.

spacer.png

So in the function above, it's saving a reference to event (which is the function), but *args is just lost. so when self.eventOverInItem get called later on, it will be missing the arguments that were provided in *args. Or Am I missing something?

Edited by Metin2 Dev
Core X - External 2 Internal
Link to comment
Share on other sites

  • Premium
3 hours ago, 3bd0 said:

Sorry maybe I didn't explain myself clearly. I was asking about how to pass the information inside *args back to the callback.

spacer.png

So in the function above, it's saving a reference to event (which is the function), but *args is just lost. so when self.eventOverInItem get called later on, it will be missing the arguments that were provided in *args. Or Am I missing something?

You can just save them like self.eventOverInItemArgs = args, and then later call the event with apply(self.eventOverInItem, self.eventOverInItemArgs). That will call the saved event with the passed arguments.

Edited by Metin2 Dev
Core X - External 2 Internal

The one and only UI programming guideline

Link to comment
Share on other sites

  • 2 months later...

Announcements



×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.