Jump to content

Leadwerks GUI

Josh

2,154 views

After a lot of research and development, Leadwerks GUI is almost ready to release.  The goal with this system was to create an in-game GUI that was customizable, extendable, and could also serve as a windowed GUI for application development in the future.

Widgets

The GUI system is based on the Widget class.  Once a GUI is created on a rendering context you can add widgets to it.  Each widget is a rectangular container with a padding area.  The widgets can be arranged in a hierarchy and their bounds will clip the contents of their child widgets, both for rendering and mouse interaction.

The GUI is automatically rendered onto the screen inside the call to Context:Sync(),

Widget Scripts

Each widget has a Lua script to control rendering and behavior, similar to the way Lua scripts work in our entity system.  The script assigned to a widget controls what type of widget it is, how it looks, and how it interacts with mouse and keyboard input.  A set of widget scripts are provided to create a variety of controls including buttons, checkboxes, text entry boxes, list boxes, text display areas, choice boxes, sliders, and more.

You can create your own widget scripts to add new types of controls, like for an RPG interface or something else.  The script below shows how the tabber widget is implemented.

--Styles
if Style==nil then Style={} end
if Style.Panel==nil then Style.Panel={} end
Style.Panel.Border=1
Style.Panel.Group=2

--Initial values
Script.indent=1
Script.tabsize = iVec2(72,28)
Script.textindent=6
Script.tabradius=5

function Script:Start()	
	self.widget:SetPadding(self.indent,self.indent,self.tabsize.y+self.indent,self.indent)
end

function Script:MouseLeave()
	if self.hovereditem~=nil then
		self.hovereditem = nil
		local scale = self.widget:GetGUI():GetScale()
		local pos = self.widget:GetPosition(true)
		local sz = self.widget:GetSize(true)
		self.widget:GetGUI():Redraw(pos.x,pos.y,sz.width,self.tabsize.y*scale+1)
		--self.widget:Redraw()
	end
end

function Script:Draw(x,y,width,height)
	local gui = self.widget:GetGUI()
	local pos = self.widget:GetPosition(true)
	local sz = self.widget:GetSize(true)
	local scale = self.widget:GetGUI():GetScale()
	local n
	local sel =  self.widget:GetSelectedItem()
	
	--Draw border
	gui:SetColor(0)
	gui:DrawRect(pos.x,pos.y+self.tabsize.y*scale,sz.width,sz.height-self.tabsize.y*scale,1)
	
	--Draw unselected tabs
	for n=0,self.widget:CountItems()-1 do
		if n~=sel then
			self:DrawTab(n)
		end
	end
	
	--Draw selected tab
	if sel>-1 then
		self:DrawTab(sel)
	end
	
	---Panel background
	gui:SetColor(0.25)
	gui:DrawRect(pos.x+1,pos.y+self.tabsize.y*scale+1,sz.width-2,sz.height-self.tabsize.y*scale-2)
end

function Script:DrawTab(n)
	local gui = self.widget:GetGUI()
	local pos = self.widget:GetPosition(true)
	local sz = self.widget:GetSize(true)
	local scale = self.widget:GetGUI():GetScale()
	local s = self.widget:GetItemText(n)
	
	local textoffset=2*scale
	if self.widget:GetSelectedItem()==n then
		textoffset=0
	end
	
	local leftpadding=0
	local rightpadding=0
	if self.widget:GetSelectedItem()==n then
		gui:SetColor(0.25)
		if n>0 then
			leftpadding = scale*1
		end
		rightpadding = scale*1
	else
		gui:SetColor(0.2)
	end
	gui:DrawRect(-leftpadding+pos.x+n*(self.tabsize.x)*scale,textoffset+pos.y,rightpadding+leftpadding+self.tabsize.x*scale+1,self.tabsize.y*scale+self.tabradius*scale+1,0,self.tabradius*scale)
	gui:SetColor(0)
	gui:DrawRect(-leftpadding+pos.x+n*(self.tabsize.x)*scale,textoffset+pos.y,rightpadding+leftpadding+self.tabsize.x*scale+1,self.tabsize.y*scale+self.tabradius*scale+1,1,self.tabradius*scale)
	
	if self.widget:GetSelectedItem()~=n then
		gui:SetColor(0)
		gui:DrawLine(pos.x+n*self.tabsize.x*scale,pos.y+self.tabsize.y*scale,pos.x+n*self.tabsize.x*scale+self.tabsize.x*scale,pos.y+self.tabsize.y*scale)
	end
	if self.hovereditem==n and self.widget:GetSelectedItem()~=n then
		gui:SetColor(1)
	else
		gui:SetColor(0.7)
	end
	gui:DrawText(s,pos.x+(n*self.tabsize.x+self.textindent)*scale,textoffset+pos.y+self.textindent*scale,(self.tabsize.x-self.textindent*2)*scale-2,(self.tabsize.y-self.textindent*2)*scale-1,Text.VCenter+Text.Center)

