الشبكة العربية لمطوري الألعاب

محترف  انس مشاركة 1

السلام عليكم.

في خضم صراعي مع البرمجة الغرضية، و بما انني احاول تطوير لعبة لمشروع مهم نوع ما مع صديق العزيز Dali، حاولت برمجة صنف يتحكم في حالات اللعبة و في الواجهات (Menu).اريد ان اشارككم به لكي اتحصل على المساعدة اللازمة لاتمام هذا الصنف، مبدئيا هو يعمل كما يفترض لكن اريد معرفة هل هذه الطريقة مقبولة، هل هناك حل انجع ؟ هل اخذت المشكل من منظور خاطئ ؟ المهم كل تعليق مرحب به.

ساشرح ماهية عمل هذا الصنف و الذي سنسميه السجل:
cGameStateManager

Methode :
public static void AddGroupe( cStateGroupe NewGroupe );
public static cStateGroupe GetGroupe( String GroupeId );
public static void GiveFocusToParent( ref cStateGroupe NewStateGroupe );
public static void GiveFocusToChildren( String ChildrenId );
public static void Update( ref cStateGroupe CurrentState );
شرح الدوال بالترتيب :
1- اظافة حالة او مجموع حالات الى الصنف،
2- استخراج مجموع حالات من الصنف(مجموع حالات هو عبارة عن قائمة تضم الحالات التي لها نفس المصدر)
3- يسمح للمصدر الحالة بالعمل عوض الحالة الحالية
4- يسمح لاحد الحالات للمصدر الحالي بالعمل.
5- تحديث و تحددي اي من مجموع الحالات عليه ان يبدا العمل.

يعمل هذا الصنف كمخزن مركزي لجميع حالات اللعبة.
مثال :
            cGameStateManager.AddGroupe(new cStateGroupe("Root", "Root"));
            cGameStateManager.AddGroupe(new cStateGroupe("Logo", "Root"));
            cGameStateManager.AddGroupe(new cStateGroupe("Game Intro", "Logo"));
            cGameStateManager.AddGroupe(new cMainMenu("Main Menu", "Game Intro"));
بهذه الطريقة نكون قد سجلنا 3 حالات ( الحالة الاولى نعتبرها الحالة الاصلية للعبة، و ليس لها دور عدا الخروج من اللعبة):
-شعار الشركة، و الذي ينحدر من الحالة الاصلية
- المقدمة، و الذي ينحدر من الحالة "شعار الشركة"
- القائمة الرئيسية و التي تنحدر من "المقدمة"

بهذه الكيفية سيعمل السجل على الانتقال من المصدر المحدد الى ما ينحدر منه.او العكس.

الان ساشرح ماهية  الحالات، اولا،العناصر التي يرثها هذا الصنف
        public bool HaveFocus 
        public StringId
         
        public virtual void Update( GameTime gameTime )

        public virtual void OnInit()
        public virtual bool IsSelecte() 
        public virtual bool OnSelecte()
        public virtual void OnShutDown() 

        public virtual void OnEnter() 
        public virtual bool OnExit() 
        public virtual void OnDraw( GameTime gameTime ) 
العناصر الخاصة بهذا الصنف :
public Stack< cAbstractGameStates> StateStack;      //1  
public Dictionary<string,cabstractgamestates> StateMap;//2
public int StateIndexer;//2
public cStateGroupe Parent;//3
public string ParentId;//4
public String ChildrenId ;//5

public cAbstractGameStates GetState( String StateId )//6
public void PushState( String NewStateId )//7
public void PopState( String NewStateId )//8
public void AddState( cAbstractGameStates NewGameState )//9
public bool TheStateIsMine( String StateId )//10
public bool TheGroupIsMine( String GroupeId )//11
public void ChangeState( cAbstractGameStates NewGameState )//12
1- مكدس يحتوي على الحالات التي تعمل( احتاج الى المساعدة في هذه النقطة لمن لديه فكرة افضل).
2- مصفوفة تحتوي على جميع الحالات التي تحتويها هذه الوحدة.
3- متغير يسمح لنا بتصفح الحالات
4- اسم مصدر الوحدة
5- اسم الوحدة المنحدرة من هذه الوحدة
6- الحصول على حالة معينة او مجموع حالات معين.
7- اظافة الحالة او حجموع الحالات الى المكدس و تشغيلها.و الخروج من المصدر
8- ازالة الحالة من المكدس و الخروج من تلك الحالة و العودة الى المصدر.
9- اضافة حالة او مجموع حالات.
10- التحقق من ان الحالة المعنية تنتمي الى هذه الوحدة.
11- التحقق من ان مجموع الحالات المعني ينتمي الى هذه الوحدة.
12- تغير الحالة او مجموع الحالات

يعمل هذا الصنف تماما كالقائمة، مثلا :
            // creat the Main Menu Groupe
            cGameStateManager.AddGroupe(new cMainMenu("Main Menu", "Game Intro"));

            // creat the Main Menu States
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("New Game", "Main Menu"));
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("Load Game", "Main Menu"));

            cGameStateManager.AddGroupe(new cStateGroupe("Game Option", "Main Menu"));

            cGameStateManager.GetGroupe("Main Menu").AddState(new cExitState("Exit", "Main Menu"));

            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Sons", "Game Option"));
            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Commande", "Game Option"));
            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Graphisme", "Game Option"));
            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Cheat", "Game Option"));
سنتحصل على القائمة التالية :
Main Menu
    New Game  : when select StartThe Game
    Load Game : When Select Load The Game
    Option    : When Select Go To Sub Menu
             SubMenu Option :
                    Sons
                    Commande
                    Graphisme
                    Cheat
    Exit Game : When Select Exit The Game
كل من هذه الحالات يمكن ان يعرف بصنف خاص به مادام الجميع ينحدر من نفس الصنف
cAbstractGameStates



بهذه الكيفية يمكننا انشاء اي حالة نريدها، و يكفي ان ترث هذه الحالة من الصنف المذكور سابقا حتى يتمكن السجل من التعامل معها.
كل حالة معرفة بصنف خاص بها مما يسمح ( على الاقل براي) ببرمجة اصناف مستقل عن البرنامج يتم انشائها في صنف الحالة المراد التعامل معها.
مثال :
لتيكن الكود التالي :
            // creat the Main Menu Groupe
            cGameStateManager.AddGroupe(new cStateGroupe("Main Menu", "Game Intro"));

            // creat the Main Menu States
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("New Game", "Main Menu"));
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("Load Game", "Main Menu"));
            cGameStateManager.AddGroupe(new cStateGroupe("Game Option", "Main Menu"));
            cGameStateManager.GetGroupe("Main Menu").AddState(new cExitState("Exit", "Main Menu"));
لاحظوا السطر الاول : نوع الحالة المرسلة : cStateGroupe
اي اننا طلبنا اضافة مجموعة حالات.النتيجة :




و هذا لان الصنف cStateGroupe صنف بدائي وظيفته التعامل مع الحالات فقط.
الان ننشئ صنف cMainMenu الذي يرث من الصنف السابق.و يحتوي على معلومات عن كيفية الاظهار، سنحصل و بنفس الكود السابق :
            // creat the Main Menu Groupe
            cGameStateManager.AddGroupe(new cMainMenu("Main Menu", "Game Intro"));

            // creat the Main Menu States
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("New Game", "Main Menu"));
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("Load Game", "Main Menu"));
            cGameStateManager.AddGroupe(new cStateGroupe("Game Option", "Main Menu"));
            cGameStateManager.GetGroupe("Main Menu").AddState(new cExitState("Exit", "Main Menu"));
السطر الاول تغير و ذلك لننشئ صنف : cMainMenu
النتيجة :


الكود المستعمل في اللعبة و الذي يتولى مهمة التعامل مع الحالات :


