Browse Source

Merge pull request #491 from snozbot/optimize-string-substitution

String substitution uses StringBuilder to avoid string allocations
master
Chris Gregan 9 years ago
parent
commit
10e76088ad
  1. 31
      Assets/Fungus/Flowchart/Scripts/Flowchart.cs
  2. 14
      Assets/Fungus/Narrative/Scripts/Localization.cs
  3. 21
      Assets/Fungus/Thirdparty/FungusLua/Scripts/LuaUtils.cs
  4. 58
      Assets/Fungus/Thirdparty/FungusLua/Scripts/StringSubstituter.cs
  5. 3
      Assets/Tests/StringSubstitution/StringSubstitutionTests.unity
  6. 5
      ProjectSettings/ProjectSettings.asset

31
Assets/Fungus/Flowchart/Scripts/Flowchart.cs

@ -2,6 +2,7 @@ using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
using UnityEngine.Serialization; using UnityEngine.Serialization;
using System; using System;
using System.Text;
using System.Linq; using System.Linq;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@ -968,15 +969,15 @@ namespace Fungus
* use the SubstituteVariables() method instead. * use the SubstituteVariables() method instead.
*/ */
[MoonSharp.Interpreter.MoonSharpHidden] [MoonSharp.Interpreter.MoonSharpHidden]
public virtual string SubstituteStrings(string input) public virtual bool SubstituteStrings(StringBuilder input)
{ {
string subbedText = input;
// Instantiate the regular expression object. // Instantiate the regular expression object.
Regex r = new Regex("{\\$.*?}"); Regex r = new Regex("{\\$.*?}");
bool modified = false;
// Match the regular expression pattern against a text string. // Match the regular expression pattern against a text string.
var results = r.Matches(input); var results = r.Matches(input.ToString());
foreach (Match match in results) foreach (Match match in results)
{ {
string key = match.Value.Substring(2, match.Value.Length - 3); string key = match.Value.Substring(2, match.Value.Length - 3);
@ -991,12 +992,14 @@ namespace Fungus
variable.key == key) variable.key == key)
{ {
string value = variable.ToString(); string value = variable.ToString();
subbedText = subbedText.Replace(match.Value, value); input.Replace(match.Value, value);
modified = true;
} }
} }
} }
return subbedText; return modified;
} }
/** /**
@ -1012,7 +1015,10 @@ namespace Fungus
stringSubstituer = new StringSubstituter(); stringSubstituer = new StringSubstituter();
} }
string subbedText = input; // Use the string builder from StringSubstituter for efficiency.
StringBuilder sb = stringSubstituer.stringBuilder;
sb.Length = 0;
sb.Append(input);
// Instantiate the regular expression object. // Instantiate the regular expression object.
Regex r = new Regex("{\\$.*?}"); Regex r = new Regex("{\\$.*?}");
@ -1033,13 +1039,20 @@ namespace Fungus
variable.key == key) variable.key == key)
{ {
string value = variable.ToString(); string value = variable.ToString();
subbedText = subbedText.Replace(match.Value, value); sb.Replace(match.Value, value);
} }
} }
} }
// Now do all other substitutions in the scene // Now do all other substitutions in the scene
return stringSubstituer.SubstituteStrings(subbedText); if (stringSubstituer.SubstituteStrings(sb))
{
return sb.ToString();
}
else
{
return input;
}
} }
} }

14
Assets/Fungus/Narrative/Scripts/Localization.cs

@ -6,6 +6,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Text;
using System.IO; using System.IO;
using Ideafixxxer.CsvParser; using Ideafixxxer.CsvParser;
@ -525,19 +526,19 @@ namespace Fungus
* Implementation of StringSubstituter.ISubstitutionHandler. * Implementation of StringSubstituter.ISubstitutionHandler.
* Relaces tokens of the form {$KeyName} with the localized value corresponding to that key. * Relaces tokens of the form {$KeyName} with the localized value corresponding to that key.
*/ */
public virtual string SubstituteStrings(string input) public virtual bool SubstituteStrings(StringBuilder input)
{ {
// This method could be called from the Start method of another component, so we // This method could be called from the Start method of another component, so we
// may need to initilize the localization system. // may need to initilize the localization system.
Init(); Init();
string subbedText = input;
// Instantiate the regular expression object. // Instantiate the regular expression object.
Regex r = new Regex("{\\$.*?}"); Regex r = new Regex("{\\$.*?}");
bool modified = false;
// Match the regular expression pattern against a text string. // Match the regular expression pattern against a text string.
var results = r.Matches(input); var results = r.Matches(input.ToString());
foreach (Match match in results) foreach (Match match in results)
{ {
string key = match.Value.Substring(2, match.Value.Length - 3); string key = match.Value.Substring(2, match.Value.Length - 3);
@ -546,11 +547,12 @@ namespace Fungus
string localizedString = Localization.GetLocalizedString(key); string localizedString = Localization.GetLocalizedString(key);
if (localizedString != null) if (localizedString != null)
{ {
subbedText = subbedText.Replace(match.Value, localizedString); input.Replace(match.Value, localizedString);
modified = true;
} }
} }
return subbedText; return modified;
} }
} }

21
Assets/Fungus/Thirdparty/FungusLua/Scripts/LuaUtils.cs vendored

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System; using System;
using System.Linq; using System.Linq;
using System.Text;
using System.Diagnostics; using System.Diagnostics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
@ -319,7 +320,7 @@ namespace Fungus
/// The string table value used depends on the currently loaded string table and active language. /// The string table value used depends on the currently loaded string table and active language.
/// </summary> /// </summary>
[MoonSharpHidden] [MoonSharpHidden]
public virtual string SubstituteStrings(string input) public virtual bool SubstituteStrings(StringBuilder input)
{ {
// This method could be called from the Start of another component, so // This method could be called from the Start of another component, so
// we need to ensure that the LuaEnvironment has been initialized. // we need to ensure that the LuaEnvironment has been initialized.
@ -335,24 +336,24 @@ namespace Fungus
if (luaEnvironment == null) if (luaEnvironment == null)
{ {
UnityEngine.Debug.LogError("No Lua Environment found"); UnityEngine.Debug.LogError("No Lua Environment found");
return input; return false;
} }
if (luaEnvironment.Interpreter == null) if (luaEnvironment.Interpreter == null)
{ {
UnityEngine.Debug.LogError("No Lua interpreter found"); UnityEngine.Debug.LogError("No Lua interpreter found");
return input; return false;
} }
MoonSharp.Interpreter.Script interpreter = luaEnvironment.Interpreter; MoonSharp.Interpreter.Script interpreter = luaEnvironment.Interpreter;
string subbedText = input;
// Instantiate the regular expression object. // Instantiate the regular expression object.
Regex r = new Regex("\\{\\$.*?\\}"); Regex r = new Regex("\\{\\$.*?\\}");
bool modified = false;
// Match the regular expression pattern against a text string. // Match the regular expression pattern against a text string.
var results = r.Matches(input); var results = r.Matches(input.ToString());
foreach (Match match in results) foreach (Match match in results)
{ {
string key = match.Value.Substring(2, match.Value.Length - 3); string key = match.Value.Substring(2, match.Value.Length - 3);
@ -366,7 +367,8 @@ namespace Fungus
DynValue languageEntry = stringTableVar.Table.Get(activeLanguage); DynValue languageEntry = stringTableVar.Table.Get(activeLanguage);
if (languageEntry.Type == DataType.String) if (languageEntry.Type == DataType.String)
{ {
subbedText = subbedText.Replace(match.Value, languageEntry.String); input.Replace(match.Value, languageEntry.String);
modified = true;
} }
continue; continue;
} }
@ -376,12 +378,13 @@ namespace Fungus
DynValue globalVar = interpreter.Globals.Get(key); DynValue globalVar = interpreter.Globals.Get(key);
if (globalVar.Type != DataType.Nil) if (globalVar.Type != DataType.Nil)
{ {
subbedText = subbedText.Replace(match.Value, globalVar.ToPrintString()); input.Replace(match.Value, globalVar.ToPrintString());
modified = true;
continue; continue;
} }
} }
return subbedText; return modified;
} }
/// <summary> /// <summary>

58
Assets/Fungus/Thirdparty/FungusLua/Scripts/StringSubstituter.cs vendored

