Skip to content

Instantly share code, notes, and snippets.

@jmcguirk
Last active December 15, 2015 05:29
Show Gist options
  • Select an option

  • Save jmcguirk/5208912 to your computer and use it in GitHub Desktop.

Select an option

Save jmcguirk/5208912 to your computer and use it in GitHub Desktop.
JSON Based Content Engine for Unity 3D.
Allows for
- Content reuse between client and server
- Git friendly merging
- Better expressiveness w/ Inheritance
Overview:
- JSON Entity Templates exist under a Resources/Data directory. One template per file. These are imported as text assets into the project
- IDE Menu Item and Build server parse out templates directory, blow out inheritance chain and create prefabs, one per template. Inheritance follows following rules: Graph subtrees are merged down to primitives, Child wins on conflict, Arrays are unmerged and assert only.
- Git ignore prevents these prefabs from being checked in
- Prefab components are semi-automagically configured with reflection (Primitives only).
- Dummy component to flag GameObjects as having originated from our content system.
- Prefabs are propped up into game objects on demand when a new Entity of a given template is requested.
Possible Future Work:
- Probably need to invest in an export feature to allow for content tuning within the IDE via the inspector. By configuring components via external files we're missing out on all the goodness of tweaking content within the U3D ide.
- Need to think of a way to roll these up into discrete asset bundles rather than just tossing all the prefabs into the main build.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
public class BaseComponent : MonoBehaviour
{
// Use this for initialization
void Start() {
}
// Update is called once per frame
void Update() {
}
/// <summary>
/// Initializes this component with configuration data
/// </summary>
/// <param name="entityName">Name of the entity.</param>
/// <param name="componentData">The component data.</param>
public virtual void InitializeComponent(string entityName, Dictionary<string, object> componentData) {
foreach (PropertyInfo propertyInfo in this.GetType().GetProperties()) {
if (propertyInfo.CanWrite) {
if (componentData.ContainsKey(propertyInfo.Name) && Util.isPrimitive(componentData[propertyInfo.Name])) {
propertyInfo.SetValue(this, componentData[propertyInfo.Name], null);
}
}
}
foreach (FieldInfo fieldInfo in this.GetType().GetFields()) {
if (componentData.ContainsKey(fieldInfo.Name) && Util.isPrimitive(componentData[fieldInfo.Name])) {
fieldInfo.SetValue(this, componentData[fieldInfo.Name]);
}
}
}
/// <summary>
/// Called when this component is attached to a new entity
/// </summary>
public virtual void OnNewEntity() {
}
/// <summary>
/// Describes this component.
/// </summary>
public virtual void Describe() {
GameLog.debug("BASE COMPONENT OVERRIDE ME PLEASE :)");
}
}
{
"name": "e_card_base",
"inherits": "e_entity_base",
"components":{
"LevelUp":{
"BaseXpBetweenLevels" : 10,
"MaxLevel" : 50,
"CurrLevelMultiplier": 1.2
},
"Action":{
"Abilities":{
"e_ability_basic_attack" : {
"priority" : 1,
"probability" : 1
}
}
},
"Icon":{
"Small" : "icon_missing.png",
"Large" : "icon_missing_large.png",
"ClassIcon" : "MedicIcon"
},
"Animation":{
"Rig":"humanRig",
"Version": 1,
"RigScaleX" : 0.11,
"RigScaleY" : 0.11,
"RigPivotX": -40,
"RigPivotY": 310,
"RigDirection" : -1.0,
"IdleState": "idle",
"AttackState": "attack",
"HurtState": "hurt",
"HurtIdleState": "idle-death",
"DeadState": "die",
"Items":{
},
"RigFrames":{
"stand": 0,
"slash": 24
}
},
"Statistics": {
"RowPreference" : "FRONT",
"PhysicalAttackDamageVariance" : 0.2,
"PhysicalAttackDamagePerStrength" : 1.6,
"Rarity": 1
},
"Upgrade": {
"Timers": [0,30,45,60,75,100,115,130,145]
}
},
"tags":["abstract"]
}
{
"name": "e_wizard_base",
"inherits": "e_card_base",
"components":{
"LevelUp":{
"MaxLevel" : 60
},
"Icon":{
"ClassIcon" : "MagicWandIcon"
},
"Statistics": {
"RowPreference" : "BACK",
"BaseAgility" : 6,
"BaseStrength" : 2,
"BaseDefense" : 3,
"BaseIntelligence" : 9,
"BaseLuck" : 5,
"BaseMaxHitpoints" : 40,
"AgilityPerLevel" : 1,
"DefensePerLevel" : 1,
"StrengthPerLevel" : 1,
"IntelligencePerLevel" : 4,
"MaxHitpointsPerLevel" : 8,
"LuckPerLevel" : 2
},
"Animation":{
"Rig":"humanRig",
"Version": 2,
"SkinName": "wizard",
"RigPivotX": -40,
"RigPivotY": 310,
"RigDirection" : -1.0,
"IdleState": "idle",
"AttackState": "attack",
"HurtState": "hurt",
"HurtIdleState": "idle-death",
"DeadState": "die",
"Items":{
},
"RigFrames":{
"stand": 0,
"slash": 24
}
}
},
"tags":["wizard"]
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
public delegate void TemplatesLoadedHandler();
public class EntityTemplateManager
{
public event TemplatesLoadedHandler onTemplatesLoaded;
/// <summary>
/// Preloads a bunch of prefabs
/// </summary>
public void loadTemplatesFromPrefab() {
GameObject go = this.createEntityByName("e_card_base");
if (go != null) {
go.Describe();
this.entityLoadComplete();
} else {
GameLog.fatal("Can't get base entity. Have you generated the entities yet? Honorbound -> Generate Entities");
}
}
/// <summary>
/// Creates the name of the entity by the given name
/// </summary>
/// <param name="entityName">Name of the entity.</param>
/// <returns></returns>
public GameObject createEntityByName(string entityName) {
GameObject result = (GameObject)Resources.Load("Data/Templates/Prefab/" + entityName);
GameObject.DontDestroyOnLoad(result);
Component[] baseComponents = result.GameComponents();
for (var i = 0; i < baseComponents.Length; i++) {
(baseComponents[i] as BaseComponent).OnNewEntity();
}
return result;
}
protected void entityLoadComplete() {
if (onTemplatesLoaded != null) {
onTemplatesLoaded();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
/// <summary>
/// Creates prefabs from configuration data
/// </summary>
public class EntityTemplatePrefabFactory
{
protected Dictionary<string, Dictionary<string, object>> m_rawEntities;
protected Dictionary<string, Dictionary<string, object>> m_flattenedEntities;
/// <summary>
/// Loads the templates from config, turning them into prefabs
/// </summary>
public void loadTemplatesFromConfig() {
Debug.Log("Parsing templates from config\n------------------\n------------------");
AssetDatabase.DeleteAsset("Assets/Resources/Data/Templates/Prefab");
AssetDatabase.CreateFolder("Assets/Resources/Data/Templates", "Prefab");
m_rawEntities = new Dictionary<string, Dictionary<string, object>>();
m_flattenedEntities = new Dictionary<string, Dictionary<string, object>>();
UnityEngine.Object[] textAssets = Resources.LoadAll("Data/Templates");
Queue<Dictionary<string, object>> queue = new Queue<Dictionary<string, object>>();
for (int i = 0; i < textAssets.Length; i++) {
string[] fileNameParts = textAssets[i].name.Split('.');
if (fileNameParts.Length == 2) {
string fileName = fileNameParts[1];
if (fileName == "json") {
string entityName = textAssets[i].name.Split('.')[0];
Dictionary<string, object> data = Json.Deserialize(textAssets[i].ToString()) as Dictionary<string, object>;
m_rawEntities[entityName] = data;
queue.Enqueue(data);
}
}
}
while (queue.Count > 0) {
Dictionary<string, object> next = queue.Dequeue();
if (!next.ContainsKey("inherits")) {
addFlattenedEntity(next);
} else {
string inheritsString = next["inherits"] as string;
if (inheritsString == null || inheritsString == "") {
addFlattenedEntity(next);
}
if (m_flattenedEntities.ContainsKey(next["inherits"] as string)) {
recursiveMerge(m_flattenedEntities[next["inherits"] as string], next);
addFlattenedEntity(next);
} else {
queue.Enqueue(next); // Requeue data since we don't have our parent
}
}
}
this.checkMergedEntities();
GameLog.debug("Finished processing total templates " + m_flattenedEntities.Keys.Count + " total entities");
this.createPrefabs();
}
/// <summary>
/// Creates prefabs from parsed entity templates
/// </summary>
protected void createPrefabs() {
foreach (KeyValuePair<string, Dictionary<string, object>> entry in m_flattenedEntities) {
string entityName = entry.Key;
Dictionary<string, object> entityData = entry.Value;
createEntityPrefab(entityName, entityData);
}
}
/// <summary>
/// Creates a single entity prefab from the given content
/// </summary>
/// <param name="entityName">Name of the entity.</param>
/// <param name="entityData">The entity data.</param>
protected void createEntityPrefab(string entityName, Dictionary<string, object> entityData) {
GameObject entity = new GameObject("temp");
try {
initializeEntityTemplate(entity, entityName, entityData);
UnityEngine.Object tempPrefab = PrefabUtility.CreateEmptyPrefab("Assets/Resources/Data/Templates/Prefab/" + entityName + ".prefab");
PrefabUtility.ReplacePrefab(entity, tempPrefab, ReplacePrefabOptions.Default);
} catch (Exception ex) {
throw ex;
} finally {
UnityEngine.Object.DestroyImmediate(entity); // Remove it from the scene
}
}
/**
* Initialize an entity template with relevant components
*/
protected void initializeEntityTemplate(GameObject entity, string entityName, Dictionary<string, object> entityData) {
entity.AddComponent("HonorboundGameObjectComponent");
entity.GetComponent<HonorboundGameObjectComponent>().InitializeComponent(entityName, entityData); // Init app specific game object component
if (entityData.ContainsKey("components")) {
Dictionary<string, object> components = entityData["components"] as Dictionary<string, object>;
foreach (KeyValuePair<string, object> entry in components) {
string componentName = entry.Key + "Component";
entity.AddComponent(componentName);
BaseComponent baseComponent = (entity.GetComponent(componentName) as BaseComponent);
if(baseComponent != null){
baseComponent.InitializeComponent(entityName, entry.Value as Dictionary<string, object>);
}
}
}
}
/// <summary>
/// Spot checks merged entities
/// </summary>
protected void checkMergedEntities() {
//Dictionary<string, object> dwarf = m_flattenedEntities["e_dwarf_warrior"];
//GameLog.debug(Json.Serialize(dwarf));
}
/// <summary>
/// Performs a logical merge of two subtrees
/// </summary>
/// <param name="left">The left subtree this takes precedence over right.</param>
/// <param name="right">The right subtree.</param>
protected void recursiveMerge(Dictionary<string, object> left, Dictionary<string, object> right) {
foreach (KeyValuePair<string, object> entry in left) {
if (!right.ContainsKey(entry.Key)) {
if (Util.isPrimitive(entry.Value)) {
right[entry.Key] = entry.Value;
} else if (entry.Value is List<object>) {
right[entry.Key] = (entry.Value as List<object>).ToList<object>();
} else if (entry.Value is Dictionary<string, object>) {
right[entry.Key] = CloneDictionaryCloningValues(entry.Value as Dictionary<string, object>);
} else {
if (entry.Value == null) {
GameLog.fatal("NULL key " + entry.Key);
} else {
GameLog.fatal("Uncopyable key " + entry.Key + " " + entry.Value + " (" + entry.Value.GetType() + ")");
}
}
} else {
if (!Util.isPrimitive(entry.Value) && !Util.isPrimitive(right[entry.Key]) && !(entry.Value is List<object>) && !(right[entry.Key] is List<object>)) {
//GameLog.fatal("Recursing " + entry.Key);
//GameLog.fatal("Left Type " + entry.Value.GetType());
//GameLog.fatal("Right Type " + right[entry.Key].GetType());
recursiveMerge(entry.Value as Dictionary<string, object>, right[entry.Key] as Dictionary<string, object>);
}
}
}
}
/// <summary>
/// Recursively deep clones a dictionary
/// </summary>
/// <param name="original">The original.</param>
/// <returns></returns>
public static Dictionary<string, object> CloneDictionaryCloningValues(Dictionary<string, object> original) {
Dictionary<string, object> ret = new Dictionary<string, object>(original.Count,
original.Comparer);
foreach (KeyValuePair<string, object> entry in original) {
if (entry.Value is Dictionary<string, object>) {
ret.Add(entry.Key, (object)CloneDictionaryCloningValues(entry.Value as Dictionary<string, object>));
} else if (entry.Value is ICloneable) {
ret.Add(entry.Key, (object)((ICloneable)entry.Value).Clone());
} else {
ret.Add(entry.Key, entry.Value);
}
}
return ret;
}
/// <summary>
/// Adds a flattened entity.
/// </summary>
/// <param name="rawData">The raw data.</param>
protected void addFlattenedEntity(Dictionary<string, object> rawData) {
m_flattenedEntities[rawData["name"] as string] = rawData;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
/// <summary>
/// Collection of utility extensions
/// </summary>
public static class GameObjectExtensions
{
/// <summary>
/// Determines whether the specified game object is a entity.
/// </summary>
/// <param name="go">The game object.</param>
/// <returns>
/// <c>true</c> if the specified go is entity; otherwise, <c>false</c>.
/// </returns>
public static bool IsEntity(this GameObject go) {
return go.GetComponent("HonorboundGameObjectComponent") != null;
}
/// <summary>
/// Returns the entity id for this object
/// </summary>
/// <param name="go">The go.</param>
/// <returns></returns>
public static string GetEntityId(this GameObject go) {
return go.GetComponent<HonorboundGameObjectComponent>().EntityId;
}
/// <summary>
/// Gets the entity template id for this object
/// </summary>
/// <param name="go">The game object.</param>
/// <returns></returns>
public static string GetEntityTemplateId(this GameObject go) {
return go.GetComponent<HonorboundGameObjectComponent>().EntityTemplateId;
}
/// <summary>
/// Gets the animation component.
/// </summary>
/// <param name="go">The game object.</param>
/// <returns></returns>
public static AnimationComponent GetAnimation(this GameObject go) {
return go.GetComponent<AnimationComponent>();
}
/// <summary>
/// Gets the stats component
/// </summary>
/// <param name="go">The game object.</param>
/// <returns></returns>
public static StatisticsComponent GetStats(this GameObject go) {
return go.GetComponent<StatisticsComponent>();
}
/// <summary>
/// Returns collection of base game components
/// </summary>
/// <param name="go">The game object.</param>
/// <returns></returns>
public static Component[] GameComponents(this GameObject go) {
return go.GetComponents(typeof(BaseComponent));
}
/// <summary>
/// Describes the specified game object
/// </summary>
/// <param name="go">The game object.</param>
public static void Describe(this GameObject go) {
if (go.IsEntity()) {
GameLog.debug("===================================");
GameLog.debug("Game Entity Object " + go.GetEntityId());
GameLog.debug("===================================");
GameLog.debug("Template " + go.GetEntityTemplateId());
GameLog.debug("");
GameLog.debug("Components");
Component[] componentList = go.GetComponents(typeof(BaseComponent));
for (var i = 0; i < componentList.Length; i++) {
GameLog.debug("");
(componentList[i] as BaseComponent).Describe();
GameLog.debug("");
}
} else {
GameLog.debug("Not a content object");
}
}
}
using UnityEngine;
using UnityEditor;
using System.Collections;
public class HonorboundEditorMenu : Editor
{
[MenuItem("Honorbound/Generate Entities")]
public static void GenerateEntities() {
EntityTemplatePrefabFactory factory = new EntityTemplatePrefabFactory();
factory.loadTemplatesFromConfig();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
public class BaseComponent : MonoBehaviour
{
// Use this for initialization
void Start() {
}
// Update is called once per frame
void Update() {
}
/// <summary>
/// Initializes this component with configuration data
/// </summary>
/// <param name="entityName">Name of the entity.</param>
/// <param name="componentData">The component data.</param>
public virtual void InitializeComponent(string entityName, Dictionary<string, object> componentData) {
foreach (PropertyInfo propertyInfo in this.GetType().GetProperties()) {
if (propertyInfo.CanWrite) {
if (componentData.ContainsKey(propertyInfo.Name) && Util.isPrimitive(componentData[propertyInfo.Name])) {
propertyInfo.SetValue(this, componentData[propertyInfo.Name], null);
}
}
}
foreach (FieldInfo fieldInfo in this.GetType().GetFields()) {
if (componentData.ContainsKey(fieldInfo.Name) && Util.isPrimitive(componentData[fieldInfo.Name])) {
fieldInfo.SetValue(this, componentData[fieldInfo.Name]);
}
}
}
/// <summary>
/// Called when this component is attached to a new entity
/// </summary>
public virtual void OnNewEntity() {
}
/// <summary>
/// Describes this component.
/// </summary>
public virtual void Describe() {
GameLog.debug("BASE COMPONENT OVERRIDE ME PLEASE :)");
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
/// <summary>
/// Collection of utility extensions for all objects
/// </summary>
public static class ObjectExtensions
{
public static void DescribeFields(this UnityEngine.Object obj) {
foreach (FieldInfo fieldInfo in obj.GetType().GetFields()) {
GameLog.debug(" " + fieldInfo.Name + " : " + fieldInfo.GetValue(obj));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment