عبد الصمد العماري

سنعمل هذه المرة على بنية الشيفرة لدينا، ونضيف الجاذبية إلى لعبتنا. ويجدر الذكر أننا قمنا بالفعل بمعظم العمل اللازم لتحقيق الجاذبية.

إليك ما سنعمل عليه في هذه المرحلة:

  • تنظيف الشيفرة الموجودة لدينا
  • إضافة الجاذبية إلى لعبتنا
  • السماح للاعبين بالقفز

تنظيف الشيفرة الموجودة لدينا

نحن بحاجة لتنظيف بعض الأشياء! دعنا في البداية نبدّل موضعي x و y إضافةً إلى العرض والارتفاع (بالنسبة للصندوق وللاعب) مع PIXI.Rectangle. إنّ لديهما هذه الخصائص، ولكنهما يتفاعلان أيضًا مع باقي عناصر PIXI بطرق مثيرة للاهتمام.

class Box {
    constructor(sprite, rectangle) {
        this.sprite = sprite;
        this.rectangle = rectangle;
    }

    animate(state) {
        if (this.sprite) {
            this.sprite.x = this.rectangle.x;
            this.sprite.y = this.rectangle.y;
        }
    }
}


export default Box;
class Player {
    constructor(sprite, rectangle) {
        this.sprite = sprite;
        this.rectangle = rectangle;

        this.velocityX = 0;
        this.maximumVelocityX = 12;
        this.accelerationX = 2;
        this.frictionX = 0.9;
    }


    animate(state) {
        if (state.keys[37]) {
            this.velocityX = Math.max(
                this.velocityX - this.accelerationX,
                this.maximumVelocityX * -1
            );
        }

        if (state.keys[39]) {

           this.velocityX = Math.min(
                this.velocityX + this.accelerationX,
                this.maximumVelocityX
            );
        }

        this.velocityX *= this.frictionX;

        var move = true;

        state.objects.forEach((object) => {
            if (object === this) {
                return;
            }

            var me = this.rectangle;
            var you = object.rectangle;

            if (me.x < you.x + you.width &&
                me.x + me.width > you.x &&
                me.y < you.y + you.height &&
                me.height + me.y > you.y) {

                if (this.velocityX < 0 && you.x <= me.x) {
                    move = false;
                }

                if (this.velocityX > 0 && you.x >= me.x) {
                    move = false;
                }
            }
        });

        if (move) {
            this.rectangle.x += this.velocityX;
        }

        this.sprite.x = this.rectangle.x;
        this.sprite.y = this.rectangle.y;

    }
}

export default Player;

هل لاحظت هذا المقدار من الشيفرة الذي نستطيع حذفه؟ لقد أصبحت عمليات الموازنة طويلة قليلاً، لكن لا شيء يمكن إصلاحه من خلال متغير محليّ واحد أو اثنين. أدركتُ أيضًا أنه يمكننا تغيير قيم x و y الابتدائية من أجل التحريك. بعد ذلك، أرغب في تغليف (encapsulation) الحدث والعارض والمسرح في صنف (class) جديدة خاصّة باللعبة (Game):

class Game {
    constructor() {
        this.state = {
            "keys": {},
            "clicks": {},
            "mouse": {},
            "objects": []
        };
    }

    get stage() {
        if (!this._stage) {
            this._stage = this.newStage();
        }

        return this._stage;
    }

    set stage(stage) {
        this._stage = stage;
    }

    newStage() {
        return new PIXI.Container();
    }

    get renderer() {
        if (!this._renderer) {
            this._renderer = this.newRenderer();
        }

        return this._renderer;
    }

    set renderer(renderer) {
        this._renderer = renderer;
    }

    newRenderer() {
        return new PIXI.autoDetectRenderer(
            window.innerWidth,
            window.innerHeight,
            this.newRendererOptions()
        );
    }

    newRendererOptions() {
        return {
            "antialias": true,
            "autoResize": true,
            "transparent": true,
            "resolution": 2
        };
    }

    animate() {
        var caller = () => {
            requestAnimationFrame(caller);

            this.state.renderer = this.renderer;
            this.state.stage = this.stage;

            this.state.objects.forEach((object) => {
                object.animate(this.state);
            })

            this.renderer.render(this.stage);
        };

        caller();

        return this;
    }

    addEventListenerToElement(element) {
        element.addEventListener("keydown", (event) => {
            this.state.keys[event.keyCode] = true;
        });

        element.addEventListener("keyup", (event) => {
            this.state.keys[event.keyCode] = false;
        });

        element.addEventListener("mousedown", (event) => {
            this.state.clicks[event.which] = {
                "clientX": event.clientX,
                "clientY": event.clientY
            };
        });

        element.addEventListener("mouseup", (event) => {
            this.state.clicks[event.which] = false;
        });

        element.addEventListener("mousemove", (event) => {
            this.state.mouse.clientX = event.clientX;
            this.state.mouse.clientY = event.clientY;
        });

        return this;
    }