end

function Script:MouseDown(button,x,y)
	if button==Mouse.Left then
		if self.hovereditem~=self.widget:GetSelectedItem() and self.hovereditem~=nil then
			self.widget.selection=self.hovereditem
			local scale = self.widget:GetGUI():GetScale()
			local pos = self.widget:GetPosition(true)
			local sz = self.widget:GetSize(true)
			self.widget:GetGUI():Redraw(pos.x,pos.y,sz.width,self.tabsize.y*scale+1)
			EventQueue:Emit(Event.WidgetAction,self.widget,self.hovereditem)
		end
	elseif button==Mouse.Right then
		if self.hovereditem~=self.widget:GetSelectedItem() and self.hovereditem~=nil then
			EventQueue:Emit(Event.WidgetMenu,self.widget,self.hovereditem,x,y)		
		end
	end
end

function Script:KeyDown(keycode)
	if keycode==Key.Right or keycode==Key.Down then
		local item = self.widget:GetSelectedItem() + 1
		if item<self.widget:CountItems() then
			self.widget.selection=item
			local scale = self.widget:GetGUI():GetScale()
			local pos = self.widget:GetPosition(true)
			local sz = self.widget:GetSize(true)
			self.widget:GetGUI():Redraw(pos.x,pos.y,sz.width,self.tabsize.y*scale+1)
			EventQueue:Emit(Event.WidgetAction,self.widget,item)
		end
	elseif keycode==Key.Left or keycode==Key.Up then
		local item = self.widget:GetSelectedItem() - 1
		if item>-1 and self.widget:CountItems()>0 then
			self.widget.selection=item
			local scale = self.widget:GetGUI():GetScale()
			local pos = self.widget:GetPosition(true)
			local sz = self.widget:GetSize(true)
			self.widget:GetGUI():Redraw(pos.x,pos.y,sz.width,self.tabsize.y*scale+1)
			EventQueue:Emit(Event.WidgetAction,self.widget,item)
		end
	elseif keycode==Key.Tab then
		local item = self.widget:GetSelectedItem() + 1
		if item>self.widget:CountItems()-1 then
			item=0
		end
		if self.widget:CountItems()>1 then
			self.widget.selection=item
			local scale = self.widget:GetGUI():GetScale()
			local pos = self.widget:GetPosition(true)
			local sz = self.widget:GetSize(true)
			self.widget:GetGUI():Redraw(pos.x,pos.y,sz.width,self.tabsize.y*scale+1)
			EventQueue:Emit(Event.WidgetAction,self.widget,item)
		end		
	end
end

function Script:MouseMove(x,y)
	local prevhovereditem = self.hovereditem
	self.hovereditem = nil
	local scale = self.widget:GetGUI():GetScale()
	local sz = self.widget:GetSize(true)
	if x>=0 and y>=0 and x<sz.width and y<self.tabsize.y*scale then
		local item = math.floor(x / (self.tabsize.x*scale))
		if item>=0 and item<self.widget:CountItems() then
			self.hovereditem=item
		end
	end
	if self.hovereditem==self.widget:GetSelectedItem() and prevhovereditem==nil then
		return
	end
	if self.hovereditem==nil and prevhovereditem==self.widget:GetSelectedItem() then
		return
	end
	if prevhovereditem~=self.hovereditem then
		local pos = self.widget:GetPosition(true)
		local sz = self.widget:GetSize(true)
		self.widget:GetGUI():Redraw(pos.x,pos.y,sz.width,self.tabsize.y*scale+1)
	end
end

Widget Rendering

Widgets are buffered and rendered with an advanced system that draws only the portions of the screen that need to be updated.  The GUI is rendered into a texture, and then the composite image is drawn onscreen.  This means you can have very complex interfaces rendering in real-time game menus with virtually no performance cost.

By default, no images are used to render the UI so you don't have to include any extra files in your project.

Widget Items

Each widget stores a list of items you can add, remove, and edit.  These are useful for list boxes, choice boxes, and other custom widgets.

GUI Events

