dgcor / DGEngine

An implementation of the Diablo 1 game engine
Other
243 stars 30 forks source link

[Newbie question] How do you implement the GUI of the game? #12

Open markus-zhang opened 7 years ago

markus-zhang commented 7 years ago

Hi! I found your project when searching for open source Diablo engines. Because I'm learning programming and writing my own 2d game engine (a very simple one as I'm just a newbie) so I dug a bit into the codes. This project is too complex for me so I'm only focusing on the part that you implement gui (e.g. how do you implement the onClick event of a button)

From what I read, the gui part seems to be scattering around "ActButton.h", "Button.h/cpp" as well as "BitmapButton.h/cpp":

In the ActButtonClick class, you have the execute() function that calls the click() function of a button (as a resource of the game), and in BitmapButton class we will addback the action to the EventManager. Then I was lost, as I couldn't find how the game will execute the actions because in the Eventmanager class I didn't find much relating code. Could you please give me a hint about this? (e.g. if I click the Inv button the inventory screen should be brought up, I'm trying to locate the code about this specific action under the "Action" directory, without success so far)

Thanks in advance! BTW if you need my email it's markus_zhang@hotmail.com

I'm really new to large projects, so I'm sorry if this is too newbie or too much of nuisance. Please ignore the message if it's bothering. I'm going to spend more time reading your code anyway.

ghost commented 7 years ago

Hi!

All UI objects in DGEngine implement the UIObject interface. This interface has the functions that all UI objects must implement, such as resizing themselves when the window size changes. You have functions for size and position of objects and to set the visible property. Finally, you have a draw and an update.

All events that an UI object can have (such as click, scroll, hover) are handled in the update function.

There are 2 types of buttons: BitmapButton and StringButton. One uses an image and the other uses a text string. StringButtons can be of 2 types: BitmapText and StringText. One uses a bitmap font (most of the Diablo game UI) and the other uses a classic font file (TrueType, etc - used for the credits and when the game data is missing). This allows one to change the type of font in the .json files to change from one type to the other easily.

Both types of text implement the DrawableText interface that has common options between all fonts. a String button has a member that is an instance of Text2, which basically a textbox control, but adds the functionality of a button to it (onClick, onHover, etc). To sum up, a Text2 holds either a BitmapText or a StringText.

On a button's update function, you check if the mouse was clicked and if it was inside the button area and, as you saw, add the click action to the EventManager instance that is in the main Game object. On every iteration of the game loop, we draw, update and execute all functions in the EventManager and remove them once they finished. An Event is just an Action with an associated time (used, for instance, in the splash screen to close it after x seconds). when you execute an action, you usually add it to the EventManager as an Event with the time as 0, so it executes immediately.

Actions return a bool. Usually true to indicate the action finished. If it returns false, it doesn't get removed from the event manager and will be executed again until it returns true. This was used for the FadeIn/Out actions in a previous version of the code. I don't think there are actions now that return false, but the functionality is there in case one is needed in the future.

Now, the ActButton.h file has actions to interact with buttons, such as enabling the button, changing the text (in case of a text button), but this file is not used when you click on the inventory button, the update function of the button is. These functions are used to, for instance execute the action of another button when clicking on a different button/pressing a key or to disable a button, etc. and can be called anywhere (such as in a mouse hover/when pressing a key).

The Inventory button is defined in gamePanel.json search for: "id": "inv". It basically just shows / hides the image that shows the game panel, for now.

Most actions use C++'s RTTI (dynamic_cast) to check if the UIObject is of the correct type (buttons need a button, not a text) and if so, execute the appropriate action. Otherwise, it does nothing. This prevents the game from crashing by executing invalid actions on objects of a different type.

A Menu is basically a list of Buttons, but has some extra actions (mouse scroll). It is used for the quests texts and the existing save games menu (you need more than 6 save games for the scrollbar to show - just like in the original game).

So, when you click the inventory button, its update function will add an Event to the Game object's EventManager instance that will be executed on the updateEvents() function on every game loop iteration and removed from the event manager after it completes. The OnClick action of the inventory button is actually an action of the type ActionList, which is just a list of Action objects. This happens when the action is an array and not an object.

"onClick": [
  { "name": "drawable.visible", "id": "invPanel", "visible": true },
  { "name": "drawable.visible", "id": "spellPanel", "visible": false },
  { "name": "button.setToggle", "id": "spell", "toggle": false }
],

Now, the inventory button has these properties:

"clickUp": true,
"onToggle": [...]

The clickUp makes the button only execute the action when the mouse is released once the button is pressed. Otherwise the action would execute before the mouse is released. This is because the original game does this as well.

The OnToggle action, by being set, turns the button into a toggle button. A toggle button is just a button with 2 actions. This is to have buttons that show on the first click and hide on the second. Note that the same behaviour can be accomplished using other actions and plain buttons, but it's not the case with the inventory button.

Finally, the EventManager has both addFront and AddBack functions so that the onHover enter/Leave actions are executed in the correct order. Otherwise, when moving the mouse very quickly between buttons with these events defined, you could have side effects (in the game panel, the texts might appear wrong or not at all).

Have fun reading the code :)

markus-zhang commented 7 years ago

@dgengin Thank you so much! I didn't expect such a long and detailed reply, that's for sure. Just browsing your answer makes me feel excited. I'm working two shifts right now and will wait until the weekend to digest your answer. Thank you again for the long reply! Really appreciate it!

pengyz commented 7 years ago

It's also helpful for me. thx