Last active
May 20, 2019 14:12
-
-
Save YaserAlOsh/1f6d748139418bbcf97cb01472d4663a to your computer and use it in GitHub Desktop.
Texts Localization Solution for the Unity Engine - Unity نظام لتعدد اللغات وتوطين النصوص في محرك الألعاب
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب | |
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/ | |
//يمكن استخدامه في أي مشروع من دون ذكر المرجع | |
//ولكن لا يسمح بيعه على حدة | |
//فهو متوفر بشكل مجاني للجميع | |
namespace Localization | |
{ | |
public class LocalizationsMaster : MonoBehaviour | |
{ | |
[SerializeField] | |
SectionLocalizedTexts[] sectionsLocalizedText; | |
//صنف لكل قسم، يحتوي تعريف Enum للقسم | |
//ونسخة ال ScriptableObject التي تحتوي على النصوص والترجمات | |
[System.Serializable] | |
public class SectionLocalizedTexts | |
{ | |
public TextSections textSection; | |
public LocalizationSO localizationScriptableObject; | |
} | |
//خاصية اللغة المختارة حاليًا | |
//لاحظ أنه لا يمكن تغييرها من خارج الملف البرمجي | |
public Language CurrentLanguage { get; private set; } | |
//تحديد اللغة بناءً على اللغة الافتراضية للجهاز؟ | |
public bool useSystemLangauge; | |
//النسخة المشتركة للملف البرمجي | |
static LocalizationsMaster instance; | |
//دالة لاسترجاع هذه النسخة من أي مكان في المشروع | |
public static LocalizationsMaster getInstance() | |
{ | |
return instance; | |
} | |
void Awake() | |
{ | |
//إذا لم تكن النسخة معرفة بعد، فهذا يعني أن هذا الملف | |
//الوحيد الموجود في المشروع | |
if(instance == null) | |
{ | |
instance = this; | |
} | |
else | |
{ | |
//وجود أكثر من نسخة يعني أنه تم قراءة عنصر من الملف | |
//سابقًا، لذا دمر هذا العنصر الجديد | |
Destroy(this); | |
} | |
//جعل الملف يعيش طوال حياة المشروع | |
DontDestroyOnLoad(gameObject); | |
if (useSystemLangauge) | |
{ | |
//قم بتعيين اللغة المختارة حسب اللغة الافتراضية | |
//..لكن فقط إذا كانت اللغة متوفرة لدينا. | |
if (Application.systemLanguage == SystemLanguage.English) | |
CurrentLanguage = Localization.Language.English; | |
else if(Application.systemLanguage == | |
SystemLanguage.Arabic) | |
CurrentLanguage = Localization.Language.Arabic; | |
else | |
CurrentLanguage = Localization.Language.English; | |
} | |
} | |
//ستفيدنا هذه لتعيين اللغة من عناصرالواجهة | |
public void SetLanguageString(string language) | |
{ | |
if (language.Contains("Arabic")) | |
CurrentLanguage = Localization.Language.Arabic; | |
else | |
{ | |
CurrentLanguage = Localization.Language.English; | |
} | |
LocalizationEvents.CallChangeLanguage(CurrentLanguage); | |
} | |
public void SetLanguageEnum(Language language) | |
{ | |
CurrentLanguage = language; | |
} | |
public string GetText(string key, TextSections textSection = TextSections.MainUI) | |
{ | |
for(int s =0; s < sectionsLocalizedText.Length; s++) | |
{ | |
if (sectionsLocalizedText[s].textSection == textSection) | |
return sectionsLocalizedText[s].localizationScriptableObject.GetString(key, CurrentLanguage); | |
} | |
return sectionsLocalizedText[0].localizationScriptableObject.GetString(key, CurrentLanguage); | |
} | |
//نضيف هنا جميع الأقسام التي نريدها في اللعبة | |
//أو نستخدم فسم واحد فقط، إذا لم تعجبنا فكرة الأقسام. | |
public enum TextSections | |
{ | |
MainUI | |
} | |
} | |
//اللفات التي سندعمها في هذا المقال، يمكنك إضافة قدر ما تريد | |
public enum Language | |
{ | |
English, | |
Arabic | |
} | |
//عرفته كمشترك لكي تبقى نسخة واحد فقط في المشروع | |
public static class LocalizationEvents | |
{ | |
//Delegate تقوم بتوظيف دالة مكان الحدث، أي أنه عندما يُستدعى الحدث، يمكن سماعه باستخدام دالة. | |
public delegate void LanguageEvent(Language language); | |
//الحدث المشترك الذي أستدعيه عند تغيير اللغة | |
//لاحظ أن نوعه LanguageEvent | |
//مما يعني أنه سيكسب الخصائص المعرفة في الأعلى لها. | |
public static event LanguageEvent LanguageChanged; | |
//نمرر اللغة الجديدة لهذه الدالة عند تغيير اللغة | |
public static void CallChangeLanguage(Language newLanguage) | |
{ | |
//إذا كان هناك أحد ما مشترك بالحدث، قم باستدعائه | |
LanguageChanged?.Invoke(newLanguage); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب | |
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/ | |
//يمكن استخدامه في أي مشروع من دون ذكر المرجع | |
//ولكن لا يسمح بيعه على حدة | |
//فهو متوفر بشكل مجاني للجميع | |
using UnityEditor; | |
using UnityEngine; | |
using Localization; | |
//هذه التعليمة تساعدنا في إنشاء نسخ من هذا الملف | |
//حيث تعرّف عنصر جددي في قائمة Create | |
//كما سنشاهد في الخطوة التالية | |
[CreateAssetMenu(fileName = "Localized Strings",menuName = "Localization")] | |
//يجب أن يرث الملف البرمجي من ScriptableObject بدلاً من MonoBehaviour | |
public class LocalizationSO : ScriptableObject | |
{ | |
[SerializeField] | |
TextAsset XmlOfStrings; | |
//مصفوفة معلومات النصوص | |
[SerializeField] //هذه التعليمة تجعل unity قادر على تحميل وعرض المتغير في المحرر inspector | |
KeyString[] strings; | |
//صنف لمعلومات النص، يحتوى على المفتاح والترجمات | |
[System.Serializable]//هذه التعليمة تجعل Unity قادر على التعامل مع الصنف المخصص وحفظه في ال metaData | |
public class KeyString | |
{ | |
public string key; | |
public LocalizedText[] localizations; | |
} | |
//صنف لكل ترجمة للنص | |
// يحتوي على النص وتعريف بلغته | |
[System.Serializable] | |
public class LocalizedText | |
{ | |
//لكي نكتب نصوص بأكثر من سطر (3 كحد أقصى) | |
[Multiline(3)] | |
public string localizedString; | |
public Language language; | |
} | |
//دالة لاسترجاع النص حسب المفتاح واللغة الحالية | |
//لاحظ أن صنف ال ScriptableObject يعمل كأي ملف برمجي | |
//حيث الدوال والمتغيرات المعرّفة يمكن استعمالها لأي نسخة منشئة منه | |
public string GetString(string key,Localization.Language language) | |
{ | |
for (int s = 0; s < strings.Length; s++) | |
{ | |
//نبحث في المصفوفة الأساسية عن المفتاح المطلوب | |
if (strings[s].key == key) | |
{ | |
//للترجمات المعرفة لهذا المفتاح، نحصل على التي تناسب اللغة الحالية | |
for (int l = 0; l < strings[s].localizations.Length; l++) { | |
if (strings[s].localizations[l].language == language) | |
{ | |
return strings[s].localizations[l].localizedString; | |
} | |
} | |
//بحال لم نجد ترجمة لهذا النص باللغة الجالية | |
return "Language: " + language + "Not Found, For Key: " + key; | |
} | |
} | |
//وبحال لم نجد المفتاح من الأساس | |
return "Key: " + key + " Not Found"; | |
} | |
[ExecuteInEditMode] //يجب إضافة هذه التعليمة لكي تعمل الدالة من دون تشغيل اللعبة | |
public void GetStringsFromXML() | |
{ | |
//إذا لم يتم تعيين أي ملف في ال inspector | |
if (XmlOfStrings == null) | |
return; | |
//ننشئ نسخة من XmlDocument ونعطيها نص الملف | |
XmlDocument xMLDocument = new XmlDocument(); | |
xMLDocument.LoadXml(XmlOfStrings.text); | |
if(xMLDocument == null) | |
{//إذا حدث خطأ ما لسبب ما (قد يكون الملف غير مخصص لل xml) | |
Debug.LogError("Not Found: " + XmlOfStrings.name); | |
return; | |
} | |
//هذه العقدة تقع بعد وصف ال Localization، وهي التي تحتوي على عناصر | |
//النصوص | |
XmlNode MainXmlNode = xMLDocument.ChildNodes.Item(1); | |
//نعرف مصفوفة جديدة بعدد العناصر الموجودة في الملف | |
KeyString[] newStrings = new KeyString[MainXmlNode.ChildNodes.Count]; | |
int currCount = 0; | |
//لكل عنصر نص في المستند | |
foreach (XmlNode xmlNode in MainXmlNode.ChildNodes) | |
{ | |
//نتأكد أنه يحتوي على المفتاح | |
if(xmlNode.LocalName == "Key") | |
{ | |
KewStrings[currCount] = new KeyString(); | |
//نحصل على المفتاح | |
newStrings[currCount].key = xmlNode.Attributes.GetNamedItem("name").Value; | |
int index = 0; | |
//نبني مصفوفة للترجمات بعدد الأبناء المتوفرة | |
newStrings[currCount].localizations = new LocalizedText[xmlNode.ChildNodes.Count]; | |
//لكل ابن موجود | |
foreach (XmlNode localizedVersions in xmlNode) | |
{ | |
Language language; | |
//نحاول تحويل اللغة إلى النوع Enum المخصص للغات | |
//إن لم تنجح هذه العملية فهذا يعني أن اللغة غير مدعومة | |
//ويجب إضافتها لل Enum | |
if(!Enum.TryParse( localizedVersions.LocalName, out language)) | |
{ | |
return; | |
} | |
newStrings[currCount].localizations[index] = new LocalizedText(); | |
//نحضر النص ونعينه | |
newStrings[currCount].localizations[index].localizedString = localizedVersions.InnerText; | |
//نعين اللغة | |
newStrings[currCount].localizations[index].language = language; | |
index++; | |
} | |
currCount++; | |
} | |
} | |
//نبدل المصفوفة الموجودة بالجديدة التي بنيناها | |
strings = newStrings; | |
} | |
} | |
[CustomEditor(typeof(LocalizationSO))] | |
public class LocalizationSOEditor : UnityEditor.Editor | |
{ | |
public override void OnInspectorGUI() | |
{ | |
//ال target هي نسخة الملف البرمجي المرتبط بهذا المحرر | |
LocalizationSO localizationSO = (LocalizationSO)target; | |
//ننشئ زرًا جديدًأ | |
if (GUILayout.Button("Get Items From XML Document")) | |
{ | |
//نستدعي الدالة التي كتبناها للتو | |
//(تأكد أن هذه التعليمة موجودة في أعلاها [ExecuteInEditMode]) | |
localizationSO.GetStringsFromXML(); | |
} | |
base.OnInspectorGUI(); | |
serializedObject.ApplyModifiedProperties(); | |
} | |
} | |
//هذه التعليمة تخبر المحرك بأن هذا الكلاس مخصص للصنف | |
//LocalizationSO.LocalizedText | |
[CustomPropertyDrawer(typeof(LocalizationSO.LocalizedText))] | |
//يجب أن نرث من PropertyDrawer لنعدل على طريقة العرض | |
public class LocalizedTextEditor : PropertyDrawer | |
{ | |
//متغير لحفظ قيمة الارتفاع الافتراضية | |
float height = EditorGUIUtility.singleLineHeight; | |
//هذه الدالة سيتم استدعائها لكل عنصر في المصفوفة، نقوم هنا بإعادة | |
///كتابتها لنغير الارتفاع حسب ما نحتاج | |
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) | |
{ | |
// نحضر القيم المطلوبة : النص واللغة | |
SerializedProperty localizedText = | |
property.FindPropertyRelative("localizedString"); | |
SerializedProperty language = | |
property.FindPropertyRelative("language"); | |
//نحفظ النص كمتغير نصي (أجل :3 | |
string correctedArabicText = localizedText.stringValue; | |
if (language.enumValueIndex == (int)Language.Arabic) | |
{ | |
//نقوم بإصلاح النص إذا كان للغة العربية | |
//انبته هنا أننا لم نعين قيمته بعد التصحيح لمحرر النص بعد | |
correctedArabicText = | |
ArabicSupport.ArabicFixer.Fix(localizedText.stringValue, true, | |
false); | |
} | |
//للعنوان، نختصره إذا كان طويلاً | |
title.text = correctedArabicText.Length > 10 ? correctedArabicText.Substring(0, 10) + "..." : correctedArabicText; | |
//نبدأ بالخاصية | |
//تسمح لنا هذه الدالة بعرض الخواص الموجودة في العناصر في | |
//الواجهة بكل سهولة | |
label = EditorGUI.BeginProperty(position, title, property); | |
//مبدئيًا، نحدد الارتفاع بالقيمة الافتراضية | |
position.height = EditorGUIUtility.singleLineHeight; | |
//نضيف القدرة على إظهار وإخفاء العناصر | |
//القيمة الأخيرة تحدد ما إذا كنا نستطيع الضغظ على الاسم للإخفاء | |
//والإظهار | |
property.isExpanded = EditorGUI.Foldout(position, | |
property.isExpanded, label,true); | |
//فقط إذا كانت العناصر ظاهرة | |
if (property.isExpanded) | |
{ | |
//نحرك كل عنصر في المصفوفة إلى اليمين قليلاً | |
EditorGUI.indentLevel = 10; | |
//الصنف Rect هو المسؤول عن أبعاد العناصر | |
//هنا نقوم فقط بتحريكه للأسفل، ونعين الارتفاع كثلاثة أضعاف | |
//الارتفاع الافتراضي (أي ثلاث سطور | |
//تذكر أن الزيادة الموجبة الرأسية تحرك العنصر للأسفل | |
//أجل، هذه أحد غرائب برمجة الواجهات | |
Rect localizedRect = new Rect(position.x, position.y + height * 1f + 4f, position.width, height * 3f); | |
//نقوم بعرض خاصية النص بالأبعاد المحددة | |
//هذه هي فائدة استخدام دالة EditorGUI.BeginProperty | |
EditorGUI.PropertyField(localizedRect, localizedText, new GUIContent("Localized String")); | |
//نقوم بالعملية ذاتها لعنصر اختيار اللغة | |
//لاحظ الفرق في موقع الإزاحة الرأسية | |
Rect languageRect = new Rect(position.x, position.y + height * 4f + 6f, position.width, height); | |
EditorGUI.PropertyField(languageRect, language, new GUIContent("Language")); | |
//والآن الخطوة الأهم، للغة العربية: | |
if (language.enumValueIndex == (int)Language.Arabic) | |
{ | |
//أبعاد جديدة لعنصر عرض النص المصحح | |
//نفس الموقع الأفقي | |
//وإنزاله للأسفل أكثر | |
Rect arabicCorrectedLabel = new Rect(position.x , position.y + height * 5f + 8f, localizedRect.width, height * 3f); | |
//بعض خواص التنسيق البسيطة | |
GUIStyle RightAlign = EditorStyles.textArea; RightAlign.alignment = TextAnchor.MiddleCenter; | |
//إنشاء خاصية لعرض النص | |
//Selectable تعني أنه يمكن نسخ النص، ولكن لا يمكن التعديل عليه | |
//لاحظ هنا أنني الآن مرّرت النص المصحح | |
EditorGUI.SelectableLabel(arabicCorrectedLabel, correctedArabicText, RightAlign); | |
} | |
} | |
EditorGUI.EndProperty(); | |
//نقوم بتطبيق التغييرات على الواجهة | |
property.serializedObject.ApplyModifiedProperties(); | |
} | |
//نسخة مسؤولة عن تحديد خواص رأسية الخصائص | |
GUIContent title = new GUIContent(); | |
//هذه الدالة هي ما تقوم بالعرض، وبداخلها سنقوم بتكوين العناصر حسب | |
//ما نرغب | |
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) | |
{ | |
//إذا لم يتم الضغظ على عنصر المصفوفة لعرض محتواه | |
if (!property.isExpanded) | |
return EditorGUIUtility.singleLineHeight; | |
//نبحث عن خاصية اللغة لعنصر المصفوفة الحالي | |
SerializedProperty language = property.FindPropertyRelative("language"); | |
//إذا كانت اللغة العربية، نقوم بزيادة الارتفاع بما يعادل 9 أضعاف القيمة الافتراضية | |
//وهذا لأن طول محرر النص 3، مع طول اختيار اللغة 1، ومحرر النص الآخر 3، و1 للعنوان يصبحوا 8 | |
if (language.enumValueIndex == (int)Language.Arabic) | |
return EditorGUIUtility.singleLineHeight * 8f + 6f; | |
//وإلا، فلن نعرض محرر النص الثاني، لذا ننقص 3 قيم للارتفاع، مع مسافة قليلة بين القيم | |
else | |
return EditorGUIUtility.singleLineHeight * 5f + 4f; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب | |
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/ | |
//يمكن استخدامه في أي مشروع من دون ذكر المرجع | |
//ولكن لا يسمح بيعه على حدة | |
//فهو متوفر بشكل مجاني للجميع | |
using UnityEngine; | |
using Localization; | |
using TMPro; | |
//فقط لأنني لا أحب كتابة أسماء طويلة، اختصرت الاسم هنا ^-^ | |
using LM = Localization.LocalizationsMaster; | |
//يجب أن يحتوى الكائن على عنصر TextMeshProUGUI | |
[RequireComponent(typeof(TextMeshProUGUI))] | |
public class LocalizeTextMPro : MonoBehaviour | |
{ | |
//حقول للمفتاح والقسم الخاص بالنص | |
[SerializeField] | |
string StringKey; | |
[SerializeField] | |
Localization.TextSections textSection; | |
private string requiredText; | |
TextMeshProUGUI tmproUGUI; | |
void Start() | |
{ | |
tmproUGUI = GetComponent<TextMeshProUGUI>(); | |
//نحصل على النص المطلوب من مدير التوطين، حسب المفتاح والقسم | |
//المحددين. | |
requiredText = LM.getInstance().GetText(StringKey, textSection); | |
//نعرض النص في العنصر tmproUGUI | |
tmproUGUI.text = requiredText; | |
CheckArabicLanguage(); | |
Localization.LocalizationEvents.LanguageChanged += LanguageChanged; | |
} | |
private void LanguageChanged(Language language) | |
{ | |
requiredText = LM.getInstance().GetText(StringKey, textSection); | |
tmproUGUI.text = requiredText; | |
CheckArabicLanguage(); | |
} | |
//الحالة الخاصة التي تكون اللغة فيها عربية - أو حتى فارسية، إذا أردنا. | |
private void CheckArabicLanguage() | |
{ | |
if (LM.getInstance().CurrentLanguage == Language.Arabic) | |
{ | |
//هذا هو الملف البرمجي الذي كتبناه في المقال السابق | |
//تأكد من وجوده في مشروعك. | |
//هنا نتأكد من وجوده في الكائن | |
if (!GetComponent<FixArabicTMProUGUI>()) | |
{ | |
gameObject.AddComponent<FixArabicTMProUGUI>(); | |
} | |
//هذه الدالة هي المسؤولة عن إعدادالنص المراد إصلاحه | |
//احصل على الملف من هنا | |
//https://gist.github.com/YaserAlOsh/48d9e5b1785135df1d4d4264437c8d03 | |
GetComponent<FixArabicTMProUGUI>().UpdateText(requiredText); | |
} //إذا لم تكن اللغة عربية، أزل هذا الملف، لأنه سيؤذي النص والأداء | |
else if (GetComponent<FixArabicTMProUGUI>()) | |
Destroy(GetComponent<FixArabicTMProUGUI>()); | |
} | |
//هناك مشكلة تحدث عن تدمير الجسم | |
//حيث أن المرجع للملف المشترك في الحدث يبقى موجودًا، بعد تدمير الجسم.. | |
//ثم عند تغيير اللغة يتم استدعاءء الحدث، ، ولكنه لم يعد موجودًا مما يعطي خطأً | |
//الحل هو إزالة المرجع عند تدمير الجسم | |
void OnDestroy() | |
{ | |
LocalizationEvents.LanguageChanged -= LanguageChanged; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//تمت كتابة الملف من قبل الموقع مراحل - لمطوري الألعاب | |
//http://www.devjourney.epizy.com/عرض-النصوص-بلغات-متعددة-في-untiy/ | |
//يمكن استخدامه في أي مشروع من دون ذكر المرجع | |
//ولكن لا يسمح بيعه على حدة | |
//فهو متوفر بشكل مجاني للجميع | |
using UnityEngine; | |
using Localization; | |
using UnityEngine.UI; | |
//فقط لأنني لا أحب كتابة أسماء طويلة، اختصرت الاسم هنا ^-^ | |
using LM = Localization.LocalizationsMaster; | |
//يجب أن يحتوى الكائن على عنصر TextMeshProUGUI | |
[RequireComponent(typeof(Text))] | |
public class LocalizeTextMPro : MonoBehaviour | |
{ | |
//حقول للمفتاح والقسم الخاص بالنص | |
[SerializeField] | |
string StringKey; | |
[SerializeField] | |
Localization.TextSections textSection; | |
private string requiredText; | |
Text textUI; | |
void Start() | |
{ | |
textUI = GetComponent<Text>(); | |
//نحصل على النص المطلوب من مدير التوطين، حسب المفتاح والقسم | |
//المحددين. | |
requiredText = LM.getInstance().GetText(StringKey, textSection); | |
//نعرض النص في العنصر textUI | |
textUI.text = requiredText; | |
CheckArabicLanguage(); | |
Localization.LocalizationEvents.LanguageChanged += LanguageChanged; | |
} | |
private void LanguageChanged(Language language) | |
{ | |
requiredText = LM.getInstance().GetText(StringKey, textSection); | |
textUI.text = requiredText; | |
CheckArabicLanguage(); | |
} | |
//الحالة الخاصة التي تكون اللغة فيها عربية - أو حتى فارسية، إذا أردنا. | |
private void CheckArabicLanguage() | |
{ | |
if (LM.getInstance().CurrentLanguage == Language.Arabic) | |
{ | |
//هذا هو الملف البرمجي الذي كتبناه في المقال السابق | |
//تأكد من وجوده في مشروعك. | |
//https://gist.github.com/YaserAlOsh/48d9e5b1785135df1d4d4264437c8d03 | |
//هنا نتأكد من وجوده في الكائن | |
if (!GetComponent<SetArabicFixedText>()) | |
{ | |
gameObject.AddComponent<SetArabicFixedText>(); | |
} | |
//هذه الدالة هي المسؤولة عن إعدادالنص المراد إصلاحه | |
GetComponent<SetArabicFixedText>().UpdateText(requiredText); | |
} //إذا لم تكن اللغة عربية، أزل هذا الملف، لأنه سيؤذي النص والأداء | |
else if (GetComponent<SetArabicFixedText>()) | |
Destroy(GetComponent<SetArabicFixedText>()); | |
} | |
//هناك مشكلة تحدث عند تدمير الجسم | |
//حيث أن المرجع للملف المشترك في الحدث يبقى موجودًا، بعد تدمير الجسم.. | |
//ثم عند تغيير اللغة يتم استدعاءء الحدث، ، ولكنه لم يعد موجودًا مما يعطي خطأً | |
//الحل هو إزالة المرجع عند تدمير الجسم | |
void OnDestroy() | |
{ | |
LocalizationEvents.LanguageChanged -= LanguageChanged; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment