مساحة اعلانية

آخر المواضيع

بناء Missile Command باستخدام تفاعل مستخدم Sprite Kit

بناء  Missile Command باستخدام تفاعل مستخدم Sprite Kit
بناء  Missile Command باستخدام تفاعل مستخدم Sprite Kit

في الدرس السابق, وضعنا اساسيات لعبة Missile Command من خلال انشاء المشروع, انشاء مشهد اللاعب الواحد و إضافة تفاعل المستخدم. في هذا الدرس, سنقوم بتمديد خبرة اللعبة من خلال إضافة وضع اللاعب المتعدد بالإضافة الى الفيزياء, الاصطدامات و التفجيرات.

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

1. تفعيل الفيزياء

يتضمن اطار عمل Sprite Kit محرك فيزيائي يعمل على تحفيز الكائنات الفيزيائية. يعمل محرك اطار عمل Sprite Kit الفيزيائي من خلال بروتوكول SKPhysicsContactDelegate. من أجل تفعيل المحرك الفيزيائي في اللعبة, نحتاج الى تعديل صنف MyScene. ابدأ بتحديث ملف الرأس كما هو موضح بالاسفل من أجل اخبار المجمع ان صنف SKScene يتوافق مع بروتوكول SKPhysicsContactDelegate.
#import <SpriteKit/SpriteKit.h>

@interface MyScene : SKScene <SKPhysicsContactDelegate>

@end



يمكننا بروتوكول SKPhysicsContactDelegate من اكتشاف فيما اذا اصطدم كائنان ببعضهما الاخر. يجب ان ينشئ نموذج MyScene بروتوكول SKPhysicsContactDelegate اذا أراد ان يتم اعلامه بحصول اصطدام بين الكائنات. يتم اعلام الكائن الذي يقوم بانشاء البروتوكول في أي وقت يبدأ و ينتهي الاصطدام.

بما اننا سنتعامل مع الانفجارات, و القذائف, و الوحوش, سنقوم بتعريف مجموعة لكل نوع من الكائنات الفيزيائية. اضف مقطع الكود التالي الى ملف الرأس من صنف MyScene.

#import <SpriteKit/SpriteKit.h>

typedef enum : NSUInteger {

    ExplosionCategory   = (1 << 0),

    MissileCategory     = (1 << 1),

    MonsterCategory     = (1 << 2)

} NodeCategory;

@interface MyScene : SKScene <SKPhysicsContactDelegate>

@end



قبل أن نقوم بالاطلاع على اطار عمل محرك  Sprite Kitالفيزيائي, يجب أن نعين خاصية الجاذبية لعالم الفيزياء بالإضافة الى contactDelegate الخاص به. قم بتحديث طريقة initWithSize: كما هو موضح بالاسفل.

      
- (id)initWithSize:(CGSize)size {

    if (self = [super initWithSize:size]) {

        self.backgroundColor = [SKColor colorWithRed:(198.0/255.0) green:(220.0/255.0) blue:(54.0/255.0) alpha:1.0];

       

        // ... //

       

        // Configure Physics World

        self.physicsWorld.gravity = CGVectorMake(0, 0);

        self.physicsWorld.contactDelegate = self;

    }

   

    return self;

}


في لعبتنا, يستخدم المحرك الفيزيائي لانشاء ثلاثة أنواع من الكائنات الفيزيائية, الرصاصات, القذائف, و الوحوش. عند العمل مع اطار عمل Sprite Kit, يجب استخدام احجام ثابتة و حركية لتحفيز الكائنات الفيزيائية. الحجم الخاص بمجموعة من الكائنات هو حجم يحوي على كل كائن من المجموعة. تعد الحجوم الثابتة و الحركية عنصر هام لتحسين أداء محرك الفيزياء, خصوصا عند العمل مع كائنات معقدة.  في اللعبة, سنقوم بتعريف نوعين من الحجوم, الدوائر مع اشعاع ثابت و كائنات تقليدية.

بينما تكون الدوائر متاحة في صنف SKPhysicsBody, يتطلب الكائن التقليدي عملا إضافيا من جهتك. لان جسد الوحش ليس دائريا, فانه يجب علينا انشاء حجم تقليدي له. من أجل جعل هذه المهمة أسهل بعض الشيء, سنقوم باستخدام مولد طريق كائن فيزيائي. تعد الأداة سهلة الاستخدام. قم باستيراد sprites و تعريف طريق الاغلاق لكل sprite. تقوم كود Objective-C بإعادة انشاء الطريق .

في حال لمس او تداخل أي كائن مع حدود أي كائن, سيتم اعلامنا بهذا الحدث. في اللعبة, الكائنات التي يمكنها ان تلمس الوحوش تعد القذائف القادمة. دعونا نبدأ باستخدام الطرق المولدة من أجل الوحوش.

من أجل انشاء كائن فيزيائي, سنقوم باستخدام بنية CGMutablePathRef, التي تمثل طريق متغير. سنستخدمه لتعريف مخطط الوحوش باللعبة.

قم بزيارة addMonstersBetweenSpace:: مرة أخرى, و انشاء طريق متغير لكل نوع وحش كما هو موضح اسفلا. تذكر انه هنالك نوعين للوحوش في اللعبة.      
- (void)addMonstersBetweenSpace:(int)spaceOrder {

    for (int i = 0; i< 3; i++) {

        int giveDistanceToMonsters = 60 * i -60;

        int randomMonster = [self getRandomNumberBetween:0 to:1];



        SKSpriteNode *monster;

        CGMutablePathRef path = CGPathCreateMutable();



        if (randomMonster == 0) {

            monster = [SKSpriteNode spriteNodeWithImageNamed:@"protectCreature4"];



            CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x;

            CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y;

            CGPathMoveToPoint(path, NULL, 10 - offsetX, 1 - offsetY);

            CGPathAddLineToPoint(path, NULL, 42 - offsetX, 0 - offsetY);

            CGPathAddLineToPoint(path, NULL, 49 - offsetX, 13 - offsetY);

            CGPathAddLineToPoint(path, NULL, 51 - offsetX, 29 - offsetY);

            CGPathAddLineToPoint(path, NULL, 50 - offsetX, 42 - offsetY);

            CGPathAddLineToPoint(path, NULL, 42 - offsetX, 59 - offsetY);

            CGPathAddLineToPoint(path, NULL, 29 - offsetX, 67 - offsetY);

            CGPathAddLineToPoint(path, NULL, 19 - offsetX, 67 - offsetY);

            CGPathAddLineToPoint(path, NULL, 5 - offsetX, 53 - offsetY);

            CGPathAddLineToPoint(path, NULL, 0 - offsetX, 34 - offsetY);

            CGPathAddLineToPoint(path, NULL, 1 - offsetX, 15 - offsetY);

            CGPathCloseSubpath(path);



        } else {

            monster = [SKSpriteNode spriteNodeWithImageNamed:@"protectCreature2"];



            CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x;

            CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y;

            CGPathMoveToPoint(path, NULL, 0 - offsetX, 1 - offsetY);

            CGPathAddLineToPoint(path, NULL, 47 - offsetX, 1 - offsetY);

            CGPathAddLineToPoint(path, NULL, 47 - offsetX, 24 - offsetY);

            CGPathAddLineToPoint(path, NULL, 40 - offsetX, 43 - offsetY);

            CGPathAddLineToPoint(path, NULL, 28 - offsetX, 53 - offsetY);

            CGPathAddLineToPoint(path, NULL, 19 - offsetX, 53 - offsetY);

            CGPathAddLineToPoint(path, NULL, 8 - offsetX, 44 - offsetY);

            CGPathAddLineToPoint(path, NULL, 1 - offsetX, 26 - offsetY);

            CGPathCloseSubpath(path);

        }



        monster.zPosition = 2;

        monster.position = CGPointMake(position * spaceOrder - giveDistanceToMonsters, monster.size.height / 2);



        [self addChild:monster];

    }

}



بما ان الطريق اصبح جاهزا, سنحتاج الى تحديث خاصية وحش physicsBody بالإضافة الى عدد من الخواص الأخرى. الق نظرة على مقطع الكود التالي من أجل التوضيح. 
- (void)addMonstersBetweenSpace:(int)spaceOrder {

    for (int i = 0; i< 3; i++) {

        // ... //

       

        monster.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];

        monster.physicsBody.dynamic = YES;

        monster.physicsBody.categoryBitMask = MonsterCategory;

        monster.physicsBody.contactTestBitMask = MissileCategory;

        monster.physicsBody.collisionBitMask = 1;

        monster.zPosition = 2;

        monster.position = CGPointMake(position * spaceOrder - giveDistanceToMonsters, monster.size.height / 2);

       

        [self addChild:monster];

    }

}