Leadwerks 4.4 introduces a new concept into your code, the event queue.  This stores a list of events that have occurred.  When you retrieve an event it is removed from the stack:

	while EventQueue:Peek() do
		local event = EventQueue:Wait()
		if event.source == widget then
			print("OK!")
		end
	end

Resolution Independence

Leadwerks GUI is designed to operate at any resolution.  Creation and positioning of widgets uses a coordinate system based on a 1080p monitor, but the GUI can use a global scale to make the interface scale up or down to accommodate any DPI including 4K and 8K monitors.  The image below is rendering the interface at 200% scaling on a 4K monitor.

gui.thumb.png.cdc37a2be840446845db2915d57a754c.png

A default script will be included that you can include from Main.lua to build up a menu system for starting and quitting games, and handling common graphical features and other settings.

Image2.thumb.jpg.86e408079029dc5f8e15be9ee8487159.jpg

Leadwerks GUI will be released in Leadwerks Game Engine 4.4.



10 Comments


Recommended Comments

Looks awesome Josh, can't wait to use it.

quick question: if you set the game time/speed to 0, does this affect the UI? This is usefull for when you want to pause the games (read: no updateworld calls) but still want to have a working UI.

Share this comment


Link to comment

@Josh May I suggest 3 sliders for volume settings in your default options menu?

  • Music volume
  • Effects volume
  • Dialogue volume

Share this comment


Link to comment
On ‎5‎/‎30‎/‎2017 at 5:37 AM, AggrorJorn said:

Looks awesome Josh, can't wait to use it.

quick question: if you set the game time/speed to 0, does this affect the UI? This is usefull for when you want to pause the games (read: no updateworld calls) but still want to have a working UI.

The GUI system is not affected by timing in any way.  It's completely event-based.

Share this comment


Link to comment

I think this design is really cool! It'll be interesting to see what type of controls people come up with and put up on the Workshop!

Share this comment


Link to comment

