[COMMUNITY PROJECT] RPG Framework based on AAA toolset

hi all, as a ‘school project’/‘thought experiment’ I poked around the old Dragon Age Origins toolset and ported some scripts to C++/UE4. For learning experience if you want to take a look

https://github.com/dhk-room101/da2ue4

visually it doesn’t do anything, but uses extensive logging. Currently there is nothing in the ‘how it looks’ category, but in the ‘how it works’ category there is pretty decent AI and combat logic to learn from. Still requires some poking around the code, and it requires solid C++, as there are no blueprints, except the minimum required for some actors/objects. Uses extensive reflection though…

The framework is of course far from complete, and I may keep on adding more features, or you can just clone the repository and do it on your own. for RPG enthusiasts such as myself this project should be very appealing :slight_smile:

Thank you for reading.

I will keep an eye on this.

The older bioware games were awesome. Cannot wait to see this in a more mature state :slight_smile:

Thank you for the comment @HeadClot !!

example of combat logic logs, tactics related


Bandit1 : Channel 1111: [sys_soundsets_h.GetFlag.SOUND_SET_FLAGS_0] Flag: 2(4) Value: 0 Result: 0 
Channel 1111: [sys_soundsets.play] oCreature: Bandit1, nSituation: 2, Sound: 2
Playing SoundSet: COMBAT BATTLE CRY
Channel 1100: [Ambient_Stop()] 
Channel 1: [IsUsingMeleeWeapon] creature: Bandit1, Weapon= DefaultWeapon_C_1, type: 2
Channel 500: [0.434608 DA2UE4RulesComponent] Enemy perceived too close - not delaying start of combat
Channel 100: [Bandit1::AI_DetermineCombatRound] ***** START ***** , last command status: 1
Bandit1 : Channel 600: [core_h.GetCreatureFlag] Flag: 8 Value: 0 Result: 0 
Channel 100: [Bandit1::AI_DetermineCombatRound] Team Help Status: 0
Channel 100: [Bandit1::_AI_HasAIStatus] checking for creature: Bandit1, status: 1
Channel 100: [Bandit1::_AI_HasAIStatus] return: 0
Channel 100: [Bandit1::_AI_GetPackageTable] Using table: 10011
_AI_GetTacticsNum: requested nPackageTable is 10011
Channel 100: [Bandit1::AI_DetermineCombatRound] tactics num: 2
Channel 100: [Bandit1::AI_DetermineCombatRound] Last Tactic ID: 0
Channel 100: [Bandit1::AI_DetermineCombatRound] nTablesDisabled= 0
Channel 100: [Bandit1::AI_DetermineCombatRound] i= 1
Channel 100: [Bandit1::_AI_ExecuteTactic] START [Package Table: 10011], TacticID: [1]
Channel 100: [Bandit1::_AI_ExecuteTactic] [1][Target: 3] [Condition: 8] [USE ABILITY] [SHIELD_BASH (617)] 
Channel 100: [Bandit1::_AI_ExecuteTactic] [Condition Valid for Target: 262135] [Condition Base: 2] [Condition Parameter: 75] 
Channel 100: [Bandit1::_AI_IsAbilityValid] ERROR: Creature does not have ability: 617
Channel 100: [Bandit1::_AI_ExecuteTactic] Tactic command can not be executed
Channel 100: [Bandit1::_AI_ExecuteTactic] START [Package Table: 10011], TacticID: [2]
Channel 100: [Bandit1::_AI_ExecuteTactic] [2][Target: 3] [Condition: 31] [USE ABILITY] [SHIELD_PUMMEL (1)] 
Channel 100: [Bandit1::_AI_ExecuteTactic] [Condition Valid for Target: 6] [Condition Base: 6] [Condition Parameter: 1] 
Channel 100: [Bandit1::_AI_IsAbilityValid] ERROR: Creature does not have ability: 1
Channel 100: [Bandit1::_AI_ExecuteTactic] Tactic command can not be executed
Channel 100: [Bandit1::AI_ExecuteDefaultAction] START
Got local actor DA2UE4Creature_C_0 from actor Bandit1
Channel 100: [Bandit1::AI_ExecuteDefaultAction] Could not assign any AI tactic - creature will try to attack normally


hit/miss related


Channel 800: [core_h.GetCreatureDefense] Defense on DA2UE4Creature_C_0 is 0.0 modifier is: 0.0
DA2UE4Creature_C_0 : Channel 1005: combat_h.GetAttackResult  ToHit Calculation: fAttack:54 = (fAttackRating: 4 fAttackRoll:50 (range penalty:0) fFlanking: 0 fBonus(script): 0). Attacker: Bandit1 
DA2UE4Creature_C_0 : Channel 1005: combat_h.GetAttackResult  ToHit Calculation (2):  fDefenseRating: 0 fCriticalHitModifier: 2 bThreatenCritical: 0. Attacker: Bandit1 
Channel 1004: [combat_damage]   fWeapon   DefaultWeapon_C_1: base: 7.0 + max: 10.5 max damage:0.0
Channel 1004: [combat_damage]   fWeapon   DefaultWeapon_C_1:10.321405 = 7.0 + Rand(3.5)
Channel 1004: [combat_damage]   fAr:  DefaultArmor_C_0:0.0 = 0.0 + Rand(0.0)
DA2UE4Creature_C_0 : Channel 1004: [combat_damage] Total: 10.321405 
Channel 1004: [combat_damage]   fStrength: 0.0
Channel 1004: [combat_damage]   fWeapon  : 10.321405
Channel 1004: [combat_damage]   fDmgBonus: 0.0
Channel 1004: [combat_damage]         fAr: 0.0
Channel 1004: [combat_damage]         fAp: 2.0
Channel 1004: [combat_damage]  fRankScale: 1.0
Getting property with ID: 7 and value 100 from actor DA2UE4Creature_C_0
Channel 1000: [combat_performattack] weapon base speed :2.0 mod:-0.1 effects: 1.0
DA2UE4Creature_C_0 : Channel 1005: combat_h.Combat_PerformAttack  Attack Result: 1 = COMMAND_RESULT_HIT. Attacker: Bandit1

in the original approach, I mostly copy/paste the code, and functionally it did the trick, that is it worked. But in terms of performance, it doesn’t work perfectly, as it has severe frame rate drops during high computation time, mostly behavior/condition etc.

So I decided to attack the problem using a more modern approach, that is using unreal editor behavior tree. I’m currently playing around with services and decorators, my goal is basically to do the same thing as the original creators but using a nice and functional behavior tree. I would most likely keep the same conditions check, but I’ll just split it in services and decorators, etc.

In its final form the behavior tree should include the AI evaluating things like:
-am I in exploring, combat, unknown game mode
-if in combat mode, I’ll evaluate tactics, and based on the result I’ll branch into either might or magic.
-if I go might, I’ll evaluate if I go melee or ranged
-if I go magic, I’ll evaluate if my target is self/ally/enemy
-Etc.

For some reason though, I currently hit the bug in behavior tree where the service that evaluates the game mode state, resets after 60 seconds from its default of exploring to unknown.

Anyways, for now I will continue to work on implementing a behavior tree as complete as possible, and add a comment here at some point.

On a side note, I’m wondering if there are other RPG enthusiasts that come across this thread, it would be nice to do some brainstorming…

I’ve been working on a conversation engine for this project. I eventually came up with a functional workflow and parser.
In its current form, the dialogues I written in the original Dragon Age Toolset.

The workflow:
-write the dialogue in the original toolset
-export to XML, convert to Json
-parse Json in unreal editor 4
-feed the parsed entries to the conversation engine
-based on the player choices the conversation flows and/or sets plot flags as needed.

Here is a snapshot.

