Custom UI Elements

From Age of Empires 4 Modding


Creating custom UI requires decent knowledge of XAML, which was created by Microsoft as part of WPF framework. Most knowledge should be transferable, but there are some differences based on library that Relic used to convert XAML to C++ & DirectX. Older version of that library can be found at https://github.com/KevinW1998/wpfg/tree/master/WPFLib/src

Basic explanation how to create:

  • Create _mod_data that contains field data_context, which will be holding all data that UI will use.
  • Create XAML UI and save it to variable
    local xaml = [[
    <Grid>
        <TextBlock>Hello</TextBlock>
    </Grid>
    ]]
    

  • Call in function UI_AddChild
UI_AddChild("ScarDefault", "XamlPresenter", _mod_data .ui, { IsHitTestVisible = true, Xaml = xaml, DataContext = UI_CreateDataContext(_mod_data .data_context) })
  • Following this continue by looking at example and setting up more complicated things such as update rules, bindings etc., NOTE: if UI should be synchronized between players, then you will need to learn more about NetworkEvent (example can be found in official diplomacy.scar)

Common Issues

Binding game objects is not possible and will end up in AVUI to LUA error ingame. To bind ingame data, its best to convert them to plain string first.

Example

Following code (scar script) creates simple button in middle of match viewport/screen. It should serve as starting point for anyone who wants to dive deeper into creating more complex custom UI for mods.
-----------------------------------------------------------------------
--
-- CookieCliker UI
--
-- UI component of CookieCliker system.  
--
-----------------------------------------------------------------------

-----------------------------------------------------------------------
-- Data
-----------------------------------------------------------------------

_cookie_button = {
	ui = "CookieButton",
	is_ui_created = false,
	data_context = {
		is_ui_visible = true,
		is_dropdown_visible = false,
		command = {
			toggle_dropdown = UI_CreateCommand("CookieButton_ToggleDropdown"),
		},
		data = {
			player = "Cookie",
			player_name = "Cookie",
			player_race = "Cookie",
			player_team = "-",
			update_count = -1,
			update_value = "-1",
		},
	},
}

-----------------------------------------------------------------------
-- Scripting framework 
-----------------------------------------------------------------------

Core_RegisterModule("CookieButton")

-----------------------------------------------------------------------
-- Callbacks
-----------------------------------------------------------------------

-- Callback invoked by Core::OnInit()
function CookieButton_OnInit()
	CookieButton_CreateDataContext()
	-- Here would be condition to unload it
	-- Core_UnregisterModule("CookieButton")	
end

-- Called by _StartMission() in core.scar
function CookieButton_Start()	
	CookieButton_CreateAndUpdateUI()
end

-- Callback invoked by Core_OnGameRestore()
--function CookieButton_OnGameRestore()
--	_cookie_button.is_ui_created = false	-- coming in from a savegame - the UI won't have been saved so we need to clear this flag and then recreate the UI
--	CookieButton_CreateAndUpdateUI()
--end

-- Callback invoked by Core_OnGameOver()
-- Stops rules rules and removes UI elements associated with CurrentAgeUI module before end mission/match screen is displayed.
function CookieButton_OnGameOver()	
	CookieButton_Show(false)
end

-- Callback invoked by Core_SetPlayerDefeated() when a player is eliminated.
function CookieButton_OnPlayerDefeated(player, reason)
	if player.isLocal then 
		CookieButton_Show(false)
	end
end

-----------------------------------------------------------------------
-- Rules
-----------------------------------------------------------------------

function CookieButton_CreateDataContext()
	--local local_player = Game_GetLocalPlayer()
	_cookie_button.data_context.data.player = "Player Cookie" 			--local_player
	_cookie_button.data_context.data.player_name = "Cookie Lord"		--Player_GetDisplayName(local_player)
	_cookie_button.data_context.data.player_race = "Dark Cookie"		--Player_GetRace(local_player)
	_cookie_button.data_context.data.player_team = "-"
	_cookie_button.data_context.data.update_count = 0	
	_cookie_button.data_context.data.update_value = tostring(0)	
end

function Rule_CookieButton_UpdateUI()		 			
	if _cookie_button ~= nil and _cookie_button.data_context ~= nil then 
		CookieButton_Update()
	end
