السلام عليكم ومرحبا بكم في الدرس الرابع من سلسلة أسس برمجة الألعاب بالجافا. في هذا الدرس سأحاول ان أبسط لكم طريقة إدارة حالات اللعبة و مختلف سيناريوهاتها.
كل لعبة تتكون أساسا من عدة حالات وكل حلة من الحالات تمثل شاشة من شاشات اللعبة. كقائمة الإختيارات ومستويات اللعبة أو لوحة النقاط والرتب أو لوحة الإعدادات...
وكسيناريو بسيط للعبة ما قد نلج إلى قائمة الإختيارات التي بدورها ستمكننا من الولوج إلى المستوى الأول من اللعبة. ثم عند إنهائه يجب أن نتمكن من الولوج أوتوماتكيا إلى المستوى الثاني أو الرجوع إلى قائمة الإختيارات... إلخ.
فكيف يمكننا إدارة هذا الأمر في اللعبة؟
هناك العديد من الطرق للقيام بذلك. فيمكننا إدماج هذا مباشرة في حلقة اللعبة مثلا
لكن بهذه الطريقة سريعا ما سيصبح الأمر معقدا كما سيصعب فهم أسطر العبة وصيانتها وكذا متعة برمجتها. وستصبح مهمة إضافة حالة جديدة للعبة كلوحة إختيار شخصية اللعبة معقدة.
لكن ماذا لو كان لدينا مكون يتحكم في كل حالات اللعبة ويحدد أياََ منها يجب تحديثه ورسمه؟ ألن يصبح الأمر أسهل وأسرع.
وهذا هدفنا من هذا الدرس. شرح كيفية إنشاء مكون يتحكم في إدارة حالات اللعبة.
يوجد نمط تصميم سهل وفعال لإدارة كل هذا يسما ب Game state manager أو مدير حالات اللعبة.
وهذا ما سنقوم بشرحه وبرمجته ثم إدماجه في مثال السلسلة. لعبة المحطم هكذا قررت تسميت مثال السلسلة هههه.
لكن أولا دعونا نحدد حالات لعبتنا. ولنجعل الأمر سهلا وبسيطا سنجعل مثالنا يتكون من حالتين أو مرحلتين فقط :
ما سنقوم به هو تفويض عمليات التحديث و الرسم وكذلك التحميل لحالات اللعبة. حيث ستقوم كل حالة بتحديث معلوماتها الخاصة ثم رسمها. وسنجعل من مدير الحالات يحدد أيا منها يجب إستدعائها وتنفيذ وظائفها داخل حلقة اللعبة.
//حلقة اللعبة public void run() { while(running) { if(this->currentState == STATE_MENU) { // تحديث ورسم قائمة الإختيارات } else if(this->currentState == STATE_LEVEL1) { // تحديث ورسم المستوى الأول } else if(this->currentState == STATE_CREDITS) { // تحديث ورسم لوحة المراتب والنقاط } } }
لكن ماذا لو كان لدينا مكون يتحكم في كل حالات اللعبة ويحدد أياََ منها يجب تحديثه ورسمه؟ ألن يصبح الأمر أسهل وأسرع.
وهذا هدفنا من هذا الدرس. شرح كيفية إنشاء مكون يتحكم في إدارة حالات اللعبة.
يوجد نمط تصميم سهل وفعال لإدارة كل هذا يسما ب Game state manager أو مدير حالات اللعبة.
وهذا ما سنقوم بشرحه وبرمجته ثم إدماجه في مثال السلسلة. لعبة المحطم هكذا قررت تسميت مثال السلسلة هههه.
لكن أولا دعونا نحدد حالات لعبتنا. ولنجعل الأمر سهلا وبسيطا سنجعل مثالنا يتكون من حالتين أو مرحلتين فقط :
- لوحة الإختيارات : ستمكننا من بدء لعبتنا أو إنهاء اللعبة والخروج.
- المستوى الأول للعبة : سيمكننا من لعب المستوى الأول.
بهذا نكون قد حددنا حالتين للعبتنا. ويجب على المكون المتحكم في هذه الحالات إظهار المناسبة منها حسب الأزرار المفعلة والسياق الذي نتواجد فيه.
أترككم مع فيديو قصير يوضح نتيجة هذا الدرس
تذكير سريع : العبة هي عبارة عن حلقة لامتناهية نقوم فيها بتحديث المعلومات ثم رسمها لإظهارها على الشاشة.
ما سنقوم به هو تفويض عمليات التحديث و الرسم وكذلك التحميل لحالات اللعبة. حيث ستقوم كل حالة بتحديث معلوماتها الخاصة ثم رسمها. وسنجعل من مدير الحالات يحدد أيا منها يجب إستدعائها وتنفيذ وظائفها داخل حلقة اللعبة.
لنبدئ بإنشاء فصيلة تحدد خاصيات حالات لعبتنا عموما. كل حالة من حالات العبة يجب أن تكون قادرة على تحميل الموارد التي ستحتاجها و على تحديث المعلومات الخاصة بها والرسم على الشاشة وكذلك التجاوب مع الأوامر المُصْدَرة.
بعد هذا يجب على كل حالات اللعبة ان ترث من GameState وتحدد وظائفها المجردة.
وقد قمنا سابقا بتحديد حالتين في مثالنا لوحة الإختيارات و المستوى الأول للعبة.
لنقم ببرمجتهما. ولنبدأ بالفصيلة التي تجسد حالة لوحة الإختيارات.
لوحة الإختيارات ستحتوي إختيارين : بدء اللعبة و الخروج. يجب ان نستطيع تغيير الإختيارات عن طرق أسهم التوجيه فوق تحت من لوحة المفاتيح. تأكيد الإختيار نقوم به عن طريق زر الدخول.
هكذا نكون قد انهينا أول حالة من اللعبة (لوحة الإختيارات).
فل ننتقل إلى إنشاء المستوى الأول من اللعبة. في هذه المرحلة من سلسلة الدروس المستوى الأول لا يحتوي على الكثير. فالهدف من هذا الدرس هو توضيح إدارة حالات اللعبة فقط. سنقوم بتطوير خصائص المستوى الأول في دروس لاحقة من هذه السلسلة.
المستوى الأول يرث أيظا من GameState ويحدد وظائفها المجردة
هكذا نكون قد أنهينا برمجة الحالتين اللتان سبق وحددناهما. الأن لنقم بإنشاء الفصيلة التي ستقوم بإدارة الحالات داخل اللعبة.
هذه الفصيلة يجب أن تكون قادرة على تحديد الحالة التي يجب تنفيذ وظائفها ( التحديث والرسم ).
لنرى كيف يمكننا برمجة هذا
هكذا نكون قد أنهينا برمجة فصيلة الإدارة لحالات اللعبة.
لنقم بدمجها مع حلقة اللعبة.
بهذا نكون قد وصلنا إلى نهاية الدرس الرابع من سلسلة برمجة الألعاب بالجافا. ستجدون روابط تحميل العبة والمشروع كاملا أسفله. وإلى القاء في الدرس الموالى حيث سأقوم بشرح كيفية تحميل خريطة المستوى وعالم اللعبة.
//حالات اللعبة، فصيلة مجردة //تحدد خاصيات حالاتها public abstract class GameState { //مرجع نحو مدير الحالات // سنقوم بإنشائه لاحقا protected GameStateManager gsm; //منشئ الحالة public GameState(GameStateManager gsm) { this.gsm = gsm; } //وظيفة مجردة:تهييء و تحميل public abstract void init(); //وظيفة مجردة:التحديث public abstract void update(); //وظيفة مجردة:الرسم public abstract void draw(Graphics2D g); //وظيفة مجردة:التجاوب مع المداخيل والأوامر المصدرة public abstract void handleInput(); }
وقد قمنا سابقا بتحديد حالتين في مثالنا لوحة الإختيارات و المستوى الأول للعبة.
لنقم ببرمجتهما. ولنبدأ بالفصيلة التي تجسد حالة لوحة الإختيارات.
لوحة الإختيارات ستحتوي إختيارين : بدء اللعبة و الخروج. يجب ان نستطيع تغيير الإختيارات عن طرق أسهم التوجيه فوق تحت من لوحة المفاتيح. تأكيد الإختيار نقوم به عن طريق زر الدخول.
public class MenuState extends GameState { //الإختيارات private final static int PLAY = 0; private final static int QUIT = 1; private final static int NBR_CHOICE = 2; //عدد الإختيارات private int currentChoice = 0; //الإختيار الحالي // الصور المستخدمة في لوحة الإختيارات private BufferedImage game_title;//عنوان اللعبة private BufferedImage ui_btn_start_normal;//إختيار البدءعادي private BufferedImage ui_btn_start_active;//إختيار البدء مفعل private BufferedImage ui_btn_quit_normal;//إختيار الخروج عادي private BufferedImage ui_btn_quit_active;//إختيار الخروج مفعل //تهيءة الفصيلة public MenuState(GameStateManager gsm) { super(gsm); init(); } //تحميل الموارد public void init() { game_title = load("/game_title.png"); ui_btn_start_normal = load("/ui_btn_start_normal.png"); ui_btn_start_active = load("/ui_btn_start_active.png"); ui_btn_quit_normal = load("/ui_btn_quit_normal.png"); ui_btn_quit_active = load("/ui_btn_quit_active.png"); } //وظيفة التحديث public void update() { //إدارة الأوامر handleInput(); } //وظيفة الرسم public void draw(Graphics2D g) { // لون خلفية اللوحة g.setColor(new Color(236, 236, 236)); g.fillRect(0, 0, GamePanel.WIDTH, GamePanel.HEIGHT); // رسم عنوان اللعبة g.drawImage(game_title, 0, 50, null); // رسم الإختيارات حسب حالتها if (currentChoice == PLAY) { g.drawImage(ui_btn_start_active, 214, 200, null); } else { g.drawImage(ui_btn_start_normal, 214, 200, null); } if (currentChoice == QUIT) { g.drawImage(ui_btn_quit_active, 214, 300, null); } else { g.drawImage(ui_btn_quit_normal, 214, 300, null); } } //إدارة الأوامر public void handleInput() { //تفعيل الإختيار if (Keys.isPressed(Keys.ENTER)){ select(); } //تغيير الإختيار الحالي if (Keys.isPressed(Keys.UP)) { if (currentChoice > 0) { currentChoice--; } } //تغيير الإختيار الحالي if (Keys.isPressed(Keys.DOWN)) { if (currentChoice < NBR_CHOICE - 1) { currentChoice++; } } } //تفعيل الإختيار private void select() { if (currentChoice == PLAY) { //نقوم بتغيير الحالة الحالية داخل مدير الحالات gsm.setState(GameStateManager.LEVEL1_STATE); } else if (currentChoice == QUIT) { System.exit(0); } } //تحميل الموارد public BufferedImage load(String s) { try { return ImageIO.read(MenuState.class.getResourceAsStream(s)); } catch (Exception e) {} return null; } }
فل ننتقل إلى إنشاء المستوى الأول من اللعبة. في هذه المرحلة من سلسلة الدروس المستوى الأول لا يحتوي على الكثير. فالهدف من هذا الدرس هو توضيح إدارة حالات اللعبة فقط. سنقوم بتطوير خصائص المستوى الأول في دروس لاحقة من هذه السلسلة.
المستوى الأول يرث أيظا من GameState ويحدد وظائفها المجردة
//حالة المستوى الأول public class Level1State extends GameState { //الوحش الذي نقوم بتحريكه في المثال private Monster monstre; //منشء الفصيلة public Level1State(GameStateManager gsm) { super(gsm); init(); } //تهيءة موارد الفصيلة public void init() { this.monstre = new Monster(GamePanel.WIDTH, GamePanel.HEIGHT); } //تحديث معلومات مكونات المستوى public void update() { handleInput(); this.monstre.update(); } //رسم وإظهار المستوى public void draw(Graphics2D g) { // رسم الخلفية g.setColor(new Color(130, 218, 242)); g.fillRect(0, 0, GamePanel.WIDTH, GamePanel.HEIGHT); g.setColor(new Color(177, 137, 73)); g.fillRect(0, GamePanel.HEIGHT - 128, GamePanel.WIDTH, GamePanel.HEIGHT); // رسم الوحش this.monstre.draw(g); } //إدارة الأوامر في المستوى public void handleInput() { this.monstre.left = Keys.isPressed(Keys.LEFT); this.monstre.right = Keys.isPressed(Keys.RIGHT); this.monstre.atack = Keys.isPressed(Keys.BUTTON_ATACK); // نعود للوحة الإختيارات عند الضغط على // Escape if (Keys.isPressed(Keys.ESCAPE)) { // تغيير الحالة الحالية // في مدير الحالات gsm.setState(GameStateManager.MENU_STATE); } } }
هذه الفصيلة يجب أن تكون قادرة على تحديد الحالة التي يجب تنفيذ وظائفها ( التحديث والرسم ).
لنرى كيف يمكننا برمجة هذا
//فصيلة إدارة الحالات public class GameStateManager { //جدول الحالات التي نود إدارتها private GameState[] gameStates; private int currentState; //الحالةالحالية //أكواد الحالات داخل اللعبة public static final int MENU_STATE = 0; public static final int LEVEL1_STATE = 1; //عدد الحالات التي نود إدارتها public static final int NUM_GAME_STATES = 2; //منشئ الفصيلة public GameStateManager() { gameStates = new GameState[NUM_GAME_STATES];//تهيئة جدول الحالات currentState = MENU_STATE;// تهيئة الحالة الحالية loadState(currentState);//تحميل الحالة الحالية } //وظيفة تحميل الحالات private void loadState(int state) { if (state == MENU_STATE) { gameStates[state] = new MenuState(this); } else if (state == LEVEL1_STATE) { gameStates[state] = new Level1State(this); } } //وظيفة تفريغ الحالات private void unloadState(int state) { gameStates[state] = null; } //تحديد الحالة الحالية public void setState(int state) { unloadState(currentState); currentState = state; loadState(currentState); } //تحديث الحالة الحالية public void update() { if (gameStates[currentState] != null) { gameStates[currentState].update(); } } //رسم الحالة الحالية public void draw(java.awt.Graphics2D g) { if (gameStates[currentState] != null) { gameStates[currentState].draw(g); } } }
لنقم بدمجها مع حلقة اللعبة.
//خيط اللعبة public class GameThread extends Thread { private boolean running; private int FPS = 60; private long targetTime = 1000 / FPS; private GamePanel gamePanel; private BufferedImage image; private Graphics2D g; //مدير الحالات private GameStateManager gsm; //... @Override public void run() { load(); long start; long elapsed; long wait; while (running) { start = System.currentTimeMillis(); update(); draw(); elapsed = System.currentTimeMillis() - start; wait = targetTime - elapsed / 1000; try { if (wait > 0) { Thread.sleep(wait); } } catch (Exception e) {} } } //... //وظيفة التحديث داخل حلقة اللعبة private void update() { //نقوم بإستدعاء وظيفة التحديث من مدير الحالات //الذي يحدد وظيفة التحديث للمرحلة الحالية gsm.update(); } private void draw() { //نقوم بإستدعاء وظيفة الرسم من مدير الحالات //الذي يحدد وظيفة الرسم للحالة الحالية gsm.draw(g); //إظهار الحالة على الشاشة Graphics2D g2 = (Graphics2D) gamePanel.getGraphics(); g2.drawImage(image, 0, 0, GamePanel.WIDTH * GamePanel.SCALE, GamePanel.HEIGHT * GamePanel.SCALE, null); g2.dispose(); } }