Here is a full conversation test video. Uses the verbatim Demo Module conversation with the barkeep from the original toolset. the conversation was minimally modified to accommodate for the modern dialogue wheel, with different displayed/spoken words.
[video]DA2UE4 Conversation example - YouTube

Also please note, although I can provide more dry logs :), for the purpose of eye candy the proof of concept currently uses
-Dragon Age Inquisition UI*
-Dragon Age Origins 3-D models and mouse cursors*
-King’s Road backgrounds*

If you have any questions, please let me know

*Buy/play these games, their respective developers are awesome, and the games are a marvel!

Dragon Age Origins (with all DLCs, Addons, Mods, etc.) was the best RPG Game I’ve ever played! Excellent taste :smiley:

I implemented the area transition system. It follows Dragon Age Origins approach, that is an invisible wall object with two strings attached to it, one representing the area to transition to, and the other one the waypoint where the player character/party should be spawn to. when the party leader overlaps with the invisible wall and the game state is in Explore mode (not combat or dialogue for example) then the area transition kicks in.

There’s not much else to say, except when the mouse hovers over, the mouse cursor changes into the DAO area transition cursor. Which reminds me, I also implemented a mouse cursor system that currently includes the DAO standard cursor, also one for dialogue, one for area transition and one for melee attack. the mouse cursor system can be easily expanded with images and actions.

below an image that shows the area transition cursor when the mouse hovers over the door.

if you have any questions, just let me know

Thanks!

Few words about plots.

In the original Dragon age origins the plots are standalone scripts attached to objects similarly with Unity.
Unreal engine and C++ do not use similar patterns, so implementing plots required some sort of conversion mechanism.

Plots in Dragon age origins are basically a list of main and defined flags and the script itself handles getters and setters of said flags and the actions associated with them.

Now the porting to unreal engine and C++ was a bit tricky. This is the current approach:

  • plots in DAO have a resource ID and a GUID.
  • The plots resource ID is an integer that was used to track it in the DAO toolset SQL database
  • the plots GUID was used by the DAO engine to find the plots and its elements in the various custom resource files such as GFF or ERF files
  • in unreal engine I took the resource ID and I created a Json file with the plot flags that I named XXX.json, where the XXX is the resource ID
  • after that I took the GUID, converted it to string, FNV64 hash it into int64 (with very low hash collision) and used the new 64- bit integer into a switch table.
  • I created a C++ script to keep track of all the plots hash, and their flags.
  • I keep track of the flags by using the plot resource ID(int32) and appending the plot ID with three zeros in between so that I avoid plot flags ID collisions
  • for example let’s say that I have a plot with resource ID 12345.
  • The flags associated with this plot would be integers using the following form: 12345000X
  • so a main flag 1 would become 123450001 and a defined flag 256 would become 12345000256
  • in the switch table I keep track of the plots getters and setters and the actions associated with them
  • Plots are being saved and persistent when traveling between levels, so for example if I talk to the barkeep in level I and decline to take his sword quest, and traveled to level II and talk to the bandit and then come back to level I, the barkeep conversation starts from the declined quest branch, based on plot flags that are persistent between levels

And that’s pretty much all.

yay! I integrated some animations with the conversation system, such as idly weight shifting and hand gestures for emphasis during conversation.

it uses the same workflow as before: Dragon age origins toolset conversation plus animations exported to XML, converted to Json, parsed and played in unreal engine.

Now it’s time to look into some visuals for the combat system, such as weapons, shields and animations…

few words about creature properties. In Dragon Age Origins the characters have 59 properties saved in a GDA file, which is basically a binary CSV (comma separated values).
consequently the creature blueprint in Unreal Engine has 59 properties. Only a handful have to be exposed as blueprint read/write, all the other ones should be extrapolated by the engine based on certain criteria. Below you can see an image with only the exposed properties

as you can see some properties were converted from simple integers as used in the original Dragon Age Origins into enums. this way there is a more visual approach when assigning certain properties such as gender, race, class, etc. some properties although assigned as integers, they are meant to be used as Boolean, and I kept it as integers for consistency with DAO, their scripting language doesn’t use Boolean at all.
*The 3 PACKAGE related properties are used in evaluating combat tactics. **
All the properties are extrapolated from the DAO Toolset.

the properties have several types: attribute, simple, depletable, derived. This is the code that fills out the defaults for the properties, in particular the property ID which is an integer that matches the original integers from DAO scripts, and the properties type, via reflection


//property ID
	for (TFieldIterator<UStructProperty> It(GetClass()); It; ++It)
	{
		UStructProperty* StructProp = *It;
		FString sStruct = StructProp->GetFName().ToString();

		if (StructProp->Struct == FActorProperty::StaticStruct())
		{
			FString sID = GetM2DACustomString(TABLE_PROPERTIES, "ID", sStruct, "Stat");
			FActorProperty* actorProperty = StructProp->ContainerPtrToValuePtr<FActorProperty>(this);
			actorProperty->nPropertyID = static_cast<EActorPropertyID>(FCString::Atoi(*sID));
		}
	}

	//property type
	for (TFieldIterator<UStructProperty> It(GetClass()); It; ++It)
	{
		UStructProperty* StructProp = *It;
		FString sStruct = StructProp->GetFName().ToString();
		
		if(StructProp->Struct == FActorProperty::StaticStruct())
		{
			FActorProperty* actorProperty = StructProp->ContainerPtrToValuePtr<FActorProperty>(this);
			FString sType = GetM2DAString(TABLE_PROPERTIES, "Type", static_cast<uint8>(actorProperty->nPropertyID));

			if (sType == "ATTRIBUTE")
				actorProperty->nPropertyType = EActorPropertyType::ATTRIBUTE;
			else if (sType == "SIMPLE")
				actorProperty->nPropertyType = EActorPropertyType::SIMPLE;
			else if (sType == "DEPLETABLE")
				actorProperty->nPropertyType = EActorPropertyType::DEPLETABLE;
			else //derived 
				actorProperty->nPropertyType = EActorPropertyType::DERIVED;
		}
	}