تعد خواص categoryBitMask و contactTestBitMask التابعة لكائن physicsBody جزء مهم و يمكن أن نحتاج بعض الشرح. تقوم خاصية categoryBitMask التابعة لكائن physicsBody بتعريف اية مجموعات تنتمي لها العقدة. تقوم خاصية contactTestBitMask بتعريف اية مجموعات من الكائنات تسبب تقاطع الاشعارات مع العقدة. بكلمات أخرى, تقوم هذه الخواص بتعريف اية كائنات يمكنها الاصطدام مع اية كائنات.

لأننا نقوم بتشكيل عقد الوحش, سنقوم بوضع categoryBitMask الى MonsterCategory و contactTestBitMask الى MissileCategory.

نحتاج أيضا الى تحديث انشاء addMissilesFromSky:. يعد تعريف كائن الفيزياء التابع للقذيفة اسهل بكثير بما ان كل قذيفة دائرية الشكل. الق نظرة على الانشاء المحدث اسفلا.
- (void)addMissilesFromSky:(CGSize)size {

    int numberMissiles = [self getRandomNumberBetween:0 to:3];



    for (int i = 0; i < numberMissiles; i++) {

        SKSpriteNode *missile;

        missile = [SKSpriteNode spriteNodeWithImageNamed:@"enemyMissile"];

        missile.scale = 0.6;

        missile.zPosition = 1;



        int startPoint = [self getRandomNumberBetween:0 to:size.width];

        missile.position = CGPointMake(startPoint, size.height);



        missile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:missile.size.height/2];

        missile.physicsBody.dynamic = NO;

        missile.physicsBody.categoryBitMask = MissileCategory;

        missile.physicsBody.contactTestBitMask = ExplosionCategory | MonsterCategory;

        missile.physicsBody.collisionBitMask = 1;



        int endPoint = [self getRandomNumberBetween:0 to:size.width];

        SKAction *move =[SKAction moveTo:CGPointMake(endPoint, 0) duration:15];

        SKAction *remove = [SKAction removeFromParent];

        [missile runAction:[SKAction sequence:@[move,remove]]];



        [self addChild:missile];

    }

}

عند هذه المرحلة, يجب ان يكون للوحوش و القذائف في اللعبة كائن فيزيائي يمكننا من كشف عندما يصطدم أي منهم بالاخر.

2. الاصطدام و الانفجارات

يعد الاصطدام و الانفجارات عنصرين مرتبطين مع بعضهما بشكل كبير. كل مرة يتم  فيها اطلاق رصاصة من خلال زهرة تصل الى وجهتها, لمسة المستخدم, تنفجر. يمكن لذلك الانفجار ان يسبب اصطدام بين الانفجار او أية قذائف في الجوار.

من اجل انشاء انفجار عندما تصل الرصاصة الى هدفها, نحتاج الى نموذج اخر SKAction. يعد نموذج SKAction مسؤولا عن ناحيتين من اللعبة, تعريف خواص التفجير و الكائن الفيزيائي للانفجار.

لتعريف الانفجار, نحتاج الى التركيز على SKSpriteNode الانفجار, zPosition الخاص به, المقياس و الموقع. يعد الموقع موقع لمسة المستخدم.

من اجل كائن فيزيائي للانفجار, نحتاج الى تعيين خاصية عقدة physicsBody كما فعلنا مسبقا. لا تنس ان تعيين بشكل صحيح خواص categoryBitMask و contactTestBitMask للكائن الفيزيائي. نقوم بانشاء انفجار في touchesBegan: كما هو موضح بالاسفل.

      
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    for (UITouch *touch in touches) {

        // ... //

       

        SKSpriteNode *bullet = [SKSpriteNode spriteNodeWithImageNamed:@"flowerBullet"];

        bullet.zPosition = 1;

        bullet.scale = 0.6;

        bullet.position = CGPointMake(bulletBeginning,110);

        bullet.color = [SKColor redColor];

        bullet.colorBlendFactor = 0.5;

        float duration = (2 * location.y)/sizeGlobal.width;

        SKAction *move =[SKAction moveTo:CGPointMake(location.x,location.y) duration:duration];

        SKAction *remove = [SKAction removeFromParent];

       

        // Explosion

        SKAction *callExplosion = [SKAction runBlock:^{

            SKSpriteNode *explosion = [SKSpriteNode spriteNodeWithImageNamed:@"explosion"];

            explosion.zPosition = 3;

            explosion.scale = 0.1;

            explosion.position = CGPointMake(location.x,location.y);

            explosion.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:explosion.size.height/2];

            explosion.physicsBody.dynamic = YES;

            explosion.physicsBody.categoryBitMask = ExplosionCategory;

            explosion.physicsBody.contactTestBitMask = MissileCategory;

            explosion.physicsBody.collisionBitMask = 1;

            SKAction *explosionAction = [SKAction scaleTo:0.8 duration:1.5];

            [explosion runAction:[SKAction sequence:@[explosionAction,remove]]];

            [self addChild:explosion];

        }];

       

        [bullet runAction:[SKAction sequence:@[move,callExplosion,remove]]];

       

        [self addChild:bullet];

    }

}




في touchesBegan:, قمنا بتحديث نشاط الرصاصة. يجب ان يقوم النشاط الجديد باستدعاء نشاط callExplosion قبل ازالته من المشهد. من اجل انجاز ذلك, سنقوم بتحديث سطر الكود التالي في touchesBegan:.
      
[bullet runAction:[SKAction sequence:@[move,remove]]];




تحتوي سلسلة النشاط الان على callExplosion كما هو موضح بالاسفل.
      
[bullet runAction:[SKAction sequence:@[move,callExplosion,remove]]];


قم ببناء و تشغيل التطبيق لترى نتيجة عملك. كما يمكنك ان ترى, فاننا ما زلنا بحاجة الى كشف التصادم بين الانفجارات و القذائف القادمة. هنا يأتي دور بروتوكول SKPhysicsContactDelegate.

هنالك طريقة تفويض واحدة تهمنا, الا و هي طريقة didBeginContact:. ستخبرنا هذه الطريقة عندما يحدث تصادم بين انفجار و قذيفة. تأخذ طريقة didBeginContact: معاملة واحدة, نموذج عن صنف SKPhysicsContact, الذي يخبرنا كل شيء نحتاجه عن الاصطدام. دعوني اشرح كيف يعمل ذلك.

لدى نموذج SKPhysicsContact خاصية bodyA و bodyB. يشير كل كائن الى كائن فيزيائي مشارك بالاصطدام. عندما تستدعى طريقة didBeginContact:, نحتاج الى كشف أي نوع من التصادم نتعامل معه. يمكن ان يكون تصادم (1) بين انفجار و قذيفة أو تصادم (2) بين قذيفة و وحش. نكشف نوع التصادم من خلال التحري عن خاصية categoryBitmask التابعة للكائنات الفيزيائية لدى نموذج SKPhysicsContact.

يعتبر كشف نوعية التصادم الذي نتعامل معه سهل جدا بفضل خاصية categoryBitmask. اذا كان لدى bodyA أو bodyB خاصية categoryBitmask من نوع ExplosionCategory, عندها سنعرف ان الاصطدام هو بين انفجار و قذيفة. الق نظرة على مقطع الكود اسفلا للتوضيح.    
- (void)didBeginContact:(SKPhysicsContact *)contact {

    if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) {

        NSLog(@"EXPLOSION HIT");

    } else {

        NSLog(@"MONSTER HIT");

    }

}

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

عندما يتم تدمير قذيفة, سنزيد من قدرة النموذج missileExploded المتاح و نقوم بتحديث العلامة التي تعرض عدد القذائف التي يقوم اللاعب بتدميرها. في حال قام اللاعب بتدمير عشرين قذيفة, فانه سيربح اللعبة.

- (void)didBeginContact:(SKPhysicsContact *)contact {

    if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) {

        // Collision Between Explosion and Missile

        SKNode *missile = (contact.bodyA.categoryBitMask & ExplosionCategory) ? contact.bodyB.node : contact.bodyA.node;

        [missile runAction:[SKAction removeFromParent]];

       

        //the explosion continues, because can kill more than one missile

        NSLog(@"Missile destroyed");

       

        // Update Missile Exploded

        missileExploded++;

        [labelMissilesExploded setText:[NSString stringWithFormat:@"Missiles Exploded: %d",missileExploded]];

       

        if(missileExploded == 20){

            SKLabelNode *ganhou = [SKLabelNode labelNodeWithFontNamed:@"Hiragino-Kaku-Gothic-ProN"];

            ganhou.text = @"You win!";

            ganhou.fontSize = 60;

            ganhou.position = CGPointMake(sizeGlobal.width/2,sizeGlobal.height/2);

            ganhou.zPosition = 3;

            [self addChild:ganhou];

        }

       

    } else {

        // Collision Between Missile and Monster

    }

}

