16 KiB
C++ to C# Unity String Format Conversion Guide
Overview
This guide explains how to convert the C++ string formatting pattern used in Perfect World Online to C# Unity.
The Original C++ Code
From DlgSkillSubListItem.cpp, line 290:
int needMoney = CECHostSkillModel::Instance().GetSkillMoney(m_skillID, m_curLevel + 1);
int needSp = CECHostSkillModel::Instance().GetSkillSp(m_skillID, m_curLevel + 1);
ACString str;
str.Format(GetStringFromTable(11326), needMoney, needSp);
GetGameUIMan()->MessageBox("Game_LearnSkill", str, MB_OKCANCEL, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox);
pMsgBox->SetData(m_skillID);
What This Code Does:
- Gets skill upgrade costs: Retrieves the money and skill points needed for the next level
- Formats localized string: Uses
GetStringFromTable(11326)to get a template string, then formats it with the cost values - Shows confirmation dialog: Displays a message box with OK/Cancel buttons
- Attaches data: Associates the skill ID with the dialog
The C# Unity Conversion
From CDlgSkillSubListItem.cs:
// Get the required money and skill points for the next level
int needMoney = CECHostSkillModel.Instance.GetSkillMoney(m_skillID, m_curLevel + 1);
int needSp = CECHostSkillModel.Instance.GetSkillSp(m_skillID, m_curLevel + 1);
// Format the confirmation message string
// C++ equivalent: str.Format(GetStringFromTable(11326), needMoney, needSp);
string confirmMessage = string.Format(GetStringFromTable(11326), needMoney, needSp);
// Show confirmation dialog with OK/Cancel buttons
uiManager.ShowConfirmDialog(
"Game_LearnSkill",
confirmMessage,
onConfirm: () => {
// Player confirmed - learn the skill
CECHostSkillModel.Instance.LearnSkill(m_skillID);
},
onCancel: null,
skillData: m_skillID
);
Key Conversion Patterns
1. String Formatting
C++ (ACString):
ACString str;
str.Format(GetStringFromTable(11326), needMoney, needSp);
C#:
Method A: Using AUIDialog.FormatPrintf (Recommended for Legacy Strings)
If the string from the table uses C++ format specifiers (%d, %s, %f), AUIDialog.FormatPrintf handles them natively without requiring string conversion.
string confirmMessage = AUIDialog.FormatPrintf(GetStringFromTable(11326), needMoney, needSp);
Method B: Using string.Format (Standard C#)
If the string table has been updated to use C# format specifiers ({0}, {1}), you can use the built-in string.Format.
string confirmMessage = string.Format(GetStringFromTable(11326), needMoney, needSp);
Notes:
- C++'s
ACString::Format()is a member function. - C#'s
string.Format()is a static function but only understands{0},{1}syntax. - If your string table still uses C++ format specifiers (
%d,%s,%f), you should useAUIDialog.FormatPrintf. Otherwise, you would need to manually convert the placeholders in the string table:%d→{0},{1}for integers%s→{0},{1}for strings%f→{0},{1}for floats
2. Message Box with Callbacks
C++ (Pointer-based):
PAUIDIALOG pMsgBox;
GetGameUIMan()->MessageBox("Game_LearnSkill", str, MB_OKCANCEL,
A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox);
pMsgBox->SetData(m_skillID);
// Later, in message handler:
void OnMessageBox_OK(const char* szName) {
if (strcmp(szName, "Game_LearnSkill") == 0) {
PAUIDIALOG pMsgBox = GetGameUIMan()->GetDialog(szName);
int skillID = pMsgBox->GetData();
// Learn skill...
}
}
C# (Callback-based):
uiManager.ShowConfirmDialog(
"Game_LearnSkill",
confirmMessage,
onConfirm: () => {
// This code runs when player clicks OK
CECHostSkillModel.Instance.LearnSkill(m_skillID);
},
onCancel: () => {
// This code runs when player clicks Cancel (optional)
},
skillData: m_skillID
);
Key Differences:
- C++ uses pointers and manual dialog lookup
- C# uses lambda callbacks (closures) that capture variables
- C# is more straightforward - no need to check dialog names or retrieve data manually
3. Localized String Table
Both versions use the same approach:
// Get localized string template from string table
// String ID 11326 might contain: "学习此技能需要 {0} 金钱和 {1} 技能点,确定学习吗?"
// English: "Learning this skill requires {0} money and {1} skill points, confirm?"
string template = GetStringFromTable(11326);
// Format with actual values
string message = string.Format(template, needMoney, needSp);
// Result: "Learning this skill requires 2480 money and 34200 skill points, confirm?"
Complete Example Comparison
C++ Version (Original)
void CDlgSkillSubListItem::OnCommand_Upgrade(const char* szCommand) {
CECHostPlayer* player = GetHostPlayer();
PAUIDIALOG pMsgBox;
// Check player state...
if (player->IsDead() || player->IsSitting() || /* ... */) {
GetGameUIMan()->MessageBox("", GetGameUIMan()->GetStringFromTable(11327),
MB_OK, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox);
return;
}
// Check learning conditions...
int nCondition = CECHostSkillModel::Instance().CheckLearnCondition(m_skillID);
int nCheckCode = 0;
if (1 == nCondition) {
nCheckCode = 270;
} else if (6 == nCondition) {
nCheckCode = 527;
}
// ... more conditions ...
if (nCheckCode == 0) {
// Show confirmation dialog
int needMoney = CECHostSkillModel::Instance().GetSkillMoney(m_skillID, m_curLevel + 1);
int needSp = CECHostSkillModel::Instance().GetSkillSp(m_skillID, m_curLevel + 1);
ACString str;
str.Format(GetStringFromTable(11326), needMoney, needSp);
GetGameUIMan()->MessageBox("Game_LearnSkill", str, MB_OKCANCEL,
A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox);
pMsgBox->SetData(m_skillID);
if (!GetGameUIMan()->m_pDlgSkillAction->IsReceivedNPCGreeting()) {
CECHostSkillModel::Instance().SendHelloToSkillLearnNPC();
}
} else {
// Show error message
GetGameUIMan()->MessageBox("", GetGameUIMan()->GetStringFromTable(nCheckCode),
MB_OK, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox);
pMsgBox->SetLife(3);
}
}
// Separate message handler
void CDlgSkillSubListItem::OnMessageBox_OK(const char* szName) {
if (strcmp(szName, "Game_LearnSkill") == 0) {
PAUIDIALOG pMsgBox = GetGameUIMan()->GetDialog(szName);
int skillID = pMsgBox->GetData();
// Send learn skill command to server
CECHostSkillModel::Instance().LearnSkill(skillID);
}
}
C# Unity Version (Converted)
private void OnCommand_Upgrade()
{
CECHostPlayer player = GetHostPlayer();
if (player == null)
{
return;
}
var uiManager = CECUIManager.Instance;
var gameUIMan = uiManager.GetInGameUIMan();
// Check player state...
if (player.IsDead() ||
player.IsSitting() ||
player.IsChangingFace() ||
player.IsTrading() ||
player.GetBoothState() != 0 ||
player.IsRooting() ||
player.IsHangerOn() ||
player.GetCurSkill() != null ||
player.IsFighting())
{
uiManager.ShowMessageBox("MessageBox", gameUIMan.GetStringFromTable(11327));
return;
}
// Check learning conditions...
int nCondition = CECHostSkillModel.Instance.CheckLearnCondition(m_skillID);
int nCheckCode = 0;
if (1 == nCondition)
{
nCheckCode = 270;
}
else if (6 == nCondition)
{
nCheckCode = 527;
}
// ... more conditions ...
if (nCheckCode == 0)
{
// Get the required costs
int needMoney = CECHostSkillModel.Instance.GetSkillMoney(m_skillID, m_curLevel + 1);
int needSp = CECHostSkillModel.Instance.GetSkillSp(m_skillID, m_curLevel + 1);
// Format the confirmation message
string confirmMessage = string.Format(GetStringFromTable(11326), needMoney, needSp);
// Show confirmation dialog with inline callback
uiManager.ShowConfirmDialog(
"Game_LearnSkill",
confirmMessage,
onConfirm: () => {
// This runs when player clicks OK
// No need to look up dialog or retrieve data - it's captured in closure
CECHostSkillModel.Instance.LearnSkill(m_skillID);
},
onCancel: null,
skillData: m_skillID
);
// 如果打开对话框时NPC发送的Hello消息没有收到回复,再次发送
// If the NPC Hello message hasn't been received when opening dialog, send again
if (!gameUIMan.IsReceivedNPCGreeting())
{
CECHostSkillModel.Instance.SendHelloToSkillLearnNPC();
}
}
else
{
// Show error message with auto-close after 3 seconds
uiManager.ShowMessageBox("", gameUIMan.GetStringFromTable(nCheckCode), autoCloseTime: 3f);
}
}
String Format Examples
Example 1: Simple Integer Formatting
C++ String Table Entry (ID 11326):
"学习此技能需要 %d 金钱和 %d 技能点,确定学习吗?"
C# String Table Entry (ID 11326) (if you convert to C# format):
"学习此技能需要 {0} 金钱和 {1} 技能点,确定学习吗?"
Usage:
string message = string.Format(GetStringFromTable(11326), 2480, 34200);
// Result: "学习此技能需要 2480 金钱和 34200 技能点,确定学习吗?"
// English: "Learning this skill requires 2480 money and 34200 skill points, confirm?"
Example 2: Mixed Types
String Table:
"Skill: {0}, Level: {1}, Cost: {2:N0} gold"
Usage:
string skillName = "Fireball";
int level = 5;
int cost = 10000;
string message = string.Format(GetStringFromTable(123), skillName, level, cost);
// Result: "Skill: Fireball, Level: 5, Cost: 10,000 gold"
Example 3: With Colors (Unity TextMeshPro)
C++:
ACString strSp;
strSp.Format(GetStringFromTable(11402), needSp);
if (spOK) {
strSp = l_colorWhite + strSp; // l_colorWhite = "^ffffff"
} else {
strSp = l_colorRed + strSp; // l_colorRed = "^ff0000"
}
C# (Unity TextMeshPro):
const string l_colorWhite = "<color=#ffffff>";
const string l_colorRed = "<color=#ff0000>";
const string l_colorClose = "</color>";
string strSp = string.Format(GetStringFromTable(11402), needSp);
if (spOK) {
strSp = l_colorWhite + strSp + l_colorClose;
} else {
strSp = l_colorRed + strSp + l_colorClose;
}
Example 4: Clickable Text Links (TextMeshPro)
C++ (Rich Text and Actions):
In C++, player names or interactive elements (like items) in chat are often wrapped with custom delimiters like &PlayerName& to be parsed later for coloring and clicking. The click actions are handled by complex UI dialogue components.
C# (Unity TextMeshPro <link> tag):
In Unity's TextMeshPro, we can use the standard <link> tag to define an interactive region of text. By parsing the legacy &PlayerName& format using Regex, we can inject a <link> tag that Unity's event system can interact with.
- Format the string with Regex replacing delimiters (e.g., in
EC_GameUIMan.cs):
if (parsedMsg.Contains("&"))
{
// Convert &PlayerName& into <color=#HexColor><u><link="PlayerName">PlayerName</link></u></color>
parsedMsg = System.Text.RegularExpressions.Regex.Replace(
parsedMsg,
@"&([^&]+)&",
$"<color=#{colorHex}><u><link=\"$1\">$1</link></u></color>"
);
}
- Handle the Click Event via Unity EventSystems (e.g., in
ChatMessageView.cs): The UI View script attached to the TextMeshPro text must implementIPointerClickHandlerto intercept pointer clicks on the<link>markup.
using UnityEngine.EventSystems;
using TMPro;
public class ChatMessageView : MonoBehaviour, IPointerClickHandler
{
public EC_UIUtility.TextOutlet messageText;
public void OnPointerClick(PointerEventData eventData)
{
if (messageText == null || messageText.tmp == null) return;
// Check if the pointer click intersects with any <link> tag boundary
int linkIndex = TMP_TextUtilities.FindIntersectingLink(messageText.tmp, eventData.position, eventData.pressEventCamera);
if (linkIndex != -1)
{
// Retrieve the link ID (which we dynamically set to the player's name)
TMP_LinkInfo linkInfo = messageText.tmp.textInfo.linkInfo[linkIndex];
string linkId = linkInfo.GetLinkID();
if (!string.IsNullOrEmpty(linkId))
{
// Trigger logic, e.g., Whisper the player!
EventBus.Publish(new WhisperPlayerEvent(linkId));
}
}
}
}
Note for specific PW Dialogues: Legacy classes (such as DlgNameLink.cs) may implement a customized command pattern (e.g., LinkCommand, MoveToLinkCommand alongside StyledTaskTraceText) to process complex hyperlink commands recursively. However, for completely rewritten or standalone UI systems like standard Chat or logging panels, utilizing TextMeshPro's native TMP_TextUtilities.FindIntersectingLink combined with IPointerClickHandler is significantly faster, lightweight, and standard for Unity development.
Common Pitfalls
❌ Wrong: Using C++ format specifiers with string.Format
// This won't work if the string uses C++ format specifiers
string template = "Cost: %d gold"; // C++ style
string message = string.Format(template, 1000); // Error!
✅ Correct: Use AUIDialog.FormatPrintf OR convert to C# format
Option 1: Use AUIDialog.FormatPrintf for C++ styles:
string template = "Cost: %d gold"; // C++ style
string message = AUIDialog.FormatPrintf(template, 1000); // "Cost: 1000 gold"
Option 2: Convert to C# format strings:
string template = "Cost: {0} gold"; // C# style
string message = string.Format(template, 1000); // "Cost: 1000 gold"
❌ Wrong: Forgetting to capture variables in lambda
void ShowDialog(int skillID) {
int needMoney = GetMoney(skillID);
// Wrong: using skillID parameter instead of captured value
ShowConfirmDialog("Learn", "Confirm?",
onConfirm: () => {
LearnSkill(skillID); // This captures the parameter, which is fine
}
);
}
This is actually correct, but be careful with loop variables:
// Dangerous in loops!
for (int i = 0; i < skills.Length; i++) {
int skillID = skills[i];
ShowButton(skillID, () => {
LearnSkill(skillID); // Captures skillID, which changes each iteration
});
}
✅ Correct: Capture loop variable properly
for (int i = 0; i < skills.Length; i++) {
int currentSkillID = skills[i]; // Create a copy for this iteration
ShowButton(currentSkillID, () => {
LearnSkill(currentSkillID); // Captures the copy, safe!
});
}
Implementation Checklist
When converting C++ string formatting to C# Unity:
- Replace
ACStringwithstring - Replace
str.Format(...)withstring.Format(...) - Convert format specifiers if needed (
%d→{0}) - Replace pointer-based message boxes with callback-based dialogs
- Use lambda expressions to capture context
- Update color codes for Unity (TextMeshPro uses
<color=#RRGGBB>) - Replace
pMsgBox->SetLife(seconds)withautoCloseTimeparameter - Remove manual dialog lookup code
- Keep Chinese comments and add English translations side by side
Summary
The key difference between C++ and C# Unity for string formatting:
| Aspect | C++ | C# Unity |
|---|---|---|
| String type | ACString |
string |
| Format method | str.Format(template, args) |
string.Format(template, args) |
| Format specifiers | %d, %s, %f |
{0}, {1}, {2} |
| Message box | Pointer + callback lookup | Lambda callbacks |
| Data passing | pMsgBox->SetData(data) |
Closure captures |
| Colors | ^ffffff |
<color=#ffffff>...</color> |
| Auto-close | pMsgBox->SetLife(seconds) |
autoCloseTime: seconds |
The C# version is generally more concise and type-safe thanks to lambda expressions and closure capture!