OMG! This update is awesome!! I am going to design game soon afterwards I get my written documents type up. Leadwerks is improving greatly. Please, Team Leadwerks keep on thriving on Leadwerks Engine. I desire to design the best video product I can invest into Leadwerks Engine. You lured away from Unreal Engine, especially the heavy cost Unreal charges and Unity charges even more than everyone, ridiculous. :wacko::unsure:

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By Josh in Josh's Dev Blog 0
      Textures in Leadwerks don't actually store any pixel data in system memory. Instead the data is sent straight from the hard drive to the GPU and dumped from memory, because there is no reason to have all that data sitting around in RAM. However, I needed to implement texture saving for our terrain system so I implemented a simple "Pixmap" class for handling image data:
      class Pixmap : public SharedObject { VkFormat m_format; iVec2 m_size; shared_ptr<Buffer> m_pixels; int bpp; public: Pixmap(); const VkFormat& format; const iVec2& size; const shared_ptr<Buffer>& pixels; virtual shared_ptr<Pixmap> Copy(); virtual shared_ptr<Pixmap> Convert(const VkFormat format); virtual bool Save(const std::string& filename, const SaveFlags flags = SAVE_DEFAULT); virtual bool Save(shared_ptr<Stream>, const std::string& mimetype = "image/vnd-ms.dds", const SaveFlags flags = SAVE_DEFAULT); friend shared_ptr<Pixmap> CreatePixmap(const int, const int, const VkFormat, shared_ptr<Buffer> data); friend shared_ptr<Pixmap> LoadPixmap(const std::wstring&, const LoadFlags); }; shared_ptr<Pixmap> CreatePixmap(const int width, const int height, const VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, shared_ptr<Buffer> data = nullptr); shared_ptr<Pixmap> LoadPixmap(const std::wstring& path, const LoadFlags flags = LOAD_DEFAULT); You can convert a pixmap from one format to another in order to compress raw RGBA pixels into BCn compressed data. The supported conversion formats are very limited and are only being implemented as they are needed. Pixmaps can be saved as DDS files, and the same rules apply. Support for the most common formats is being added.
      As a result, the terrain system can now save out all processed images as DDS files. The modern DDS format supports a lot of pixel formats, so even heightmaps can be saved. All of these files can be easily viewed in Visual Studio itself. It's by far the most reliable DDS viewer, as even the built-in Windows preview function is missing support for DX10 formats. Unfortunately there's really no modern DDS viewer application like the old Windows Texture Viewer.

      Storing terrain data in an easy-to-open standard texture format will make development easier for you. I intend to eliminate all "black box" file formats so all your game data is always easily viewable in a variety of tools, right up until the final publish step.
    • By Josh in Josh's Dev Blog 1
      I wanted to see if any of the terrain data can be compressed down, mostly to reduce GPU memory usage. I implemented some fast texture compression algorithms for BC1, BC3, BC4, BC5, and BC7 compression. BC6 and BC7 are not terribly useful in this situation because they involve a complex lookup table, so data from different textures can't be mixed and matched. I found two areas where texture compression could be used, in alpha layers and normal maps. I implemented BC3 compression for terrain alpha and could not see any artifacts. The compression is very fast, always less than one second even with the biggest textures I would care to use (4096 x 4096).
      For normals, BC1 (DXT1 and BC3 (DXT5) produce artifacts: (I accidentally left tessellation turned on high in these shots, which is why the framerate is low):

      BC5 gives a better appearance on this bumpy area and closely matches the original uncompressed normals. BC5 takes 1 byte per pixel, one quarter the size of uncomompressed RGBA. However, it only supports two channels, so we need one texture for normals and another for tangents, leaving us with a total 50% reduced size.

      Here are the results:
      2048 x 2048 Uncompressed Terrain:
      Heightmap = 2048 * 2048 * 2 = 8388608 Normal / tangents map = 16777216 Secret sauce = 67108864 Secret sauce 2 = 16777216 Total = 104 MB 2048 x 2048 Compressed Terrain:
      Heightmap = 2048 * 2048 * 2 = 8388608 Normal map = 4194304 Tangents = 4194304 Secret sauce = 16777216 Secret sauce 2 = 16777216 Total = 48 MB Additionally, for editable terrain an extra 32 MB of data needs to be stored, but this can be dumped once the terrain is made static. There are other things you can do to reduce the file size but it would not change the memory usage, and processing time is very high for "super-compression" techniques. I investigated this thoroughly and found the best compression methods for this situation that are pretty much instantaneous with no noticeable loss of quality, so I am satisfied.
    • By jen in jen's Blog 0
      My small project will be called Foregate, it will be a dark medieval Diablo style single player action RPG.
      The graphics will be simple, no PBR, 256x256 map, reasonably low-res models.
      Camera style? Top-down-ish I think? Like in Diablo exactly - and because the camera is not directly in-front of the 3 models, I can get away with low-resolution assets - bonus. Also, with top-down view, I won't have to worry about high resolution sky-boxes. 
      What's my plan for this project?
      I plan to make this project as small and as simple as possible, possibly release it as open-source, and have fun with it of course.
      My previous experience with game development (1-2 years ago?) was amateurish I think, still is now. I want to give it a go again, this time with experience although my skill in C++ is not really that good? Maybe I can improve it in this project.
      More about the game
      The content is not set in stone yet but I have a general idea of how the mechanics is going to look and feel - Diablo-ish obviously. It'll have monsters (ancient & mythical probably), loot when killing a monster, gold as in-game currency, visual grid inventory, player stats (level, strength, agility, vitality, energy, &c.). 
      The game will be single-player. Possibly a coop multiplayer also? I don't have any interest in making massive multi-player. 
      I started my development yesterday with the basic preparations (setting up project environment, &c.), today I made my first step in developing the core components; worker class, game state, task class.
      I have a game state that keeps a single source of truth for the entire application; all game data will be stored in this class as "states". 
      I also have a "Worker" which will do the processing of tasks in the game.
      I also have "object" class, this can be a monster, the player, a weapon, a prop, or an NPC.
      So the idea is to have a CQRS type of interaction between the classes and the data. Any action in the game will be interpreted as "Task" for the Worker class. The worker class iterates through the Task. Tasks can be created by any class interfaced with the Worker class trough "addNewTask" and the new tasks can be of a certain type i.e.: ATTACK, IDLE, SAVE_GAME, EXIT_GAME, the new task will also have a payload data and it's processed according to its task type e.g. an ATTACK with payload "{ Damage: 10, Target: MonsterA }" will reduce the health of MonsterA by 10 - the worker class will change the game state; find MonsterA in MonsterState and reduce its health by 10. 
      I think it's advantageous to have this type of centralized module where all actions are processed; I can do all sorts of procedures during the processes, maybe debug data, filter actions, mutate payloads, and such.
      How much time am I going to put into this?
      A couple of hours a day for 3 days a week maybe.
      So it's all a rough sketch for now and it's heading the right direction. I'll have more to report later on. 

      This is Forgate Castle, minus the castle, in the map Forgate; the starting location for the player. The fortification will have merchants, and quest givers.
       
×
×
  • Create New...