iOS SDK: بناء لعبة حقائق – منطق اللعبة |
مرحبا بك في الجزء الثالث و الأخير من لعبة الحقائق باستخدام سلسلة Sprite Kit التعليمية. ستتعلم في هذا الدرس استخدام اطار عمل Sprite Kit لانشاء لعبة حقائق مرتكزة على طرح أسئلة. تصمم اللعبة لكلا المستخدمين الجدد و المتقدمين. خلال هذه الفترة, ستقوم بتطبيق اساسيات Sprite Kit.
المقدمة
في هذا الدرس, ستقوم ببرمجة منطق اللعبة بالكامل بما في ذلك حياة اللاعب, السؤال و إجابة اللاعب. تقسم هذه السلسلة الى ثلاثة أجزاء: بدء تشغيل المشروع, واجهة الحقائق, و منطق اللعبة. في حال لم تكمل بعد الجزء الثاني, بامكانك تنزيل المشروع و المتابعة من حيث توقفت. سينتج كل جزء نتيجة عملية, و سينتج مجموع كل الأجزاء اللعبة النهائية. على الرغم من حقيقة أنه يمكن قراءة كل جزء بشكل مستقل, و لكن لفهم أفضل نقترح متابعة الدروس خطوة بخطوة. قمنا أيضا بتضمين كود المصدر لكل جزء بشكل منفصل. و بذلك نوفر طريقة لبدء الدرس من أي قسم بالسلسلة.
1. صنف الحقائق التقليدية
في اخر درس, قمت بتعريف ملف plist للأسئلة. يحتوي كل ملف على اربعة خصائص. من أجل التحكم بهم, عليك انشاء صنف تقليدي من أجل القيام بتلك المهمة بشكل مناسب. لذلك عليك انشاء صنف Objective-C اخر. اطلق عليها اسم factObject و عرف الصنف الفرعي NSObject.
دعونا الان نقم بتحرير ملف الرأس و إضافة خصائص plist الأربعة. لدى كل خاصية plist ميزاتها الخاصة:
المقدمة
في هذا الدرس, ستقوم ببرمجة منطق اللعبة بالكامل بما في ذلك حياة اللاعب, السؤال و إجابة اللاعب. تقسم هذه السلسلة الى ثلاثة أجزاء: بدء تشغيل المشروع, واجهة الحقائق, و منطق اللعبة. في حال لم تكمل بعد الجزء الثاني, بامكانك تنزيل المشروع و المتابعة من حيث توقفت. سينتج كل جزء نتيجة عملية, و سينتج مجموع كل الأجزاء اللعبة النهائية. على الرغم من حقيقة أنه يمكن قراءة كل جزء بشكل مستقل, و لكن لفهم أفضل نقترح متابعة الدروس خطوة بخطوة. قمنا أيضا بتضمين كود المصدر لكل جزء بشكل منفصل. و بذلك نوفر طريقة لبدء الدرس من أي قسم بالسلسلة.
1. صنف الحقائق التقليدية
في اخر درس, قمت بتعريف ملف plist للأسئلة. يحتوي كل ملف على اربعة خصائص. من أجل التحكم بهم, عليك انشاء صنف تقليدي من أجل القيام بتلك المهمة بشكل مناسب. لذلك عليك انشاء صنف Objective-C اخر. اطلق عليها اسم factObject و عرف الصنف الفرعي NSObject.
دعونا الان نقم بتحرير ملف الرأس و إضافة خصائص plist الأربعة. لدى كل خاصية plist ميزاتها الخاصة:
- يعد تعبير Id هو int.
- التعبير هو NSString.
- يعد تعبير isCorrect هو int.
- تعد المعلومات الإضافية هي NSString.
يجب أن تبدو النتيجة النهائية كالتالي:
@property(nonatomic,readwrite) int factID;
@property(nonatomic,readwrite,retain) NSString *statement;
@property(nonatomic,readwrite) NSInteger isCorrect;
@property(nonatomic,readwrite,retain) NSString *additionalInfo;
ليس عليك استخدام كلف الانشاء (.m). سنقوم بتحليل ملف plist الى هذا الصنف التقليدي و استخدام القيم مباشرة من الذاكرة.
2. واجهة الحقائق: عملية البدء
في اخر درس, قمت بتعريف البنية الأساسية لواجهة الحقائق. حان الوقت الان لتكمل الواجهة بخطوات منطق اللعبة. لاكمال اللعبة, نحتاج الى انشاء علامة السؤال, تعبير خلفية معدل, و زر يطلب سؤالا اخرا, و واجهة صحيحه و خاطئة. تترجم هذه التعبيرات الأربع الى خمسة خصائص معرفة في ملف FactsScene.h. مرة أخرى, يمكنك تسميتهم كما تشاء. الانشاء كالتالي:
@property (nonatomic, retain) UILabel *questionLabel;
@property (nonatomic, retain) SKSpriteNode *backgroundStatement;
@property (nonatomic, retain) UIButton *nextQuestion;
@property (nonatomic, retain) SKSpriteNode* wrong;
@property (nonatomic, retain) SKSpriteNode* correct;
انتبه الى FactsScene.m. يجب عليك تعريف عدة كائنات تستخدم بشكل داخلي في الصنف:
NSMutableArray من اجل تخزين الأسئلة
قيمة عشوائية تمثل سؤال عشوائي
معرف السؤال
العتبة الدنيا للاسئلة الصحيحة; تشير هذه العتبة الى اقل عدد من الإجابات صحيحة المطلوبة للانتقال الى المستوى التالي. في هذا الدرس, ستتعلم القيمة سبعة.
يجب ان يبدو ملف الانشاء كالتالي:
NSMutableArray *statements;
int randomQuestion;
int questionNumber;
int totalRightQuestions; // need 7 out of 10 to pass to the next level
حان الوقت لتعيين بعض القيم و البدء بمنطق اللعبة. في
-(id) initWithSize:(CGSize)size inLevel:(NSInteger)level withPlayerLives:(int)lives نقوم ببدء questionNumber و totalRightQuestions. بما أنها أول مرة تستخدمه, فان عملية البدء سهلة و يمكن أن تكون كالتالي:
questionNumber = 1;
totalRightQuestions=0;
حان الان الوقت لاستخدام الصنف التقليدي المعرف بالخطوة المذكورة انفا. قم بتحليل ملف plist و استخدام مخزن المعلومات في plist لتعيين و تأهيل كائنات factObject الجديدة. لاحظ أننا سنقوم بتخزين كل كائن factObject في NSMutableArray تقليدية معرفة مسبقا (تعبيرات). المقطع الكامل تراه بالاسفل.
statements = [[NSMutableArray alloc] init];
NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"LevelDescription" ofType:@"plist"];
NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:plistPath];
if ([dictionary objectForKey:@"Questions" ] != nil ){
NSMutableArray *array = [dictionary objectForKey:@"Questions"];
for(int i = 0; i < [array count]; i++){
NSMutableDictionary *questions = [array objectAtIndex:i];
factObject *stat = [factObject new];
stat.factID = [[questions objectForKey:@"id"] intValue];
stat.statement = [questions objectForKey:@"statement"];
stat.isCorrect = [[questions objectForKey:@"isCorrect"] integerValue];
stat.additionalInfo = [questions objectForKey:@"additionalInfo"];
[statements addObject:stat];
}
}
تزيل هذه الخطوة الكود التحليلية الاقدم من طريقة -(void) didMoveToView:(SKView *)view. يمكنك زالتها بما انك لن تستخدمها بعد الان.
3. واجهة الحقائق: المنطق
حان الوقت الان لنركز على كود منطق اللعبة بحد ذاته. نحتاج الى ان نري المستخدم السؤال. و لكن يعد السؤال خيارا عشوائيا بشكل دائم. ابدأ بتعريف مستطيلا لتقديم السؤال و من ثم لتعيين الموارد الضرورية لنص السؤال. سيقوم المقطع التالي بمساعدتك:
CGRect labelFrame = CGRectMake(120,300, 530, 100);
_questionLabel = [[UILabel alloc] initWithFrame:labelFrame];
randomQuestion = [self getRandomNumberBetween:0 to:([statements count]-1)];
NSString *labelText = [[statements objectAtIndex:randomQuestion] statement];
[_questionLabel setText:labelText];
[_questionLabel setTextColor:[UIColor whiteColor]];
[_questionLabel setFont:[UIFont fontWithName:NULL size:23]];
[_questionLabel setTextAlignment:NSTextAlignmentCenter];
// The label will use an unlimited number of lines
[_questionLabel setNumberOfLines:0];
لاحظ انك لن تستخدم SKLabelNode فوق NSString لان حدود ;SKLabelNode هذا من أجل نص من سطر واحد فقط. سيظهر تحذير فيما يتعلق بطريقة getRandomNumberBetween:0 to:X. يجب عليك أن تعلن عنها و تكتب الكود التابعة لها; يعود الهدف من ذلك الى ارجاع القيمة بين القيمتين. يقوم المقطع التالي بمساعدتك:
-(int)getRandomNumberBetween:(int)from to:(int)to {
return (int)from + arc4random() % (to-from+1);
}
يمكنك الان رؤية السؤال, يمكننا إضافة بعض الوظائف الى زر الخطأ و الصواب. غير كلا الخيارين و استدع طريقة جديدة تدعى: presentCorrectWrongMenu.
[_falseButton addTarget:self action:@selector(presentCorrectWrongMenu:) forControlEvents:UIControlEventTouchUpInside];
[_trueButton addTarget:self action:@selector(presentCorrectWrongMenu:) forControlEvents:UIControlEventTouchUpInside];
بالإضافة الى ذلك, قم بتعريف لاحقة لكل زر. ستكون tag = 1 لاحقة زر الصواب و tag = 0 لاحقة زر الخطأ. ستساعدك هذه اللواحق عند استدعاء طريقة the -(void)presentCorrectWrongMenu:(UIButton*)sender من أجل تحديد اية زر نقر لاستدعاء نفس تلك الطريقة.
[_trueButton setTag:1];
[_falseButton setTag:0];
تقضي الخطوة التالية بإضافة طريقة -(void)presentCorrectWrongMenu:(UIButton*)sender. تعد هذه الطريقة معقدة و تميز أية زر يتم النقر عليه, و تضيف واجهة الإجابة و تضيف أيضا زرا يقوم باستدعاء السؤال التالي. استخدم المقطع التالي لانجاز المواضيع انفا:
-(void)presentCorrectWrongMenu:(UIButton*)sender{
int userData = sender.tag;
// background
_backgroundStatement = [SKSpriteNode spriteNodeWithImageNamed:@"background.png"];
_backgroundStatement.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
_backgroundStatement.size = CGSizeMake(768, 1024);
_backgroundStatement.zPosition = 10;
_backgroundStatement.alpha = 0.0;
[self addChild:_backgroundStatement];
_nextQuestion = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_nextQuestion.frame = CGRectMake(CGRectGetMidX(self.frame)-100, CGRectGetMidY(self.frame)+90, 200, 70.0);
_nextQuestion.backgroundColor = [UIColor clearColor];
[_nextQuestion setTitleColor:[UIColor blackColor] forState:UIControlStateNormal ];
[_nextQuestion setTitle:@"Tap Here to Continue" forState:UIControlStateNormal];
[_nextQuestion addTarget:self action:@selector(nextQuestion) forControlEvents:UIControlEventTouchUpInside];
_nextQuestion.alpha = 1.0;
[self.view addSubview:_nextQuestion];
[_backgroundStatement runAction:[SKAction fadeAlphaTo:1.0f duration:0.2f]];
_trueButton.alpha = 0.0;
_falseButton.alpha = 0.0;
سيظهر تحذير, و لكن لا تثبتها فورا. أولا, انه اعلان الطريقة. و بعد ان قمت بانشاء الواجهة التقليدية للإجابة, يجب عليك اختبار إجابة اللاعب. من أجل انجاز ذلك, عليك معرفة أية زر قد نقر عليه اللاعب و إجابة السؤال الداخلي. انك على معرفة مسبقة بهذا, لذا عليك فقط انشاء حالة اختبار منطقي بسيط. فعل ذلك, عليك اختبار فيما اذا الإجابة كانت صحيحة أم خاطئة, اعزف الصوت تبعا لهذه النتيجة, و تابع تحديث الخواص. سيقوم المقطع التالي بمساعدتك. لاحظ انه يجب عليك وضعه حيث انتهى اخر مقطع كود.
if( ([[statements objectAtIndex:randomQuestion] isCorrect] == 0 && userData == 0) || ([[statements objectAtIndex:randomQuestion] isCorrect] == 1 && userData == 1) ){
if ([[statements objectAtIndex:randomQuestion] isCorrect] == 0)
_questionLabel.text = [[statements objectAtIndex:randomQuestion] additionalInfo];
_correct = [SKSpriteNode spriteNodeWithImageNamed:@"correct.png"];
_correct.scale = .6;
_correct.zPosition = 10;
_correct.position = CGPointMake(CGRectGetMidX(self.frame),800);
_correct.alpha = 1.0;
totalRightQuestions++;
[self touchWillProduceASound:@"True"];
[self addChild:_correct];
}
else{
if ([[statements objectAtIndex:randomQuestion] isCorrect] == 0)
_questionLabel.text = [[statements objectAtIndex:randomQuestion] additionalInfo];
_wrong = [SKSpriteNode spriteNodeWithImageNamed:@"wrong.png"];
_wrong.scale = .6;
_wrong.zPosition = 10;
_wrong.position = CGPointMake(CGRectGetMidX(self.frame),800);
_wrong.alpha = 1.0;
[self removePlayerLife];
[self touchWillProduceASound:@"False"];
[self addChild:_wrong];
}
}
هل تتذكر اخر تنبيه؟ يجب عليك الان رؤية التنبيهات الأخيرة. لا تقلق, انهم يحذرونك بأن هنالك عدة طرق مفقودة. يمكننا تصحيح ذلك. أول طريقة سنعرفها هي -(void)nextQuestion. و كما يوحي الاسم فان هذه الطريقة تستدعي السؤال التالي. بالاضافة الى تقديم سؤال جديد, فان الطريقة تعيد تعيين المؤقت, زيادة رقم السؤال, تحديث علامة السؤال الحالي, إزالة السؤال المعروض من المصفوفة و فحص المنطق اللازم للانتقال الى المستوى الاخر. كود المصدر الكاملة لـ -(void)nextQuestion هي:
-(void)nextQuestion{
[self resetTimer];
questionNumber++;
_currentLevelLabel.text = [[NSString alloc] initWithFormat:@"Level: %ld of 10", (long)questionNumber];
_wrong.alpha = 0.0;
_correct.alpha = 0.0;
_backgroundStatement.alpha = 0.0;
_nextQuestion.alpha = 0.0;
[statements removeObject:[statements objectAtIndex:randomQuestion]];
//random question
randomQuestion = [self getRandomNumberBetween:0 to:([statements count]-1)];
[_questionLabel setText:[[statements objectAtIndex:randomQuestion] statement]];
_trueButton.alpha = 1.0;
_falseButton.alpha = 1.0;
if (questionNumber == 10 && totalRightQuestions > 7){
int nexLevel = playerLevel+2;
[defaults setInteger:nexLevel forKey:@"actualPlayerLevel"];
[self removeUIViews];
SKTransition* transition = [SKTransition doorwayWithDuration:2];
LevelSelect* levelSelect = [[LevelSelect alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
[self.scene.view presentScene:levelSelect transition:transition];
}
}
لاحظ انك قمت بوضع كود صعبة لحد أعظم من الأسئلة لهذا المستوى (10) اسئلة و عتبة الانتقال الى المستوى التالي هي (7). مرة أخرى, سيظهر تحذيرا جديدا. لا يوجد طريقة resetTimer, تعيد هذه الطريقة تعيين فقط خاصية maximumTime الى 60 و تحديث الاعلامة تبعا لذلك:
-(void)resetTimer{
maximumTime = 60;
[_timerLevel setText:@"60"];
}
في اخر درس, قمت بتعريف طريقة touchWillProduceASound. و لكن في هذا الدرس, ستحتاج الى تعديلها أكثر. الهدف من ذلك تمريرها ككائن يمثل الإجابة الصحيحة أو الخاطئة. و من ثم سيتم عزف الصوت الموافق للإجابة. ستكون الطريقة الكاملة كالتالي:
-(void) touchWillProduceASound:(NSString*)answer{
long soundFlag = [defaults integerForKey:@"sound"];
if (soundFlag == 1){
SKAction* sound;
if ([answer isEqualToString:@"False"]) {
sound = [SKAction playSoundFileNamed:@"wrong.mp3" waitForCompletion:YES];
} else {
sound = [SKAction playSoundFileNamed:@"right.mp3" waitForCompletion:YES];
}
[self runAction:sound];
}
}
ما زلت تحتاج الى تعريف طريقة -(void)removePlayerLife. و كما يوحي الاسم, فان الطريقة تقوم باختبار حياة اللاعب و التصرف تبعا لذلك. اذا كان للاعب اكثر من حياة, فان الحياة ستنقص و سيتم تحديث الموجودات الداخلية أو الانتقال الى الشاشة الرئيسية. الطريقة الكاملة تجدها في الأسفل.
-(void)removePlayerLife{
if (playerLives > 1){
for(NSInteger i = 0; i < playerLives; i++){
SKSpriteNode* node = [heartArray objectAtIndex:i];
if (i == (playerLives-1)){
node.alpha = .1;
}
}
playerLives--;
} else {
[self moveToHome];
}
}
عند هذه المرحلة, شارفنا على الانتهاء. حان الوقت الان لتحديث - (void)updateTimer المعرفة بالدرس السابق. تعد هذه الطريقة مسؤولة عن تحديث قيمة المؤقت و اختبار حياة اللاعب. ستقوم بالتفاعل أوتوماتيكيا عندما يصبح المؤق صفر. عند ذلك الوقت, ستقوم باختبار حياة اللاعب و التصرف تبعا لذلك. ستقوم بالذهاب الى القائمة الرئيسية مباشرة في حال كانت حياة اللاعب أقل من واحد أو تستدعي سؤالا اخرا (انقاص حياة اللاعب). ترى كامل المقطع اسفلا:
- (void)updateTimer{
maximumTime--;
if (maximumTime == 0){
if (playerLives < 1){
[self touchWillProduceASound:@"False"];
[self moveToHome];
} else{
[self presentCorrectWrongMenu:_trueButton];
[self touchWillProduceASound:@"False"];
[self removePlayerLife];
}
}
[_timerLevel setText:[[NSNumber numberWithInt:maximumTime] stringValue]];
}
4. طرق اضافية
تم انشاء طريقتين اضافيتين: -(void)moveToHome و -(void)removeUIViews. يجب ان نعرفهم لأننا سنستخدمهم أكثر من مرة. يعتبر إعادة استخدام الكود جيدا بدلا من طباعتها أكثر من مرة أخرى. تعد -(void)moveToHome فقط استدعاء لصنف SKTransition و MyScene. الكود هي:
-(void)moveToHome{
SKTransition* transition = [SKTransition fadeWithDuration:2];
MyScene* myscene = [[MyScene alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
[self.scene.view presentScene:myscene transition:transition];
}
تزيل -(void)removeUIViews مشاهد UIKit من superview. هذا ما ستبدو عليه الكود:
-(void)removeUIViews{
[_trueButton removeFromSuperview];
[_falseButton removeFromSuperview];
[_questionLabel removeFromSuperview];
}
5. التحسينات الاخيرة
هنالك خطوة أخيرة قبل أن تنتهي. يجب علينا بدء actualPlayerLevel بشكل صحيح عند اختيار المستوى. حول انتباهك الى صنف MyScene.m (اول واحد قمت بإنشائه باستخدام Xcode) و دعنا نضيف بعض سطور من الكود. في بادئ الامر, قم بإضافة كائن من نوع NSUserDefaults الى قسم @implementation. سيقوم المقطع التالي بمساعدتك:
@implementation MyScene {
// code
NSUserDefaults* defaults;
}
قم بإضافة عملية البدء NSUserDefaults الداخلية الى داخل -(void) didMoveToView:(SKView *)view. تعد القيمة الافتراضية واحدة, لذا فان كل لاعب جديد يبدأ لعبة جديدة في المستوى 1. بالإضافة الى ذلك, في حال لم يحقق اللاعب الشروط الدنيا لربح المستوى, فانه يبدأ مرة أخرى من نفس المستوى. انظر الى النتيجة بالاسفل:
defaults = [NSUserDefaults standardUserDefaults];
[defaults setInteger:1 forKey:@"actualPlayerLevel"];
//more code..
هنالك عدة تعديلات يمكن تطبيقها على هه اللعبة. بامكانك تعديل معدلات الإجابات الصحيحة للاسئلة, للاحياء, للانتقالات أو تغيير المشهد او الصورة. نظن أنك جاهز لفعل تلك المهام. حاول تطبيقهم بنفسك.
الخاتمة
في نهاية دروس لعبة الحقائق, يجب ان تكون قادرا على انشاء لعبة SpriteKit, انشاء و تركيب عدة مشاهد و نشاطات SpriteKit, تشكيل عدة مشاهد SpriteKit, استخدامهم بشكل متناسب مع SpriteKit و تحيلي قوائم الخواص. لا تتردد بترك أي تعليق او اقتراح في صندوق التعليقات اسفلا. شكرا على القراءة!
ليست هناك تعليقات: