ألعاب المتصفح إنشاء السلالم وختام اللعبة


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

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

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

  • إنشاء أول سلّم
  • السماح للاعبين بتسلق السلالم
  • السماح للاعبين بالوقوف والقفز على السلالم

إنشاء أول سلّم

لنبدأ إنشاء سلّمنا بنسخ صنف الصندوق Box:

class Ladder {
    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 Ladder;
import Ladder from "./ladder";

game.addObject(
    new Ladder(
        new PIXI.Sprite.fromImage(
            "ladder.png"
        ),
        new PIXI.Rectangle(
            200, window.innerHeight - 192, 64, 192
        )
    )
);

game.addObject(
    new Box(
        new PIXI.Sprite.fromImage(
            "box.png"
        ),
        new PIXI.Rectangle(
            136, window.innerHeight - 191, 64, 64
        )
    )
);

ستحتاج إلى إنشاء صورة ladder.png للسلم؛ بالنسبة لي، اخترت استخدام شكلٍ بسيطٍ بمقاسات 64x192 بيكسيل. تأكد من إضافة الصورة إلى اللعبة قبل إضافة اللاعب وإلا سيكون الشكل الخاص بالسلم أمام الشكل الخاص اللاعب.

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

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 (object.collides && this.velocityY > 0 && you.y >= me.y) {
        this.velocityY = 0;
        this.grounded = true;
        this.jumping = false;
        return;
    }


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

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

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

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

السماح للاعبين بتسلق السلالم

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

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 = 20;
        this.accelerationY = 5;
        this.jumpVelocity = -40;

        this.climbingSpeed = 10;
    }

    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
        );

        var onLadder = false;

        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 (object.collides && this.velocityY > 0 && you.y >= me.y) {
                    this.velocityY = 0;
                    this.grounded = true;

                   this.jumping = false;
                    return;
                }

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

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

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

                if (object.constructor.name === "Ladder") {
                    onLadder = true;

                    if (state.keys[38] || state.keys[40]) {
                        this.grounded = false;
                        this.jumping = false;
                        this.climbing = true;

                        this.velocityY = 0;
                        this.velocityX = 0;
                    }

                    if (state.keys[38]) {
                        this.rectangle.y -= this.climbingSpeed;
                    }

                    if (state.keys[40] && me.y + me.height < you.y + you.height) {
                        this.rectangle.y += this.climbingSpeed;
                    }
                }
            }
        });

        if (!onLadder) {
            this.climbing = false;
        }

        if (state.keys[38] && this.grounded) {
            this.velocityY = this.jumpVelocity;
            this.jumping = true;
            this.grounded = false;
        }


        this.rectangle.x += this.velocityX;

        if (!this.climbing) {
            this.rectangle.y += this.velocityY;
        }

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

export default Player;

نبدأ بإضافة خاصية جديدة سنسميها التسلق (climbing). ستكون قيمتها صحيحة "true" عندما يكون اللاعب في وضع تسلق السلم. ننشِئ أيضًا متغيرًا onLadder محلي، حتى نتمكن من معرفة ما إذا كان لا يزال واقفًا على سلم.

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

إذا تم الضغط على السهم للأعلى، فإنّنا نرفع اللاعب لأعلى. إذا تم الضغط لأسفل، فإنّنا نحرّك اللاعب لأسفل حتى يصبح الجزء السفلي من اللاعب أدنى من الجزء السفلي للسلم. هذا يجعل السلالم السفلية لا تتيح للاعب أن يتجاهل التصادم مع الأرض.

السماح للاعبين بالوقوف على السلالم

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

class Box {
    collides(state) {
        var collides = false;

        state.objects.forEach((object) => {
            if (object.constructor.name !== "Player") {
                return;
            }

            let edges = this.getEdges(this, object);

            if (!(
                edges.boxTop > edges.playerBottom ||
                edges.boxRight < edges.playerLeft ||
                edges.boxBottom < edges.playerTop ||
                edges.boxLeft > edges.playerRight
            )) {
                collides = true;
                return;
            }
        });

        return collides;
    }

    getEdges(box, player) {
        return {
            "boxLeft" : box.rectangle.x,
            "boxRight" : box.rectangle.x + box.rectangle.width,
            "boxTop" : box.rectangle.y,
            "boxBottom" : box.rectangle.y + box.rectangle.height,
            "playerLeft" : player.rectangle.x,
            "playerRight" : player.rectangle.x + player.rectangle.width,
            "playerTop" : player.rectangle.y,
            "playerBottom" : player.rectangle.y + player.rectangle.height
        };
    }

    collidesInDirection(box, player) {
        let edges = this.getEdges(box, player);
        let offsetLeft = edges.playerRight - edges.boxLeft;
        let offsetRight = edges.boxRight - edges.playerLeft;
        let offsetTop = edges.playerBottom - edges.boxTop;
        let offsetBottom = edges.boxBottom - edges.playerTop;

        if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetTop) {
            return "↓";
        }

        if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetBottom) {
            return "↑";
        }

        if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetLeft) {
            return "→";
        }

        if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetRight) {
            return "←";
        }

        return "unknown";
    }

    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;

يستطيع صنف الصندوق Box الآن حساب ما إذا كان سيتصادم مع اللاعب. بمجرد اكتشاف التصادم بين لاعب وصندوق، يمكننا تنفيذ الدالّة التابع collidesInDirection. سنحصل على سهمٍ أنيقٍ UTF-8 يشير إلى الاتجاه الذي كان اللاعب يتحرك فيه قبل حدوث التصادم. أثناء إجراء هذا التغيير، حدث أنّ صنف السلم لا يقوم بأي شيء مختلف عن صنف الصندوق. يحدث مفهوم التسلق في فئة "اللاعب"، وبالتالي فإن فئة "السلم" يمكن أن تكون امتدادًا لفئة "الصندوق":

import Box from "./box";

class Ladder extends Box {

}

export default Ladder;

يبدو صنف اللّاعب أكثر أناقةً وتنظيمًا بعد نقل منطق التصادم خارجه:

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
    );

    var onLadder = false;

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

        if (object.collides(state)) {
            let type = object.constructor.name;
            let direction = object.collidesInDirection(object, this);

            if (type === "Ladder") {
                onLadder = true;

                let player = this.rectangle;
                let ladder = object.rectangle;

                if (state.keys[38] || state.keys[40]) {
                    this.grounded = false;
                    this.jumping = false;
                    this.climbing = true;
                    this.velocityY = 0;
                    this.velocityX = 0;
                }

                if (state.keys[38]) {
                    let limit = ladder.y - this.rectangle.height + 1;

                    this.rectangle.y = Math.max(
                        this.rectangle.y -= this.climbingSpeed,
                        limit
                    );

                    if (player.y === limit) {
                        this.grounded = true;
                        this.jumping = false;
                    }
                }

                if (state.keys[40] && player.y + player.height < ladder.y + ladder.height) {
                    this.rectangle.y += this.climbingSpeed;
                }

                return;
            }

            if (type === "Box") {
                if (direction === "↓") {
                    this.velocityY = 0;
                    this.grounded = true;
                    this.jumping = false;
                }

                if (direction === "↑") {
                    this.velocityY = this.accelerationY;
                }

                if (direction === "←" && this.velocityX < 0) {
                    this.velocityX = 0;
                }

                if (direction === "→" && this.velocityX > 0) {
                    this.velocityX = 0;
                }
            }
        }
    });

    if (!onLadder) {
        this.climbing = false;
    }

    if (state.keys[38] && this.grounded && !this.climbing) {
        this.velocityY = this.jumpVelocity;
        this.jumping = true;
        this.grounded = false;
    }

    this.rectangle.x += this.velocityX;

    if (!this.climbing) {
        this.rectangle.y += this.velocityY;
    }

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

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

يبدو الآن منطق التصادم المتعلّق بالصندوق أنقى وأبسط بكثير! وماهي النتيجة؟ سلالمٌ تعمل بشكل جيد.

ختام اللعبة: ماذا بعد؟

لقد علمتني هذه المقالات القليلة الكثير من الأمور، أو لِنَقُل أنّهما أمران بارزان على وجه التحديد.

الأول هو أن هناك قدرًا لا حصر له من الأشياء التي يمكننا إضافتها أو تغييرها في ألعابنا. والثاني هو أن هذا النشاط مربحٌ ومتعبٌ في الوقت ذاته.

لقد انطلقت من رغبةٍ في معرفة كيفية صنع لعبة منصة بسيطة، وبعدها قمنا معا بتغطية كل شيء تقريبا نحتاجه لبناء تلك اللعبة. لا أريد التوقف هنا، رغم ذلك. أريد أن أستمر في التعلم وصقل مهاراتي. أريد أن أستمر في مشاركة تلك التجارب معك.

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

اقرأ أيضًا





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


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

تمّ تعديل بواسطة هايز جاكلين

شارك هذا التعليق


رابط هذا التعليق
شارك على الشبكات الإجتماعية


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

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

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


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

تسجيل الدخول

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


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