using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace XNATesting
{

    public class Game1 : Microsoft.Xna.Framework.Game
    {
        public static Game1 TheGame{
            get { return game; }
        }

        private static Game1 game;
        public GraphicsDeviceManager graphics;
        public SpriteBatch spriteBatch;

        public SpriteFont font,smallFont,largeFont;
        cStateGroupe firstgroupe;
.....
..... 
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            audio = new AudioLibrary();
            audio.LoadContent(Content);
            Services.AddService(typeof(AudioLibrary), audio);

...
...

/////////////////////////////////////////////////////////////////////////////////
            // creat the root parent
            cGameStateManager.AddGroupe(new cStateGroupe("Root", "Root"));
            cGameStateManager.AddGroupe(new cStateGroupe("Logo", "Root"));
            cGameStateManager.AddGroupe(new cStateGroupe("Game Intro", "Logo"));
            cGameStateManager.AddGroupe(new cStateGroupe("Menu 01", "Game Intro"));
            cGameStateManager.AddGroupe(new cStateGroupe("Menu 02", "Game Intro"));
            // creat the Main Menu Groupe
            cGameStateManager.AddGroupe(new cMainMenu("Main Menu", "Game Intro"));

            // creat the Main Menu States
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("New Game", "Main Menu"));
            cGameStateManager.GetGroupe("Main Menu").AddState(new cPlayState("Load Game", "Main Menu"));
            cGameStateManager.AddGroupe(new cStateGroupe("Game Option", "Main Menu"));
            cGameStateManager.GetGroupe("Main Menu").AddState(new cExitState("Exit", "Main Menu"));

            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Sons", "Game Option"));
            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Commande", "Game Option"));
            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Graphisme", "Game Option"));
            cGameStateManager.GetGroupe("Game Option").AddState(new cPlayState("Cheat", "Game Option"));

            cGameStateManager.GetGroupe("Game Intro").AddState(cGameStateManager.GetGroupe("Main Menu"));
            cGameStateManager.GetGroupe("Game Intro").AddState(cGameStateManager.GetGroupe("Game Intro"));

            firstgroupe = cGameStateManager.GetGroupe("Logo");
            firstgroupe.HaveFocus = true;
            firstgroupe.OnEnter();
////////////////////////////////////////////////////////////////////////////////////
        }


        protected override void Update( GameTime gameTime )
        {
/////////////////////////////////////////
            cInput.Input.UpdateInput();
/////////////////////////////////////////

            // Allows the game to exit
            if ( GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed )
                this.Exit();

            // TODO: Add your update logic here
            firstgroupe.Update(gameTime);
            cGameStateManager.Update(ref firstgroupe); 




            base.Update(gameTime);
        }


        protected override void Draw( GameTime gameTime )
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            spriteBatch.Begin();
////////////////////////////////////////////
            firstgroupe.OnDraw(gameTime);
///////////////////////////////////////////
            spriteBatch.End();
            base.Draw(gameTime);
        }
    }
}


---------------------------

ميزات هذه الطريقة ( على الاقل ما اظنه ميزات) :
1- مرونة طريق اظافة الحالات او مجموع الحالات (قائمة).
2- برمجة مكونات اللعبة بشكل مستقل بما ان كل حالة من اللعبة لديها تصرف خاص، اذن صنف خاص.
3- اذا كان هناك صنف بنفس التصرف يمكننا انشائه مرة واحدة فقط.

ما يجب فعله :
- انشاء النوافذ العائمة : PopWindow مثل تلك التي تطلب منا تاكيد اختيار الخروج.
- تصحيح بعض الاخطاء .

ما اريده هو تعلثق عن مدى فعالية هذا الطريفة، و هل انا في الطريق السليم ؟ ماهي خبرتكم في هذا الموضوع ؟
كيف يمكن تحسين هذه الطريقة.

المصادر التي اطلعت عليها :
مقال : An Architecture For Game State Management Based On State  Hierarchies By Luis Valente Aura Conci Bruno Feijó
http://www.icad.puc-rio.br/~lvalente/docs/2006_sbgames.pdf

مقال : Using the Game State Management Sample
http://www.nazspace.com/wp/2008/02/04/using-the-game-state-management-sample/

مقال : State Pattern in C++ Applications
http://www.codeproject.com/KB/architecture/statepattern3.aspx

مشروع عبر السدم : StateFlow classe


المشروع لمن اراد رؤية الكود باكمله :http://cid-68fc9008694076f5.office.live.com/self.aspx/.Public/XNATesting.rar

اختيار القائمة الموالية (مجموع الحالات او الحالة) : يمين، شمال
اختيار عناصر القائمة الاساسية Main Menu  : فوق،تحت ( صدور صوت المؤشر)...يمين شمال ( عدم صدور اي صوت)
التاكيد على الاختيار : زر Enter
العودة : BackSpace


شكرا جزيلا انا بحاجة الى مساعدتكم في اقرب وقت.
الى اللقاء.سلام

محترف  انس مشاركة 2

☺ ارى ان النص طويل بصفة مزعجة نوع ما، لم الاحظ ذلك حتى الان، اسف.
حسنا سابسط السؤال : ما هي الطرق المستعملة لمعالجة حالات اللعبة ؟

خبير مدير وسام البهنسي مشاركة 3

ما شاء الله، التصميم ممتاز وقدرات الصنف عالية ومرنة. أعجبني موضوع المكدس الذي يسمح بتشغيل حالات فوق حالات أخرى... هكذا تستطيع مثلاً تمثيل نظام قوائم هيكلي وبمرونة وبساطة.
 
لا يوجد خطأ في التصميم الذي طرحته يا أنس. لكن في سؤالك عن وجود طرق بديلة، فالجواب هو نعم توجد. والخيار يتعلق بالدرجة الأولى بمقدار التعقيد والإمكانيات التي تودّ الحصول عليها من النظام...
 
كمثال، لعبة وادي الملوك تحوي نظامي إدارة حالات. الأول عالي المستوى ويقترب من التصميم الذي طرحته أنت هنا، إلا أنه لا يملك ميزة تكديس الحالات (هذا هو نفس النظام الذي نستخدمه الآن في عبر السدم أيضاً). الثاني تجده في برمجة تصرفات الوحوش. حيث أن الوحش يكون في حالة معينة (مشي نحو نقطة، وقوف وانتظار، مشي عشوائي، ملاحقة، وهكذا). إلا أن هذا النظام بسيط للغاية بهدف تخفيف العبء وتسريع الحسابات داخل اللعبة.
 
يمكنك رؤية هذين النظامين في كود اللعبة، كما أن ياسر عبد الحليم قام بترجمة مقالة عن حالات اللعبة قد تعطيك أفكاراً إضافية أيضاً. أنظر:
 
http://www.agdn-online.com/papers/state_sys.htm
 
ما شاء الله، إن كانت هذه بدايتك مع البرمجة غرضية التوجه، فكيف ستصبح بعد أن تتقدم بها؟ 😄

وسام البهنسي
مبرمج في إنفيديا وإنفريمز

محترف  انس مشاركة 4

شكرا استاذ وسام، قد قمت باصميم هذا و انا غير واثق من صحته من الناحية البرمجية. اما الان فقد ساعدتني كي اتمم تصميم النظام  و ذلك باظافة تاثيرات بصرية للانتقال من حالة لاخرى Screen Transition
بصراحة  لم افكر في تصرف الكائنات من نوع لاعب او وحش اثناء التصميم، بل الفكرة كانت : التعامل مع حالات اللعبة، و ليس حالة الاعب
ساقراء المقال الذي قدمته لي حتى استفيد من هذا النظام في تصرف اللاعب و الوحوش.


حاليا اطمح لاظافة الميزات التالية :
-نافذة عائمة، و التي غالبا تحتوي رسالة : موافق، لا ، لتاكيد او حذف حالة ما
- برمجة حالات الانتقال.
- برمجة خاصية الانتقال من حالة ما الى اي قائمة (مجموع حالات) في اللعبة.
- تحميل الحالات الممكنة للعبة من ملف معين.
- انشاء واجهة لتصميم القائمات و الحالات المتصلة بها و حفظها في ملف.




لو سمحت لي استاذ وسام وحتى تسهل علي المهمة، كيف برايك يمكن الاستفادة من هذا النظام في حالة : حالات اللاعب او الوحوش ؟
قد يبدو سؤالا تافها لكن اواجه صعوبات كبيرة قبل ان اصل الى نتيجة 😄 لهذا اطلب منك التوجيه من فضلك 😄 .


شكرا جزيلا و السلام

خبير مدير وسام البهنسي مشاركة 5

في 27/شوال/1431 09:51 ص، قال انس بهدوء وتؤدة:

لو سمحت لي استاذ وسام وحتى تسهل علي المهمة، كيف برايك يمكن الاستفادة من هذا النظام في حالة : حالات اللاعب او الوحوش ؟
قد يبدو سؤالا تافها لكن اواجه صعوبات كبيرة قبل ان اصل الى نتيجة 😄 لهذا اطلب منك التوجيه من فضلك 😄 .

قد يكون من المتعذر استخدام نفس الصنف لمعالجة حالات تصرف الوحوش، فمثلاً فكرة النقلات البصرية غير مفيدة في معالجة التصرفات.
 
قبل أن نسأل هذا السؤال، يجب أن تقرر ما الهدف من وراء هذا الصنف. إن كنت تودّ جعله أداة لبناء واجهات اللعبة فيجب أن تصممه على هذا الأساس، ولا تشغل بالك بالاستخدامات الأخرى. أما إن كنت تود جعله أداة لمعالجة الحالات لاستخدامات عدة، فتجب إعادة النظر في التصميم، وتخيل الاستخدامات الممكنة للصنف، ومن ثم التعميم عليها وبناء الصنف.
 
طبعاً تصميم مثل هذه الأدوات يفضل أن يتم مع كود تجريبي للعبة ما (حتى وإن كانت تخيلية)، وذلك لتحس بمدى سهولة استخدام الصنف ومدى نجاحه في تحقيق الأهداف التي وضعتها له.
 
في وادي الملوك كان صنف Stateflow مسؤولاً عن تدفق مكونات اللعبة العامة (من المقدمة إلى القائمة إلى اللعبة ... الخ)، وهو مخصص لتلك المهمة فقط. أما الوحوش (فكما لك أن ترى) استخدمَتْ كود إدارة حالات أبسط بكثير، لا يعدو أن يكون متغيراً واحداً رقمياً يحدد نوع الحالة التي يوجد بها الوحش.

وسام البهنسي
مبرمج في إنفيديا وإنفريمز

محترف  انس مشاركة 6

اظن انه من الافضل انشاء صنف منفصل يعمل بنفس المبدا يكون محتوى في صنف اللاعب و الوحوش، فهذا سيمكننا من التعامل بطريقة منفصلة و متميزة من صنف لاخر.

اقصد ننشئ صنف عام يحتوي على كيفية التحكم بحالات اللاعب او الوحوش، ثم  ننشئ صنف يرث من الصنف السابق، مع تعديل الكود لكل صنف جديد .
اقصد شيئ ما من هذا القبيل : 


class cBehavior{

public virtuel bool OnStop();
public virtuel bool OnAction();
public virtuel bool ChangeBehavior( String BehaviorID );
public virtuel void RestBehavior();
public virtuel void AddBehavior();
public virtuel void GetBehavior();

}

ثم صنف حالات اللاعب :


class cPlayerBehavior : cBehavior{

// implemente the virtuel methode
// and add methode if necessery

}


و في صنف اللاعب  :


class cPlayer{

  cPlayerBehavior MyBehavior;
///
}

هل هذا التصميم سليم ؟

خبير مدير وسام البهنسي مشاركة 7

في 27/شوال/1431 11:02 ص، غمغم انس باستغراب قائلاً:

هل هذا التصميم سليم ؟

نعم، باستثناء أن تغيير الحالة يتم عن طريق تمرير نص، وكما نعلم جميعاً، فإن مقارنة النصوص ببعضها عملية بطيئة مقارنة بمقارنة متغيرات عددية صحيحة. ما الفائدة التي تجنيها من استخدام نص لتحديد الحالة مقارنة بالتعداد مثلاً enum؟

وسام البهنسي
مبرمج في إنفيديا وإنفريمز

محترف  انس مشاركة 8

الفائدة الحالية هي امكانية اضافة عدد معتبر من الحالات، ففي حالة الـ Enum انا مظطر لاظافة اي حالة اريدها الى هذا الـ Enum اما باستخدام النصوص فيمكننا اظافة اي عدد من الحالات المختلفة.
الفائدة الثانية هي امكانية الحصول على اسماء الحالات و بالتالي استخدامها في صنف  الذي يتحكم في مظهر القائمة الرئيسية.
هل يمكن بصفة او باخرى استعمال طريقة افضل ؟