@ -17,21 +17,33 @@ namespace Fungus
public interface ISubstitutionHandler public interface ISubstitutionHandler
{ {
/// <summary> /// <summary>
/// Returns a new string with tokens replaced by subsituted values. /// Modifies a StringBuilder so that tokens are replaced by subsituted values.
/// It's up to clients how to implement substitution but the convention looks like: /// It's up to clients how to implement substitution but the convention looks like:
/// "Hi {$VarName}" => "Hi John" where VarName == "John" /// "Hi {$VarName}" => "Hi John" where VarName == "John"
/// <returns>True if the input was modified</returns>
/// </summary> /// </summary>
string SubstituteStrings(string input); bool SubstituteStrings(StringBuilder input);
} }
protected List<ISubstitutionHandler> substitutionHandlers = new List<ISubstitutionHandler>(); protected List<ISubstitutionHandler> substitutionHandlers = new List<ISubstitutionHandler>();
/**
* The StringBuilder instance used to substitute strings optimally.
* This property is public to support client code optimisations.
*/
public StringBuilder stringBuilder;
private int recursionDepth;
/// <summary> /// <summary>
/// Constructor which caches all components in the scene that implement ISubstitutionHandler. /// Constructor which caches all components in the scene that implement ISubstitutionHandler.
/// <param name="recursionDepth">Number of levels of recursively embedded keys to resolve.</param>
/// </summary> /// </summary>
public StringSubstituter() public StringSubstituter(int recursionDepth = 5)
{ {
CacheSubstitutionHandlers(); CacheSubstitutionHandlers();
stringBuilder = new StringBuilder(1024);
this.recursionDepth = recursionDepth;
} }
/// <summary> /// <summary>
@ -64,28 +76,48 @@ namespace Fungus
/// </summary> /// </summary>
public virtual string SubstituteStrings(string input) public virtual string SubstituteStrings(string input)
{ {
string newString = input; stringBuilder.Length = 0;
stringBuilder.Append(input);
if (SubstituteStrings(stringBuilder))
{
return stringBuilder.ToString();
}
else
{
return input; // String wasn't modified
}
}
public virtual bool SubstituteStrings(StringBuilder input)
{
bool result = false;
// Perform the substitution multiple times to expand nested keys // Perform the substitution multiple times to expand nested keys
int lasthash = 0;
int currenthash = -1;
int loopCount = 0; // Avoid infinite recursion loops int loopCount = 0; // Avoid infinite recursion loops
while (loopCount < recursionDepth)
while (lasthash != currenthash &&
loopCount < 5)
{ {
lasthash = newString.GetHashCode(); bool modified = false;
foreach (ISubstitutionHandler handler in substitutionHandlers) foreach (ISubstitutionHandler handler in substitutionHandlers)
{ {
newString = handler.SubstituteStrings(newString); if (handler.SubstituteStrings(input))
{
modified = true;
result = true;
}
}
if (!modified)
{
break;
} }
currenthash = newString.GetHashCode();
loopCount++; loopCount++;
} }
return newString; return result;
} }
} }
} }

3
Assets/Tests/StringSubstitution/StringSubstitutionTests.unity

@ -1427,6 +1427,7 @@ MonoBehaviour:
- Fungus.Command, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - Fungus.Command, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
- Fungus.CommandInfoAttribute, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, - Fungus.CommandInfoAttribute, Assembly-CSharp, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null PublicKeyToken=null
- System.Text.StringBuilder, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- UnityEngine.Rect, UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - UnityEngine.Rect, UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
boundObjects: boundObjects:
- key: text - key: text
@ -1592,7 +1593,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 1 m_IsActive: 0
--- !u!114 &1475498171 --- !u!114 &1475498171
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

5
ProjectSettings/ProjectSettings.asset

@ -117,6 +117,8 @@ PlayerSettings:
m_Bits: 238 m_Bits: 238
iPhoneSdkVersion: 988 iPhoneSdkVersion: 988
iPhoneTargetOSVersion: 22 iPhoneTargetOSVersion: 22
tvOSSdkVersion: 0
tvOSTargetOSVersion: 900
uIPrerenderedIcon: 0 uIPrerenderedIcon: 0
uIRequiresPersistentWiFi: 0 uIRequiresPersistentWiFi: 0
uIRequiresFullScreen: 1 uIRequiresFullScreen: 1
@ -199,6 +201,7 @@ PlayerSettings:
wiiUSystemHeapSize: 128 wiiUSystemHeapSize: 128
wiiUTVStartupScreen: {fileID: 0} wiiUTVStartupScreen: {fileID: 0}
wiiUGamePadStartupScreen: {fileID: 0} wiiUGamePadStartupScreen: {fileID: 0}
wiiUDrcBufferDisabled: 0
wiiUProfilerLibPath: wiiUProfilerLibPath:
actionOnDotNetUnhandledException: 1 actionOnDotNetUnhandledException: 1
enableInternalProfiler: 0 enableInternalProfiler: 0
@ -266,6 +269,7 @@ PlayerSettings:
ps4DownloadDataSize: 0 ps4DownloadDataSize: 0
ps4GarlicHeapSize: 2048 ps4GarlicHeapSize: 2048
ps4Passcode: 5PN2qmWqBlQ9wQj99nsQzldVI5ZuGXbE ps4Passcode: 5PN2qmWqBlQ9wQj99nsQzldVI5ZuGXbE
ps4UseDebugIl2cppLibs: 0
ps4pnSessions: 1 ps4pnSessions: 1
ps4pnPresence: 1 ps4pnPresence: 1
ps4pnFriends: 1 ps4pnFriends: 1
@ -331,6 +335,7 @@ PlayerSettings:
psp2UseLibLocation: 0 psp2UseLibLocation: 0
psp2InfoBarOnStartup: 0 psp2InfoBarOnStartup: 0
psp2InfoBarColor: 0 psp2InfoBarColor: 0
psp2UseDebugIl2cppLibs: 0
psmSplashimage: {fileID: 0} psmSplashimage: {fileID: 0}
spritePackerPolicy: spritePackerPolicy:
scriptingDefineSymbols: {} scriptingDefineSymbols: {}

Loading…
Cancel
Save