في حال نتعامل مع اصطدام بين قذيفة و وحش, فننا سنقوم بإزالة عقدة القذيفة و الوحش من المشهد من خلال إضافة نشاط [SKAction removeFromParent] الى قائمة النشاطات المنفذة من قبل العقدة. نقوم أيضا بتعزيز نموذج monstersDead المتاح و التأكد فيما اذا كان مساويا لـ 6. اذا كان كذلك, فان اللاعب يكون قد خسر اللعبة و سنقوم بعرض رسالة تخبره بان اللعبة قد انتهت.       
- (void)didBeginContact:(SKPhysicsContact *)contact {

    if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) {

        // Collision Between Explosion and Missile

        // ... //

       

    } else {

        // Collision Between Missile and Monster

        SKNode *monster = (contact.bodyA.categoryBitMask & MonsterCategory) ? contact.bodyA.node : contact.bodyB.node;

        SKNode *missile = (contact.bodyA.categoryBitMask & MonsterCategory) ? contact.bodyB.node : contact.bodyA.node;

        [missile runAction:[SKAction removeFromParent]];

        [monster runAction:[SKAction removeFromParent]];

       

        NSLog(@"Monster killed");

        monstersDead++;

        if(monstersDead == 6){

            SKLabelNode *perdeu = [SKLabelNode labelNodeWithFontNamed:@"Hiragino-Kaku-Gothic-ProN"];

            perdeu.text = @"You Lose!";

            perdeu.fontSize = 60;

            perdeu.position = CGPointMake(sizeGlobal.width/2,sizeGlobal.height/2);

            perdeu.zPosition = 3;

            [self addChild:perdeu];

            [self moveToMenu];

        }

    }

}

قبل تشغيل اللعبة على iPad, نحتاج الى انشاء طريقة moveToMenu. تستدعى هذه الطريقة عندما يخسر اللاعب. في moveToMenu, تنتقل اللعبة الى مشهد القائمة لكي يبدأ اللاعب من جديد. لا تنس استيراد التصريح من صنف MenuScene.         

      
- (void)moveToMenu {

    SKTransition* transition = [SKTransition fadeWithDuration:2];

    MenuScene* myscene = [[MenuScene alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];

    [self.scene.view presentScene:myscene transition:transition];

}

      

#import "MyScene.h"



#import "MenuScene.h"



@interface MyScene () {

    // ... //

}



@end



حان الوقت لبناء المشروع و تشغيل اللعبة لترى النتيجة النهائية.

التحدي: تتمثل تحديات هذا القسم بالتالي.

قم بتغيير قوانين اللعبة من خلال تعديل عدد الوحوش و الرصاصات.

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


3. اللاعب المتعدد

في وضع اللاعب المتعدد, يمكن للاعبين ان يتحديا بعضهما الاخر من خلال وضع الشاشة المنقسمة. لا يمكن لوضع اللاعب المتعدد ان يغير اللعبة بحد ذاتها. يكمن الاختلاف بين اللاعب المنفرد و المتعدد بالقائمة اسفلا.
  1. نحتاج الى مجموعتين من الممتلكات.
  2. يجب تحديث موقع و توجه الممتلكات.
  3. نحتاج الى انشاء منطق للعبة من أجل اللاعب الثاني.
  4. يجب فحص الانفجارات و فهم اساس ما قبل الانفجار.
  5. يمكن للاعب واحد ان يربح.
  6. في وضع اللاعب المتعدد, يجب ان تبدو اللعبة كالصورة لتالية.
  7. وضع اللاعب المتعدد في اللعبة

ها هنا اخر تحد في الدرس. انها ليست بهذا التعقيد كما تبدو. يهدف التحد الى إعادة انشاء لعبة Missile Command من خلال تفعيل وضع اللاعب المتعدد. تحوي ملف مصدر هذه الدروس على مشاريع Xcode, يعد( Missile Command اللعب المتعدد) واحدا منها و هو يحوي على انشاء كامل لوضع اللاعب المتعدد. لاحظ ان صنف MultiScene غير مكتمل و بذلك فان مهمتك هي انهاء الانشاء بنجاح لاكمال التحدي. ستجد تلميحات و تعليقات (/* Work HERE - CODE IS MISSING */) لمساعدتك بهذا التحدي.

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

الخاتمة

لقد قمنا بتغطية الكثير في هذه السلسة القصيرة عن Sprite Kit.يجب ان تكون قادرا الان على انشاء العاب مشابهة لـ Missile Command باستخدام اطار عمل Sprite Kit. اذا كان لديك أي تعليق او سؤال, لا تتردد بتركه في صندوق التعليقات.

الكــاتــب

    • مشاركة

ليست هناك تعليقات:

جميع الحقوق محفوظة لــ الشبح للمعلوميات 2019 ©