I'm a doctor, not a mechanic

UE3:DynamicUIListTutorial

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, search

Listing custom objects in a menu (with a picture and a description)[edit]

I will say right now, this may not be the best way of doing this. I didnt look into the MenuList method that Epic uses as it seems to go for a prebuilt list. Instead I went for a UTUIList as it allows me to dynamically decide what goes in the list. The following is the beginning to end of getting a list appearing on an ingame UI tab. Why would you want this? The immediate thing that springs to mind is a Counter-strike style buy system, where you only want to list the items the user can afford (or has reached the skill level to use).

What I've written below isn't tested. Its based on code that I have working but its not been put into a project from scratch and confirmed to work. I'll update as people point out problems.

I started by looking for something that matched my end goal. The GameMode selection menu was a perfect example of what I was trying to do, so I went looking for that first. I loaded the game, and selected deathmatch. On here were the words "Free-for-all deathmatch action". Thats going to be pretty unique, so I searched the UDK directory for it. A single result is found in UTGame.int. An .INT file is basically a .INI file, but the data within it is for localized content, eg content you want to translate. Chances are there will also be some data in UTGame.ini

Here is what we see in the .int

;///////////////
; UT3 Game Modes
;///////////////
[UTDeathmatch UTUIDataProvider_GameModeInfo]
FriendlyName=Deathmatch
Description=Free-for-all deathmatch action.

Everything looks obvious, its the UTDeathmatch game type, and we are seeing the Friendly name and the description for the international translation. But whats this UTUIDataProvioder_GameModeInfo thing? Another search shows there is a UTUIDataProvioder_GameModeInfo class and looking at the file we can see the FriendlyName and Description created here. So it looks like the way it works is you create an entry in the .ini for the class you are interested in, and then say the dataprovider that should consume the data you are looking at.

As we are doing something new in the menu system, lets create a new provider.

class MyUIDataProvider_Buy extends UTUIResourceDataProvider
	PerObjectConfig;
 
/** The items class path */
var config string ClassName;
 
/** Friendly name for the items */
var config localized string FriendlyName;
 
/** Description for the items */
var config localized string Description;
 
/** Image that the icon of this item is on. */
var config string IconImage;
 
/* The cost of the item */
var config float cost;
 
defaultproperties
{
	bSearchAllInis=true
}

OK. Now I've found where the data comes from, Im stuck. Lets look for another start point. We know that the game mode selector is a User interface, and I know from looking through the Unreal Editor that they are accessible from the content browser. So I'll go looking there. If you change the filter to only show UIScenes you can see that there is a UTUITabPage_GameModeSelection, lets see what goodies that holds.

/** Preview image for a game mode. */
var transient UIImage	GameModePreviewImage;
 
/** Description of the currently selected game mode. */
var transient UILabel	GameModeDescription;
 
/** Reference to the game mode list. */
var transient UTUIList GameModeList;

Looks like Im in the right place. I see the image box, the description box and the List area. Every UI element has a SetDataStoreBinding, and the lovely Unreal Modding community pointed out to me that you can link a UIList to a datastore. I guess its time to find out what a datastore is. I did a search for "GameModes" and in amongst the results (and boy was there a lot!) is DefaultGame.ini with

+ElementProviderTypes=(ProviderTag="GameModes",ProviderClassName="UTGame.UTUIDataProvider_GameModeInfo")

This is written under UTGame.UTUIDataStore_MenuItems, and the description for that file says "Inherited version of the game resource datastore that has UT specific dataproviders.". Hey, I just wrote a data provider! So a DataStore stores data provided by providers (good naming convention ;) ) and it looks like to add to the menu providers, we just have to add a new line to defaultgame.ini

+ElementProviderTypes=(ProviderTag="BuyEquipment",ProviderClassName="MyMod.MyUIDataProvider_Buy")

That should do it. Now we have told the menulist Datastore to consume anything with a MyUIDataProvider_Buy element in any ini or .int files that it finds. Lets put some stuff in the .ini and .int.

Add this to the .ini