also, this code is used to fill some default values for the nonexposed properties, in this example the main character attributes: strength, dexterity, willpower, magic, cunning, constitution


	int32 nClass = static_cast<uint8>(CLASS);
	int32 nRace = static_cast<uint8>(RACE);

	//baseline
	Health.fValueBase = Health.fValueTotal = Health.fValueCurrent = 85.f;
	Mana_Stamina.fValueBase = Mana_Stamina.fValueTotal = Mana_Stamina.fValueCurrent = 90.f;
	
	Strength.fValueBase = Strength.fValueTotal = Strength.fValueCurrent = 10;
	Dexterity.fValueBase = Dexterity.fValueTotal = Dexterity.fValueCurrent = 10;
	Willpower.fValueBase = Willpower.fValueTotal = Willpower.fValueCurrent = 10;
	Magic.fValueBase = Magic.fValueTotal = Magic.fValueCurrent = 10;
	Cunning.fValueBase = Cunning.fValueTotal = Cunning.fValueCurrent = 10;
	Constitution.fValueBase = Constitution.fValueTotal = Constitution.fValueCurrent = 10;
	
	BaseAttackRating.fValueBase = BaseAttackRating.fValueTotal = BaseAttackRating.fValueCurrent = 50;
	BaseDefenseRating.fValueBase = BaseDefenseRating.fValueTotal = BaseDefenseRating.fValueCurrent = 40;

	switch (nClass)
	{
	case CLASS_WARRIOR:
	{
		switch (nRace)
		{
		case RACE_HUMAN:
		{
			Health.fValueBase = Health.fValueTotal = Health.fValueCurrent += 15.f;
			Mana_Stamina.fValueBase = Mana_Stamina.fValueTotal = Mana_Stamina.fValueCurrent += 10.f;

			Strength.fValueBase = Strength.fValueTotal = Strength.fValueCurrent += 4;
			Dexterity.fValueBase = Dexterity.fValueTotal = Dexterity.fValueCurrent += 3;
			Willpower.fValueBase = Willpower.fValueTotal = Willpower.fValueCurrent += 0;
			Magic.fValueBase = Magic.fValueTotal = Magic.fValueCurrent += 0;
			Cunning.fValueBase = Cunning.fValueTotal = Cunning.fValueCurrent += 0;
			Constitution.fValueBase = Constitution.fValueTotal = Constitution.fValueCurrent += 3;

			BaseAttackRating.fValueBase = BaseAttackRating.fValueTotal = BaseAttackRating.fValueCurrent += 10;
			BaseDefenseRating.fValueBase = BaseDefenseRating.fValueTotal = BaseDefenseRating.fValueCurrent += 5;

			break;
		}
	}
	}

these default values are extrapolated from Dragon Age Origins wikia.

And that’s the basics for properties :slight_smile:

A recent milestone I worked on was inventory related, in particular connecting an item abstract object to its corresponding mesh if needed:
basically there are two major branches that require meshes in the world

  1. the head, eyes, hair and nude body (all skeletal meshes)
  2. armor and helmet (skeletal meshes), weapons and shields (static meshes)

In my tests I focused on Human, Male, Warrior, Sword and Shield. I was careful to make sure the pattern can be easily extended to include other races, genders, classes but my focus was only on the default I already mentioned.

One notable approach divergent from Dragon Age Origins, and instead based on Dragon Age Inquisition was to merge armor, gloves and boots into one encompassing mesh.
The other one also based on Dragon Age Inquisition is to scrap the main/alternate weapon sets and instead have only one, that can be melee, range and so on.

Equipping armor or weapons etc. follows the original Dragon Age Origins pattern, that is triggering EVENT_EVENT_TYPE_EQUIP, both the abstract object and its mesh when needed, an excerpt for example:


case EVENT_TYPE_EQUIP:
	{
		//TODO HandleEvent_Equip for Player
		AActor* oItem = GetEventObject(ev, 0);
		//int32 nEquipSlot = (GetEventInteger(ev, 0) == INVENTORY_SLOT_INVALID) ? GetItemSlot(GetBaseItemType(oItem)) : GetEventInteger(ev, 0);

		FItem* itemPtr = Inventory.FindByPredicate(
			&](const FItem& _item)
		{
			return _item.Item == oItem;
		});//DHK

		FItem item;
		if (itemPtr)
		{
			item = *itemPtr;
			INVENTORY_SLOTS.Add(item);

			//update mesh if needed
			if (GetItemSlot(GetBaseItemType(oItem)) == INVENTORY_SLOT_CHEST)
			{
				FString sPath = "/Game/Data/Art/Meshes/UTI";
				FString rItemFileName = GetLetterRace() + GetLetterGender() + "_" + item.ItemName.ToString() + "_0";

				const TCHAR *delim;
				delim = TEXT("_");
				TArray<FString> Parsed;
				rItemFileName.ParseIntoArray(Parsed, delim);

				for (int32 i = 0; i < Parsed.Num() - 1; i++)
				{
					FString s = Parsed*;
					if (GetPathFromPattern(s) != "")
						sPath = sPath + "/" + GetPathFromPattern(s);
				}
				sPath += "/";

				LoadedSkeletalMeshesDatabase->SkeletalMeshList[MESH_ARM].MeshResource = FStringAssetReference(sPath + rItemFileName + "." + rItemFileName);
				ChangeMeshARM();
			}

I tried my best to stay away from hard coding paths, so besides the root path, all the other objects path are extrapolated based on name pattern that closely follows Dragon Age Origins.

Below you can see the main player as a Human, Male, Warrior, Sword and Shield, and another NPC as a Commoner, without armor or weapons.

If you have any questions, please let me know.

Thanks!