Jump to content

Leadwerks GUI

Josh

2,145 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
      Documentation in Leadwerks 5 will start in the header files, where functions descriptions are being added directly like this:
      /// <summary> /// Sets the height of one terrain point. /// </summary> /// <param name="x">Horizontal position of the point to modify.</param> /// <param name="y">Vertical position of the point to modify.</param> /// <param name="height">Height to set, in the range -1.0 to +1.0.</param> virtual void SetHeight(const int x, const int y, const float height); This will make function descriptions appear automatically in Visual Studio, to help you write code faster and more easily:

      Visual Studio can also generate an XML file containing all of the project's function descriptions as part of the build process. The generated XML file will serve as the basis for the online documentation and Visual Studio Code extension for Lua. This is how I see it working:

      I am also moving all things private to private members. I found a cool trick that allows me to create read-only members. In the example below, you can access the "position" member to get an entity's local position, but you cannot modify it without using the SetPosition() method. This is important because modifying values often involves updating lots of things in the engine under the hood and syncing data with other threads. This also means that any method Visual Studio displays as you are typing is okay to use, and there won't be any undocumented / use-at-your-own risk types of commands like we had in Leadwerks 4.
      class Entity { private: Vec3 m_position; public: const Vec3& position; }; Entity::Entity() : position(m_position) {} It is even possible to make constructors private so that the programmer has to use the correct CreateTerrain() or whatever command, instead of trying to construct a new instance of the class, with unpredictable results. Interestingly, the constructor itself has to be added as a friend function for this to work.
      class Terrein { private: Terrain(); public: friend shared_ptr<World> CreateTerrain(shared_ptr<World>, int, int, int) }; The only difference is that inside the CreateTerrain function I have to do this:
      auto terrain = shared_ptr<Terrain>(new Terrain); instead of this, because make_shared() doesn't have access to the Terrain constructor. (If it did, you would be able to create a shared pointer to a new terrain, so we don't want that!)
      auto terrain = make_shared<Terrain>(); I have big expectations for Leadwerks 5, so it makes sense to pay a lot of attention to the coding experience you will have while using this. I hope you like it!
    • By Josh in Josh's Dev Blog 0
      A new update is available for beta testers.
      Terrain
      The terrain building API is now available and you can begin working with it, This allows you to construct and modify terrains in pure code. Terrain supports up to 256 materials, each with its own albedo, normal, and displacement maps. Collision and raycasting are currently not supported.
      Fast C++ Builds
      Precompiled headers have been integrated into the example project. The Debug build will compile in about 20 seconds the first run, and compile in just 2-3 seconds thereafter. An example class is included which shows how to add files to your game project for optimum compile times. Even if you edit one of your header files, your game will still compile in just a few seconds in debug mode! Integrating precompiled headers into the engine actually brought the size of the static libraries down significantly, so the download is only about 350 MB now.
      Enums Everywhere
      Integer arguments have been replaced with enum values for window styles, entity bounds, and load flags. This is nice because the C++ compiler has some error checking so you don't do something like this:
      LoadTexture("grass.dds", WINDOW_FULLSCREEN); Operators have been added to allow combining enum values as bitwise flags.
      A new LOAD_DUMP_INFO LoadFlags value has been added which will print out information about loaded files (I need this to debug the GLTF loader!).
      Early Spring Cleaning
      Almost all the pre-processor macros have been removed from the Visual Studio project, with just a couple ones left. Overall the headers and project structure have been massively cleaned up.
    • By Josh in Josh's Dev Blog 6
      An often-requested feature for terrain building commands in Leadwerks 5 is being implemented. Here is my script to create a terrain. This creates a 256 x 256 terrain with one terrain point every meter, and a maximum height of +/- 50 meters:
      --Create terrain local terrain = CreateTerrain(world,256,256) terrain:SetScale(256,100,256) Here is what it looks like:

      A single material layer is then added to the terrain.
      --Add a material layer local mtl = LoadMaterial("Materials/Dirt/dirt01.mat") local layerID = terrain:AddLayer(mtl) We don't have to do anything else to make the material appear because by default the entire terrain is set to use the first layer, if a material is available there:

      Next we will raise a few terrain points.
      --Modify terrain height for x=-5,5 do for y=-5,5 do h = (1 - (math.sqrt(x*x + y*y)) / 5) * 20 terrain:SetElevation(127 + x, 127 + y, h) end end And then we will update the normals for that whole section, all at once. Notice that we specify a larger grid for the normals update, because the terrain points next to the ones we modified will have their normals affected by the change in height of the neighboring pixel.
      --Update normals of modified and neighboring points terrain:UpdateNormals(127 - 6, 127 - 6, 13, 13) Now we have a small hill.

      Next let's add another layer and apply it to terrain points that are on the side of the hill we just created:
      --Add another layer mtl = LoadMaterial("Materials/Rough-rockface1.json") rockLayerID = terrain:AddLayer(mtl) --Apply layer to sides of hill for x=-5,5 do for y=-5,5 do slope = terrain:GetSlope(127 + x, 127 + y) alpha = math.min(slope / 15, 1.0) terrain:SetMaterial(rockLayerID, 127 + x, 127 + y, alpha) end end We could improve the appearance by giving it a more gradual change in the rock layer alpha, but it's okay for now.

      This gives you an idea of the basic terrain building API in Leadwerks 5, and it will serve as the foundation for more advanced terrain features. This will be included in the next beta.
×
×
  • Create New...