[MyWeap_Pistol MyUIDataProvider_Buy]
ClassName=MyMod.MyWeap_Pistol
cost=300.00
IconImage=<Images:UI_FrontEnd_Art.GameTypes.___TeamDeathmatch>
IconU=442
IconV=76
IconUL=129
IconVL=104
 
[MyWeap_SubMachineGun MyUIDataProvider_Buy]
ClassName=MyMod.MyWeap_SubMachineGun
cost=600.00
IconImage=<Images:UI_FrontEnd_Art.GameTypes.___Deathmatch>
IconU=442
IconV=76
IconUL=129
IconVL=104

and this to the .int

[MyWeap_Pistol MyUIDataProvider_Buy]
FriendlyName=Sub-machine gun
Description=SMG desc goes here
 
[MyWeap_SubMachineGun MyUIDataProvider_Buy]
FriendlyName=Sub-machine gun
Description=SMG desc goes here

The next step is to create a datastore thats only got the data that I want to display. Its very quick and easy, it will just extend the string datastore, and use a custom tag.

class MyUIDataStore_Equipment extends UTUIDataStore_StringList;
 
defaultproperties
{
	Tag="EquipmentStore"
}

Now I guess I should create a list to display the data. We need to create a new class that can handle a UI_Scene. With my game I added a tab to the mid-game menu. I'm going to skip how to make a UI_Scene for now as this tutorial has taken long enough to get written and is going out of scope (the focus here is a customizable UIList). I'll revisit if really needed - You'll need to create your own mid game menu, then add a tab with the class MyUITabPage_BuyEquipment. I'd have a look at this tutorial http://www.moddb.com/games/unreal-tournament-3/tutorials/unreal-learning-2-ut3-configurable-mutators if you really need help getting started. Make sure your code compiles before you create the scene as your UIScene needs to extend this class and it will only appear in UEd once your code compiles correctly.

The following allows us to hook into the elements in the scene. For convience I named everything in the scene with the same name as the variables.

class MyUITabPage_BuyEquipment extends UTUITabPage_InGame;
 
/** The lists of equipment the player can choose **/
var transient UTUIList lstEquipment;
 
/** Preview image for an item. */
var transient UIImage	imgEquipPreview;
 
/** scrollframe which contains the description label - allows the player to read long descriptions */
var	transient UIScrollFrame DescriptionScrollFrame;
var transient UILabel	lblDescription;
 
var transient MyUIDataStore_Equipment EquipmentDataStore;
var array<UTUIResourceDataProvider> EquipmentProvider;
 
/** Reference to the menu item datastore. */
var transient UTUIDataStore_MenuItems MenuItemDataStore;

On PostInitialize, we hook the variables to the values in the UIScene

/** Post initialization event - Setup widget delegates.*/
event PostInitialize()
{	
	InitDataStores();
 
	Super.PostInitialize();
 
	// Store widget references
	lstEquipment = UTUIList(FindChild('lstEquipment', true)); // Find the item in the UIScene
	lstEquipment.SetDataStoreBinding("<EquipmentStore:AvailableEquipment>"); // Hook the list to our data (set up in InitDataStore)
	lstEquipment.OnValueChanged = OnEquipment_ValueChanged; // When the use selects from the list, run this function
	lstEquipment.OnSubmitSelection = None;
	lstEquipment.OnRawInputKey=None;
	lstEquipment.OnRefreshSubscriberValue = None;
 
	// Store widget references
	imgEquipPreview = UIImage(FindChild('imgEquipPreview', true)); // Find the image in the UIScene
	lblDescription = UILabel(FindChild('lblDescription', true)); // Find the description in the UIScene
	DescriptionScrollFrame = UIScrollFrame(FindChild('DescriptionScrollFrame',true)); // Find the scroller in the UIScene
 
	// if were on a console platform, make the scrollframe not focusable.
	if ( IsConsole() && DescriptionScrollFrame != None )
	{
		DescriptionScrollFrame.SetPrivateBehavior(PRIVATE_NotFocusable, true);
	}
}

The eagle eyed will spot that the first thing PostInitalize does is InitDataStores(). This is a custom function that creates the EquipmentStore datastore, and fills it with the dataprovider.

/** We need to filter down from all weapons, to allowed weapons **/
function InitDataStores()
{
	local DataStoreClient DSClient;
	local int Idx;
 
 
	DSClient = class'UIInteraction'.static.GetDataStoreClient();
 
	if (DSClient == none)
		return;
 
	// Create the equipment store for the menu if we havent already
	EquipmentDataStore = MyUIDataStore_Equipment(DSClient.FindDataStore(class'MyUIDataStore_Equipment'.default.Tag));
 
	// Dont do this bit if we already have a datastore (otherwise well start duplicating data in the menu)
	// Alternatively, use EquipmentDataStore.Empty(AvailableEquipment) to clear the list before filling it again.
 
	if (EquipmentDataStore != None)
		return;
	else
		EquipmentDataStore = DSClient.CreateDataStore(class'MyUIDataStore_Equipment');
 
	if (EquipmentDataStore != None)
		DSClient.RegisterDataStore(EquipmentDataStore);
 
	// Get the data about equipment from the ini/int files
	MenuItemDataStore = UTUIDataStore_MenuItems(DSClient.FindDataStore('UTMenuItems'));
	MenuItemDataStore.GetProviderSet('BuyEquipment', EquipmentProvider);
 
	// If length = 0 then dont bother with the rest
	if(EquipmentProvider.Length == 0)
		return;
 
	// Fill the datastore with the allowed values from the ini/int
	for(Idx=0; Idx<EquipmentProvider.length; Idx++)
	{
		// Add your filter here.  Ive hard coded it to anything costing less than £500
		if (MyUIDataProvider_Buy(EquipmentProvider[Idx]).cost <= 500.00)
			EquipmentDataStore.AddStr('AvailableEquipment',  MyUIDataProvider_Buy(EquipmentProvider[Idx]).FriendlyName, false);
	}
        // Just to show you can add stuff without using a .int/ini
	EquipmentDataStore.AddStr('AvailableEquipment', "Test string", false);
}

So the above should get us a list, but when you click it nothing happens. We need to write some code to change the image each time the user selects from the list. That was the OnValueChanged that we wrote in the PostInitialize function.

function OnEquipment_ValueChanged( UIObject Sender, optional int PlayerIndex=0 )
{
	local int SelectedItem;
	local MyUIDataProvider_Buy selEquip;
	local string equipname;
 
	//`log ("In value changed "$Sender );
 
	InitDataStores();
 
	if (EquipmentProvider.Length == 0)
		return;
 
	SelectedItem = UTUIList(Sender).GetCurrentItem();
	equipname = EquipmentDataStore.GetStr('AvailableEquipment', SelectedItem);
 
	selEquip = GetSelectedEquipment(EquipmentProvider, equipname);
	if(selEquip != none)
	{
		//`log ("image = "$selEquip.IconImage);
		imgEquipPreview.SetDatastoreBinding(selEquip.IconImage);
		lblDescription.SetDatastoreBinding(selEquip.Description);
	}
}
 
function MyUIDataProvider_Buy GetSelectedEquipment(array<UTUIResourceDataProvider> Providers, string FriendlyName)
{
	local int Idx;
 
	//`log ("providers.length"@ Providers.Length);
	if (Providers.Length <= 0)
		return none;
 
	for(Idx=0; Idx<Providers.length; Idx++)
	{
		if (Providers[Idx] == none)
			continue;
		if (FriendlyName ~= MyUIDataProvider_Buy(Providers[Idx]).FriendlyName)
			return MyUIDataProvider_Buy(Providers[Idx]);
	}
 
	return none;
}

A downloadable zip file with this can be found here - http://www.mediafire.com/?undjileiwzk (excuse the crappy hosting I don't know where else to put it). Everything can be dropped into place apart from the UTGame.ini file. I recommend just taking the ElementProviderTypes line and adding that to your own code. I've checked it out and the mod loads and runs. Press escape and click the buy tab to see it working.