end

function CookieButton_Update()
	--local local_player = Game_GetLocalPlayer()
	local update = _cookie_button.data_context.data.update_count + 1
	_cookie_button.data_context.data.update_count = update	
	_cookie_button.data_context.data.update_value = tostring(update)	
		
	if _cookie_button.is_ui_created then 
		UI_SetDataContext(_cookie_button.ui, _cookie_button.data_context)	
	end
end

-----------------------------------------------------------------------
-- Private/Internal functions
-----------------------------------------------------------------------

function CookieButton_Show(show_ui)
	if _cookie_button.is_ui_created then 
		_cookie_button.data_context.is_ui_visible = show_ui
		UI_SetDataContext(_cookie_button.ui, _cookie_button.data_context)		
	end
end

function CookieButton_CreateAndUpdateUI()	
	
	CookieButton_CreateUI()
	
	if not Rule_Exists(Rule_CookieButton_UpdateUI) then
		Rule_AddInterval(Rule_CookieButton_UpdateUI, 0.250)
	end
end

function CookieButton_CreateUI()	
	if not _cookie_button.is_ui_created then
		
		local xaml = [[<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:esControls="clr-namespace:WPFGUI.Shared.Controls;assembly=EngineUI"
		xmlns:esUtility="clr-namespace:WPFGUI.Shared.Utility;assembly=EngineUI"
		xmlns:wm="clr-namespace:WPFGUI.Shared.MarkupExtensions;assembly=EngineUI"
		HorizontalAlignment="Center" 
		VerticalAlignment="Center"  
		Visibility="{Binding [is_ui_visible], Converter={StaticResource BoolToVis}}">
	<Grid.RowDefinitions>
	    <RowDefinition Height="Auto" />
	    <RowDefinition Height="*" />
	</Grid.RowDefinitions>
	<Button Grid.Row="0" 
			Width="500"
			Height="100"
			Panel.ZIndex="2"
			Content="Get cookie"
			Command="{Binding [command][toggle_dropdown]}">	
	</Button>
	<Border Grid.Row="1"
		    Panel.ZIndex="1"
			Width="300"
			Height="500"
			BorderBrush="Lime"
		    Visibility="{Binding [is_dropdown_visible], Converter={StaticResource BoolToVis}}">
		<Grid>
			<Grid.RowDefinitions>
			    <RowDefinition Height="Auto" />
			    <RowDefinition Height="Auto" />
			    <RowDefinition Height="Auto" />
			    <RowDefinition Height="Auto" />
			    <RowDefinition Height="Auto" />
			</Grid.RowDefinitions>
	        <TextBlock Grid.Row="0" Text="Welcome to the dark side!" />
	        <TextBlock Grid.Row="1" Text="{Binding [data][player_name]}" />
	        <TextBlock Grid.Row="2" Text="{Binding [data][player_race]}" /> 
	        <TextBlock Grid.Row="3" Text="{Binding [data][player_team]}" /> 
	        <TextBlock Grid.Row="4" Text="{Binding [data][update_value]}" />  
		</Grid>
	</Border>
  </Grid>
]]
		
		UI_AddChild("ScarDefault", "XamlPresenter", _cookie_button.ui, { IsHitTestVisible = true, Xaml = xaml, DataContext = UI_CreateDataContext(_cookie_button.data_context) })
		_cookie_button.is_ui_created = true
	end
end


function CookieButton_ToggleDropdown(context)
	_cookie_button.data_context.is_dropdown_visible = not _cookie_button.data_context.is_dropdown_visible
	UI_SetDataContext(_cookie_button.ui, _cookie_button.data_context)	
	if(_cookie_button.data_context.is_dropdown_visible) then
		UI_AddEventHandler("Scar", "HUDPageBase.Cancel", "CookieButton_HideDropdown")
	else
		UI_RemoveEventHandler("Scar", "HUDPageBase.Cancel", "CookieButton_HideDropdown")
	end
end

-- callback invoked by pressing the ESC key while Cookie panel is open
function CookieButton_HideDropdown(context)
	if(_cookie_button.data_context.is_dropdown_visible) then
		CookieButton_ToggleDropdown()
	end
end