    addRendererToElement(element) {
        element.appendChild(this.renderer.view);

        return this;
    }

    addObject(object) {
        this.state.objects.push(object);

        if (object.sprite) {
            this.stage.addChild(object.sprite);
        }

        return this;
    }
}
export default Game;

لاحِظ دالّتي الجالب والضابط الخاصتين بصنفٍ ES6. إنهما مفيدتان لملء التبعيات الاختيارية حسب الحاجة. يمكننا تجاهل المصيّر والمسرح إذا كنا بحاجة إلى ذلك، ولكن لديهما قيم افتراضية معقولة أيضًا.

هذا، على الأغلب، مجرد نقل للشيفرة التي كانت في main.js إلى game.js. الاختلاف الوحيد الملحوظ هو أننا لم نعد نحتاج إلى إضافة الأشكال إلى المسرح بشكل منفصل عن إضافة الكائنات إلى الحالة (state).

أنا محتارٌ حول هذا الموضوع. فمن ناحية، يكون الأمر أكثر وضوحًا إذا أضفناها إلى المسرح يدويًا. ومن ناحية أخرى، هل سنضيف أشكالًا (مرتبطة بالكائنات) دون إضافة الكائنات؟ لا أعتقد ذلك.

ربما سنعود ونغير ذلك. في الوقت الحالي، تبدو الشيفرة أنظف قليلاً. إذن، كيف تبدو main.js الآن؟

import Game from "./game";
import Player from "./player";
import Box from "./box";

var game = new Game();

game.addObject(
    new Box(
        new PIXI.Sprite.fromImage(
            "box.png"
        ),
        new PIXI.Rectangle(
            window.innerWidth * 1/4, 152, 64, 64
        )
    )
);

game.addObject(
    new Player(
        new PIXI.Sprite.fromImage(
            "player.png"
        ),
        new PIXI.Rectangle(
            window.innerWidth * 2/4, 200, 64, 64
        )
    )
);

game.addObject(
    new Box(
        new PIXI.Sprite.fromImage(
            "box.png"
        ),
        new PIXI.Rectangle(
            window.innerWidth * 3/4, 248, 64, 64
        )
    )
);

game.addEventListenerToElement(window);
game.addRendererToElement(document.body);
game.animate();

هذا يبدو أفضل بكثير! لدينا سيطرة كافية لتعيين نقاط الانطلاق لكل كائنٍ في اللعبة. ولكن بعد الإطار الأول، تبدأ الدالّة التابع ()animate في العمل. ستبدأ أشياء مثل الجاذبية والتصادمات في التحكم في كيفية تقدم اللعبة. لقد كان الأمر هكذا دائمًا، لذلك يبدو هذا الملف كذلك أيضًا.

كان بإمكاننا أن نحصُر الأحداث والعارض في مجموعة صغيرة من العناصر. كما كنا نستطيع إضافة أي عدد من الكائنات إلى اللعبة من هنا. كل شيء آخر مرتبط باللعبة يوجد داخل فئة اللعبة. وكل شيء آخر مرتبط باللاعب أو الصندوق يوجد داخل الفئة الخاصّة به. هكذا تبدو الأمور أنيقة ومنظمة!

إضافة الجاذبية إلى لعبتنا

أحد الأشياء التي تجعل ألعاب المنصة ممتعةً هو وجود قدرٍ معتدلٍ من الأجسام في محيط اللعب. دعنا نضيف الجدران والأرضية:

var game = new Game();

game.addObject(
    new Box(
        null,
        new PIXI.Rectangle(
            -10, 0, 10, window.innerHeight
        )
    )
);

game.addObject(
    new Box(
        null,
        new PIXI.Rectangle(
            0, window.innerHeight, window.innerWidth, 10
        )
    )
);

game.addObject(
    new Box(
        null,
        new PIXI.Rectangle(
            window.innerWidth, 0, 10, window.innerHeight
        )
    )
);

game.addObject(
    new Player(
        new PIXI.Sprite.fromImage(
            "player.png"
        ),
        new PIXI.Rectangle(
            window.innerWidth * 2/4, 200, 64, 64
        )
    )
);

بدلاً من مربعين أحمرين، لدينا ثلاثة مستطيلات رفيعة. إنها تتصادم بنفس الطريقة. الآن، دعنا نرى كيف نضيف هذه الجاذبية:

class Player {
    constructor(sprite, rectangle) {
        this.sprite = sprite;
        this.rectangle = rectangle;

        this.velocityX = 0;
        this.maximumVelocityX = 12;
        this.accelerationX = 2;
        this.frictionX = 0.9;

        this.velocityY = 0;
        this.maximumVelocityY = 30;
        this.accelerationY = 10;
        this.jumpVelocity = -50;
    }

    animate(state) {
        if (state.keys[37]) {
            this.velocityX = Math.max(
                this.velocityX - this.accelerationX,
                this.maximumVelocityX * -1
            );
        }

        if (state.keys[39]) {
            this.velocityX = Math.min(
                this.velocityX + this.accelerationX,
                this.maximumVelocityX
            );
        }

        this.velocityX *= this.frictionX;

        this.velocityY = Math.min(
            this.velocityY + this.accelerationY,
            this.maximumVelocityY
        );

        state.objects.forEach((object) => {
            if (object === this) {
                return;
            }

            var me = this.rectangle;
            var you = object.rectangle;

            if (me.x < you.x + you.width &&
                me.x + me.width > you.x &&
                me.y < you.y + you.height &&
                me.y + me.height > you.y) {

                if (this.velocityY > 0 && you.y >= me.y) {
                    this.velocityY = 0;
                    return;
                }

                if (this.velocityY < 0 && you.y <= me.y) {
                    this.velocityY = this.accelerationY;
                    return;
                }

                if (this.velocityX < 0 && you.x <= me.x) {
                    this.velocityX = 0;
                    return;
                }

                if (this.velocityX > 0 && you.x >= me.x) {
                    this.velocityX = 0;
                    return;
                }
            }
        });

        this.rectangle.x += this.velocityX;
        this.rectangle.y += this.velocityY;

        this.sprite.x = this.rectangle.x;
        this.sprite.y = this.rectangle.y;
    }
}

export default Player;

نبدأ بإنشاء مجموعة من الخصائص لتتناسب مع تلك التي أنشأناها لتتبع الحركة الأفقية. لا نحتاج إلى احتكاك عمودي، إذ أنّ هذا المستوى من التفاصيل يُحذَف غالبا من ألعاب المنصّات.

علينا أيضًا أن نتتبع التصادمات العمودية والأفقية. عندما يكون التصادم بين اللاعب والمنصة أو الأرضية، فإننا نوقف حركة الهبوط. وعندما يكون مع السقف، فإننا نستبدل الحركة الصعودية بقوة الجاذبية.

السماح للاعبين بالقفز

القفز هو ببساطة مقاومة للجاذبية لفترة قصيرة:

 

animate(state) {
    if (state.keys[37]) {
        this.velocityX = Math.max(
            this.velocityX - this.accelerationX,
            this.maximumVelocityX * -1
        );
    }
 
    if (state.keys[39]) {
        this.velocityX = Math.min(
            this.velocityX + this.accelerationX,
            this.maximumVelocityX
        );
    }
 
    this.velocityX *= this.frictionX;
 
    this.velocityY = Math.min(
        this.velocityY + this.accelerationY,
        this.maximumVelocityY
    );
 
    state.objects.forEach((object) => {
        if (object === this) {
            return;
        }
 
        var me = this.rectangle;
        var you = object.rectangle;
 
        if (me.x < you.x + you.width &&
            me.x + me.width > you.x &&
            me.y < you.y + you.height &&
            me.y + me.height > you.y) {
 
            if (this.velocityY > 0 && you.y >= me.y) {
                this.velocityY = 0;

               this.grounded = true;
                this.jumping = false;
                return;
            }
 
            if (this.velocityY < 0 && you.y <= me.y) {
                this.velocityY = this.accelerationY;
                return;
            }
 
            if (this.velocityX < 0 && you.x <= me.x) {
                this.velocityX = 0;
                return;
            }
 
            if (this.velocityX > 0 && you.x >= me.x) {
                this.velocityX = 0;
                return;
            }
        }
    });
 
    if (state.keys[32] && !this.jumping && this.grounded) {
        this.velocityY = this.jumpVelocity;
        this.jumping = true;
        this.grounded = false;
    }
 
    this.rectangle.x += this.velocityX;
    this.rectangle.y += this.velocityY;
 
    this.sprite.x = this.rectangle.x;
    this.sprite.y = this.rectangle.y;
}

لقد قمنا هنا بتعيين مفتاح المسافة من أجل حركة القفز. نضيف فحص لوحة المفاتيح بعد فحص التصادم لأننا نريد لاعبنا أن يقفز فقط إذا كان يقف على منصة أو أرضية. يمكنك الآن إنشاء مستوياتٍ انطلاقًا من الصناديق. تستطيع كذلك إعطاءها بنيةً مرئيةً، والقفز من حولها. تمتع بقضاء بعض الوقت في إنشاء المستوى والقفز من خلاله!

ترجمة -وبتصرف- للمقال Gravity لصاحبه Christopher Pitt

اقرأ أيضًا

المقال التالي: إنشاء السلالم وختام اللعبة

المقال السابق: كشف التصادمات





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن