كيفية بناء لعبة SpriteKit في Swift 3 (الجزء 3)
نشرت: 2022-03-10هل تساءلت يومًا عما يتطلبه الأمر لإنشاء لعبة SpriteKit؟ هل تبدو الأزرار مهمة أكبر مما ينبغي؟ هل تساءلت يومًا كيف تستمر في الإعدادات في اللعبة؟ لم يكن صنع الألعاب أسهل من أي وقت مضى على iOS منذ تقديم SpriteKit. في الجزء الثالث من هذه السلسلة المكونة من ثلاثة أجزاء ، سننهي لعبة RainCat الخاصة بنا ونكمل مقدمتنا إلى SpriteKit.
إذا فاتك الدرس السابق ، فيمكنك اللحاق بالحصول على الكود على GitHub. تذكر أن هذا البرنامج التعليمي يتطلب Xcode 8 و Swift 3.
مزيد من القراءة على SmashingMag: Link
- Gamification و UX: حيث يفوز المستخدمون أو يخسرون
- تصميم UX ممتع: بناء لعبة أفضل
- الجمع بين تصميم UX وعلم النفس لتغيير سلوك المستخدم
هذا هو الدرس الثالث في رحلتنا RainCat. في الدرس السابق ، قضينا يومًا طويلًا من خلال بعض الرسوم المتحركة البسيطة وسلوكيات القطط والمؤثرات الصوتية السريعة والموسيقى الخلفية.
اليوم سوف نركز على ما يلي:
- عرض الرؤوس (HUD) للتسجيل ؛
- القائمة الرئيسية - مع الأزرار.
- خيارات لكتم الأصوات ؛
- خيار الإقلاع عن اللعبة.
المزيد من الأصول
أصول الدرس الأخير متوفرة على GitHub. اسحب الصور إلى Assets.xcassets
مرة أخرى ، تمامًا كما فعلنا في الدروس السابقة.
انتباه!
نحن بحاجة إلى وسيلة للحفاظ على النتيجة. للقيام بذلك ، يمكننا إنشاء شاشة عرض رأس (HUD). سيكون هذا بسيطًا جدًا ؛ سيكون SKNode
الذي يحتوي على النتيجة وزرًا لإنهاء اللعبة. في الوقت الحالي ، سنركز فقط على النتيجة. الخط الذي سنستخدمه هو Pixel Digivolve ، والذي يمكنك الحصول عليه من Dafont.com. كما هو الحال مع استخدام الصور أو الأصوات التي ليست لك ، اقرأ ترخيص الخط قبل استخدامه. ينص هذا على أنه مجاني للاستخدام الشخصي ، ولكن إذا كنت تحب الخط حقًا ، فيمكنك التبرع للمؤلف من الصفحة. لا يمكنك دائمًا أن تصنع كل شيء بنفسك ، لذا فإن رد الجميل لأولئك الذين ساعدوك على طول الطريق أمر جيد.
بعد ذلك ، نحتاج إلى إضافة الخط المخصص إلى المشروع. يمكن أن تكون هذه العملية صعبة في المرة الأولى.
قم بتنزيل الخط ونقله إلى مجلد المشروع ، ضمن مجلد "الخطوط". لقد فعلنا ذلك عدة مرات في الدروس السابقة ، لذلك سنخوض هذه العملية بسرعة أكبر. أضف مجموعة باسم Fonts
إلى المشروع ، وأضف ملف Pixel digivolve.otf
.
وهنا يظهر الجزء الخداع. إذا فاتك هذا الجزء ، فربما لن تتمكن من استخدام الخط. نحتاج إلى إضافته إلى ملف Info.plist
بنا. هذا الملف موجود في الجزء الأيمن من Xcode. انقر فوقه وسترى قائمة الممتلكات (أو plist
). انقر بزر الماوس الأيمن فوق القائمة ، ثم انقر فوق "إضافة صف".
عندما يظهر الصف الجديد ، أدخل ما يلي:
Fonts provided by application
بعد ذلك ، ضمن Item 0
، نحتاج إلى إضافة اسم الخط الخاص بنا. يجب أن تبدو plist
كما يلي:
يجب أن يكون الخط جاهزًا للاستخدام! يجب أن نجري اختبارًا سريعًا للتأكد من أنه يعمل على النحو المنشود. انتقل إلى GameScene.swift
، وفي sceneDidLoad
أضف الكود التالي في الجزء العلوي من الوظيفة:
let label = SKLabelNode(fontNamed: "PixelDigivolve") label.text = "Hello World!" label.position = CGPoint(x: size.width / 2, y: size.height / 2) label.zPosition = 1000 addChild(label)
هل يعمل؟
إذا نجحت ، فهذا يعني أنك فعلت كل شيء بشكل صحيح. إذا لم يكن كذلك ، فهناك خطأ ما. يحتوي Code With Chris على دليل أكثر تعمقًا لاستكشاف الأخطاء وإصلاحها ، ولكن لاحظ أنه مخصص لإصدار قديم من Swift ، لذلك سيتعين عليك إجراء تعديلات طفيفة لإحضاره إلى Swift 3.
الآن بعد أن أصبح بإمكاننا التحميل في الخطوط المخصصة ، يمكننا البدء في HUD. احذف تسمية "Hello World" ، لأننا استخدمناها فقط للتأكد من تحميل خطنا. سيكون HUD بمثابة SKNode
، يعمل مثل حاوية لعناصر HUD الخاصة بنا. هذه هي نفس العملية التي اتبعناها عند إنشاء عقدة الخلفية في الدرس الأول.
قم بإنشاء ملف HudNode.swift
باستخدام الطرق المعتادة ، وأدخل الكود التالي:
import SpriteKit class HudNode : SKNode { private let scoreKey = "RAINCAT_HIGHSCORE" private let scoreNode = SKLabelNode(fontNamed: "PixelDigivolve") private(set) var score : Int = 0 private var highScore : Int = 0 private var showingHighScore = false /// Set up HUD here. public func setup(size: CGSize) { let defaults = UserDefaults.standard highScore = defaults.integer(forKey: scoreKey) scoreNode.text = "\(score)" scoreNode.fontSize = 70 scoreNode.position = CGPoint(x: size.width / 2, y: size.height - 100) scoreNode.zPosition = 1 addChild(scoreNode) } /// Add point. /// - Increments the score. /// - Saves to user defaults. /// - If a high score is achieved, then enlarge the scoreNode and update the color. public func addPoint() { score += 1 updateScoreboard() if score > highScore { let defaults = UserDefaults.standard defaults.set(score, forKey: scoreKey) if !showingHighScore { showingHighScore = true scoreNode.run(SKAction.scale(to: 1.5, duration: 0.25)) scoreNode.fontColor = SKColor(red:0.99, green:0.92, blue:0.55, alpha:1.0) } } } /// Reset points. /// - Sets score to zero. /// - Updates score label. /// - Resets color and size to default values. public func resetPoints() { score = 0 updateScoreboard() if showingHighScore { showingHighScore = false scoreNode.run(SKAction.scale(to: 1.0, duration: 0.25)) scoreNode.fontColor = SKColor.white } } /// Updates the score label to show the current score. private func updateScoreboard() { scoreNode.text = "\(score)" } }
قبل أن نفعل أي شيء آخر ، افتح Constants.swift
وأضف السطر التالي إلى أسفل الملف - سنستخدمه لاسترداد النتيجة العالية واستمرارها:
let ScoreKey = "RAINCAT_HIGHSCORE"
في الكود ، لدينا خمسة متغيرات تتعلق بلوحة النتائج. المتغير الأول هو SKLabelNode
الفعلي ، والذي نستخدمه لتقديم الملصق. التالي هو المتغير لدينا للاحتفاظ بالدرجة الحالية ؛ ثم المتغير الذي يحمل أفضل نتيجة. المتغير الأخير هو منطقي يخبرنا ما إذا كنا نقدم حاليًا الدرجة العالية (نستخدم هذا لتحديد ما إذا كنا بحاجة إلى تشغيل SKAction
لزيادة مقياس لوحة النتائج وتلوينها إلى اللون الأصفر للأرضية).
الوظيفة الأولى ، setup(size:)
، موجودة فقط لإعداد كل شيء. قمنا بإعداد SKLabelNode
بالطريقة نفسها التي فعلناها سابقًا. لا تحتوي فئة SKNode
على أي خصائص حجم افتراضيًا ، لذلك نحتاج إلى إنشاء طريقة لتعيين حجم لوضع تسمية scoreNode
بنا. نحن أيضًا نحصل على الدرجة العالية الحالية من UserDefaults
. هذه طريقة سريعة وسهلة لحفظ أجزاء صغيرة من البيانات ، لكنها ليست آمنة. نظرًا لأننا لسنا قلقين بشأن الأمان في هذا المثال ، UserDefaults
يعد جيدًا تمامًا.
في addPoint()
بنا ، نقوم بزيادة متغير score
الحالي والتحقق مما إذا كان المستخدم قد حصل على درجة عالية. إذا كانت لديهم درجة عالية ، فسنحفظ هذه النتيجة في UserDefaults
مما إذا كنا نعرض حاليًا أفضل نتيجة. إذا حقق المستخدم درجة عالية ، فيمكننا تحريك حجم ولون scoreNode
.
في وظيفة resetPoints()
، قمنا بتعيين الدرجة الحالية على 0
. نحتاج بعد ذلك إلى التحقق مما إذا كنا نعرض الدرجة العالية ، وإعادة تعيين الحجم واللون إلى القيم الافتراضية إذا لزم الأمر.
أخيرًا ، لدينا وظيفة صغيرة تسمى updateScoreboard
. هذه وظيفة داخلية لتعيين النتيجة إلى نص scoreNode
. يسمى هذا في كل من addPoint()
و resetPoints()
.
تركيب شاشة HUD
نحتاج إلى اختبار ما إذا كان HUD الخاص بنا يعمل بشكل صحيح. انتقل إلى GameScene.swift
، وأضف السطر التالي أسفل متغير foodNode
في أعلى الملف:
private let hudNode = HudNode()
أضف السطرين التاليين في وظيفة sceneDidLoad()
، بالقرب من الأعلى:
hudNode.setup(size: size) addChild(hudNode)
ثم ، في وظيفة spawnCat()
، قم بإعادة تعيين النقاط في حالة سقوط القطة عن الشاشة. أضف السطر التالي بعد إضافة كائن القط إلى المشهد:
hudNode.resetPoints()
بعد ذلك ، في handleCatCollision(contact:)
، نحتاج إلى إعادة ضبط النتيجة مرة أخرى عندما يصطدم القط بالمطر. في بيان switch
في نهاية الوظيفة - عندما يكون الجسم الآخر من RainDropCategory
- أضف السطر التالي:
hudNode.resetPoints()
أخيرًا ، نحتاج إلى إخبار لوحة النتائج عندما يكسب المستخدم نقاطًا. في نهاية الملف في handleFoodHit(contact:)
، ابحث عن الأسطر التالية حتى هنا:
//TODO increment points print("fed cat")
واستبدلها بهذا:
hudNode.addPoint()
هاهو!
يجب أن تشاهد HUD أثناء العمل. نلف وندور وجمع بعض الطعام. في المرة الأولى التي تجمع فيها الطعام ، يجب أن ترى النتيجة تتحول إلى اللون الأصفر وتنمو في الحجم. عندما ترى هذا يحدث ، دع القطة تضرب. إذا تمت إعادة تعيين النتيجة ، فستعرف أنك على المسار الصحيح!
المشهد التالي
هذا صحيح ، نحن ننتقل إلى مشهد آخر! في الواقع ، عند الانتهاء ، ستكون هذه هي الشاشة الأولى لتطبيقنا. قبل أن تفعل أي شيء آخر ، افتح Constants.swift
وأضف السطر التالي إلى أسفل الملف - سنستخدمه لاسترداد النتيجة العالية واستمرارها:
let ScoreKey = "RAINCAT_HIGHSCORE"
قم بإنشاء مشهد جديد ، ضعه تحت مجلد "Scenes" ، MenuScene.swift
. أدخل الكود التالي في ملف MenuScene.swift
:
import SpriteKit class MenuScene : SKScene { let startButtonTexture = SKTexture(imageNamed: "button_start") let startButtonPressedTexture = SKTexture(imageNamed: "button_start_pressed") let soundButtonTexture = SKTexture(imageNamed: "speaker_on") let soundButtonTextureOff = SKTexture(imageNamed: "speaker_off") let logoSprite = SKSpriteNode(imageNamed: "logo") var startButton : SKSpriteNode! = nil var soundButton : SKSpriteNode! = nil let highScoreNode = SKLabelNode(fontNamed: "PixelDigivolve") var selectedButton : SKSpriteNode? override func sceneDidLoad() { backgroundColor = SKColor(red:0.30, green:0.81, blue:0.89, alpha:1.0) //Set up logo - sprite initialized earlier logoSprite.position = CGPoint(x: size.width / 2, y: size.height / 2 + 100) addChild(logoSprite) //Set up start button startButton = SKSpriteNode(texture: startButtonTexture) startButton.position = CGPoint(x: size.width / 2, y: size.height / 2 - startButton.size.height / 2) addChild(startButton) let edgeMargin : CGFloat = 25 //Set up sound button soundButton = SKSpriteNode(texture: soundButtonTexture) soundButton.position = CGPoint(x: size.width - soundButton.size.width / 2 - edgeMargin, y: soundButton.size.height / 2 + edgeMargin) addChild(soundButton) //Set up high-score node let defaults = UserDefaults.standard let highScore = defaults.integer(forKey: ScoreKey) highScoreNode.text = "\(highScore)" highScoreNode.fontSize = 90 highScoreNode.verticalAlignmentMode = .top highScoreNode.position = CGPoint(x: size.width / 2, y: startButton.position.y - startButton.size.height / 2 - 50) highScoreNode.zPosition = 1 addChild(highScoreNode) } }
نظرًا لأن هذا المشهد بسيط نسبيًا ، فلن ننشئ أي فئات خاصة. سيتكون مشهدنا من زرين. يمكن أن تكون هذه (وربما تستحق أن تكون) فئة خاصة بها من SKSpriteNodes
، ولكن نظرًا لاختلافها بدرجة كافية ، فلن نحتاج إلى إنشاء فئات جديدة لها. هذه نصيحة مهمة عند إنشاء لعبتك الخاصة: يجب أن تكون قادرًا على تحديد مكان التوقف وإعادة صياغة الكود عندما تصبح الأمور معقدة. بمجرد إضافة أكثر من ثلاثة أو أربعة أزرار للعبة ، فقد يكون الوقت قد حان للتوقف وإعادة صياغة رمز زر القائمة في فئته الخاصة.
الكود أعلاه لا يفعل أي شيء خاص ؛ إنها تحدد أوضاع أربعة نقوش متحركة. نقوم أيضًا بتعيين لون خلفية المشهد ، بحيث تكون الخلفية بأكملها هي القيمة الصحيحة. أداة رائعة لإنشاء رموز الألوان من سلاسل HEX لـ Xcode هي UI Color. يقوم الكود أعلاه أيضًا بتعيين القوام لحالات الزر الخاصة بنا. زر بدء اللعبة له حالة طبيعية وحالة مضغوطة ، في حين أن زر الصوت هو مفتاح تبديل. لتبسيط الأشياء للتبديل ، سنقوم بتغيير قيمة ألفا لزر الصوت عند ضغط المستخدم. نحن أيضًا SKLabelNode
عالي الجودة.
MenuScene
لدينا جيدة جدا. نحتاج الآن إلى إظهار المشهد عند تحميل التطبيق. انتقل إلى GameViewController.swift
وابحث عن السطر التالي:
let sceneNode = GameScene(size: view.frame.size)
استبدلها بهذا:
let sceneNode = MenuScene(size: view.frame.size)
سيؤدي هذا التغيير الصغير إلى تحميل MenuScene
افتراضيًا ، بدلاً من GameScene
.
الدول زر
يمكن أن تكون الأزرار صعبة في SpriteKit. يتوفر الكثير من خيارات الجهات الخارجية (حتى أنني قمت بعمل واحد بنفسي) ، لكن نظريًا ما عليك سوى معرفة طرق اللمس الثلاث:
-
touchesBegan(_ touches: with event:)
-
touchesMoved(_ touches: with event:)
-
touchesEnded(_ touches: with event:)
لقد غطينا هذا الأمر بإيجاز عند تحديث المظلة ، لكننا نحتاج الآن إلى معرفة ما يلي: أي زر تم لمسه ، وما إذا كان المستخدم قد أطلق نقره أو نقر على هذا الزر ، وما إذا كان المستخدم لا يزال يلمسها. هذا هو المكان الذي يلعب فيه متغير الزر selectedButton
. عندما تبدأ اللمسة ، يمكننا التقاط الزر الذي بدأ المستخدم النقر فوقه باستخدام هذا المتغير. إذا قاموا بالسحب خارج الزر ، فيمكننا التعامل مع هذا وإعطاء النسيج المناسب له. عندما يتركون اللمس ، يمكننا بعد ذلك معرفة ما إذا كانوا لا يزالون يلمسون داخل الزر. إذا كانت كذلك ، فيمكننا تطبيق الإجراء المرتبط بها. أضف الأسطر التالية إلى الجزء السفلي من MenuScene.swift
:
override func touchesBegan(_ touches: Set, with event: UIEvent?) { if let touch = touches.first { if selectedButton != nil { handleStartButtonHover(isHovering: false) handleSoundButtonHover(isHovering: false) } // Check which button was clicked (if any) if startButton.contains(touch.location(in: self)) { selectedButton = startButton handleStartButtonHover(isHovering: true) } else if soundButton.contains(touch.location(in: self)) { selectedButton = soundButton handleSoundButtonHover(isHovering: true) } } } override func touchesMoved(_ touches: Set, with event: UIEvent?) { if let touch = touches.first { // Check which button was clicked (if any) if selectedButton == startButton { handleStartButtonHover(isHovering: (startButton.contains(touch.location(in: self)))) } else if selectedButton == soundButton { handleSoundButtonHover(isHovering: (soundButton.contains(touch.location(in: self)))) } } } override func touchesEnded(_ touches: Set, with event: UIEvent?) { if let touch = touches.first { if selectedButton == startButton { // Start button clicked handleStartButtonHover(isHovering: false) if (startButton.contains(touch.location(in: self))) { handleStartButtonClick() } } else if selectedButton == soundButton { // Sound button clicked handleSoundButtonHover(isHovering: false) if (soundButton.contains(touch.location(in: self))) { handleSoundButtonClick() } } } selectedButton = nil } /// Handles start button hover behavior func handleStartButtonHover(isHovering : Bool) { if isHovering { startButton.texture = startButtonPressedTexture } else { startButton.texture = startButtonTexture } } /// Handles sound button hover behavior func handleSoundButtonHover(isHovering : Bool) { if isHovering { soundButton.alpha = 0.5 } else { soundButton.alpha = 1.0 } } /// Stubbed out start button on click method func handleStartButtonClick() { print("start clicked") } /// Stubbed out sound button on click method func handleSoundButtonClick() { print("sound clicked") }
هذا هو التعامل البسيط مع الأزرار لأزرارنا. في touchesBegan(_ touches: with events:)
، نبدأ بالتحقق مما إذا كان لدينا أي أزرار محددة حاليًا. إذا فعلنا ذلك ، نحتاج إلى إعادة تعيين حالة الزر إلى "غير مضغوط". بعد ذلك ، نحتاج إلى التحقق مما إذا تم الضغط على أي زر. إذا تم الضغط على أحد ، فسيظهر الحالة المميزة للزر. بعد ذلك ، قمنا بتعيين الزر selectedButton
على الزر لاستخدامه في الطريقتين الأخريين.
في touchesMoved(_ touches: with events:)
، نتحقق من الزر الذي تم لمسه في الأصل. بعد ذلك ، نتحقق مما إذا كانت اللمسة الحالية لا تزال ضمن حدود الزر selectedButton
، ونقوم بتحديث الحالة المميزة من هناك. تغير الحالة المميزة لـ startButton
النسيج إلى نسيج الحالة المضغوطة ، حيث تكون الحالة المميزة لـ soundButton
لها قيمة ألفا للكائن على 50٪.
أخيرًا ، في touchesEnded(_ touches: with event:)
، نتحقق مرة أخرى من الزر الذي تم تحديده ، إن وجد ، ثم ما إذا كان اللمس لا يزال ضمن حدود الزر. إذا تم استيفاء جميع الحالات ، فإننا نسمي handleStartButtonClick()
أو handleSoundButtonClick()
للزر الصحيح.
حان وقت العمل
الآن بعد أن أصبح لدينا سلوك الزر الأساسي معطلاً ، نحتاج إلى تشغيل حدث عند النقر عليه. الزر الأسهل للتنفيذ هو زر startButton
. عند النقر ، نحتاج فقط إلى تقديم GameScene
. قم بتحديث handleStartButtonClick()
في وظيفة MenuScene.swift
إلى الكود التالي:
func handleStartButtonClick() { let transition = SKTransition.reveal(with: .down, duration: 0.75) let gameScene = GameScene(size: size) gameScene.scaleMode = scaleMode view?.presentScene(gameScene, transition: transition) }
إذا قمت بتشغيل التطبيق الآن والضغط على الزر ، فستبدأ اللعبة!
الآن نحن بحاجة إلى تنفيذ تبديل كتم الصوت. لدينا بالفعل مدير صوت ، لكننا نحتاج إلى أن نكون قادرين على إخباره ما إذا كان كتم الصوت قيد التشغيل أو الإيقاف. في Constants.swift
، نحتاج إلى إضافة مفتاح للاستمرار عند تشغيل كتم الصوت. أضف السطر التالي:
let MuteKey = "RAINCAT_MUTED"
سنستخدم هذا لحفظ قيمة منطقية في UserDefaults
. الآن بعد أن تم إعداد هذا ، يمكننا الانتقال إلى SoundManager.swift
. هذا هو المكان الذي سنتحقق فيه UserDefaults
لمعرفة ما إذا كان كتم الصوت قيد التشغيل أو الإيقاف. في الجزء العلوي من الملف ، ضمن متغير trackPosition
، أضف السطر التالي:
private(set) var isMuted = false
هذا هو المتغير الذي تتحقق منه القائمة الرئيسية (وأي شيء آخر من شأنه تشغيل الصوت) لتحديد ما إذا كان الصوت مسموحًا به أم لا. نقوم بتهيئته على أنه false
، لكننا الآن بحاجة إلى التحقق من UserDefaults
لمعرفة ما يريده المستخدم. استبدل وظيفة init()
بما يلي:
private override init() { //This is private, so you can only have one Sound Manager ever. trackPosition = Int(arc4random_uniform(UInt32(SoundManager.tracks.count))) let defaults = UserDefaults.standard isMuted = defaults.bool(forKey: MuteKey) }
الآن بعد أن أصبح لدينا قيمة افتراضية لـ isMuted
، نحتاج إلى القدرة على تغييرها. أضف الكود التالي إلى أسفل SoundManager.swift
:
func toggleMute() -> Bool { isMuted = !isMuted let defaults = UserDefaults.standard defaults.set(isMuted, forKey: MuteKey) defaults.synchronize() if isMuted { audioPlayer?.stop() } else { startPlaying() } return isMuted }
ستعمل هذه الطريقة على تبديل المتغير الذي تم كتمه ، بالإضافة إلى تحديث UserDefaults
. إذا لم يتم كتم صوت القيمة الجديدة ، سيبدأ تشغيل الموسيقى ؛ إذا تم كتم صوت القيمة الجديدة ، فلن يبدأ التشغيل. خلاف ذلك ، سنوقف تشغيل المسار الحالي. بعد ذلك ، نحتاج إلى تعديل عبارة if
في startPlaying()
.
ابحث عن السطر التالي:
if audioPlayer == nil || audioPlayer?.isPlaying == false {
واستبدله بهذا:
if !isMuted && (audioPlayer == nil || audioPlayer?.isPlaying == false) {
الآن ، إذا تم إيقاف تشغيل كتم الصوت ولم يتم ضبط مشغل الصوت أو لم يعد مشغل الصوت الحالي قيد التشغيل ، فسنقوم بتشغيل المسار التالي.
من هنا ، يمكننا العودة إلى MenuScene.swift
لإنهاء زر كتم الصوت. استبدل handleSoundbuttonClick()
بالكود التالي:
func handleSoundButtonClick() { if SoundManager.sharedInstance.toggleMute() { //Is muted soundButton.texture = soundButtonTextureOff } else { //Is not muted soundButton.texture = soundButtonTexture } }
يؤدي هذا إلى تبديل الصوت في SoundManager
، والتحقق من النتيجة ثم ضبط النسيج بشكل مناسب لإظهار ما إذا كان الصوت مكتومًا أم لا. نحن على وشك الإنتهاء! نحتاج فقط إلى ضبط النسيج الأولي للزر عند التشغيل. في sceneDidLoad()
، ابحث عن السطر التالي:
soundButton = SKSpriteNode(texture: soundButtonTexture)
واستبدله بهذا:
soundButton = SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ? soundButtonTextureOff : soundButtonTexture)
يستخدم المثال أعلاه عامل تشغيل ثلاثي لتعيين النسيج الصحيح.
الآن بعد توصيل الموسيقى ، يمكننا الانتقال إلى CatSprite.swift
لتعطيل مواء القطة عند تشغيل الكتم. في hitByRain()
، يمكننا إضافة عبارة if
التالية بعد إزالة إجراء المشي:
if SoundManager.sharedInstance.isMuted { return }
سيعرض هذا البيان ما إذا كان المستخدم قد كتم صوت التطبيق أم لا. لهذا السبب ، سوف نتجاهل تمامًا currentRainHits
maxRainHits
و maxRainHits والمواء.
بعد كل ذلك ، حان الوقت الآن لتجربة زر كتم الصوت. قم بتشغيل التطبيق وتحقق مما إذا كان يقوم بتشغيل الأصوات وكتم صوتها بشكل مناسب. كتم الصوت وأغلق التطبيق وأعد فتحه. تأكد من استمرار إعداد كتم الصوت. لاحظ أنه في حالة كتم صوت التطبيق وإعادة تشغيله من Xcode ، فربما لم تمنح وقتًا كافيًا لحفظ UserDefaults
. العب اللعبة وتأكد من عدم تموء القطة أبدًا عند كتم صوتك.
الخروج من اللعبة
الآن بعد أن أصبح لدينا النوع الأول من الأزرار للقائمة الرئيسية ، يمكننا الدخول في بعض الأعمال الصعبة عن طريق إضافة زر إنهاء إلى مشهد لعبتنا. يمكن أن تأتي بعض التفاعلات الشيقة مع أسلوب لعبتنا ؛ حاليًا ، ستنتقل المظلة إلى أي مكان يلمس فيه المستخدم أو يحرك لمسته. من الواضح أن انتقال المظلة إلى زر إنهاء عندما يحاول المستخدم الخروج من اللعبة يعد تجربة مستخدم سيئة للغاية ، لذلك سنحاول منع حدوث ذلك.
زر الإنهاء الذي نطبقه سيحاكي زر بدء اللعبة الذي أضفناه سابقًا ، مع بقاء الكثير من العملية على حالها. سيكون التغيير في كيفية تعاملنا مع اللمسات. احصل على quit_button
و quit_button_pressed
في ملف Assets.xcassets
، وأضف الكود التالي إلى ملف HudNode.swift
:
private var quitButton : SKSpriteNode! private let quitButtonTexture = SKTexture(imageNamed: "quit_button") private let quitButtonPressedTexture = SKTexture(imageNamed: "quit_button_pressed")
سيعالج هذا quitButton
، جنبًا إلى جنب مع الزخارف التي سنقوم بتعيينها لحالات الزر. للتأكد من أننا لا نقوم بتحديث المظلة عن غير قصد أثناء محاولة الإقلاع ، نحتاج إلى متغير يخبر HUD (ومشهد اللعبة) أننا نتفاعل مع زر إنهاء وليس المظلة. أضف الكود التالي أسفل المتغير المنطقي showingHighScore
:
private(set) var quitButtonPressed = false
مرة أخرى ، هذا متغير يمكن لـ HudNode
فقط تعيينه ولكن يمكن للفئات الأخرى التحقق منه. الآن بعد أن تم إعداد المتغيرات الخاصة بنا ، يمكننا إضافة الزر إلى HUD. أضف الكود التالي إلى وظيفة setup(size:)
:
quitButton = SKSpriteNode(texture: quitButtonTexture) let margin : CGFloat = 15 quitButton.position = CGPoint(x: size.width - quitButton.size.width - margin, y: size.height - quitButton.size.height - margin) quitButton.zPosition = 1000 addChild(quitButton)
سيضبط الكود أعلاه زر إنهاء مع نسيج حالة عدم الضغط لدينا. نقوم أيضًا بتعيين الموضع على الزاوية العلوية اليمنى ونقوم بتعيين موضع zPosition
على رقم كبير من أجل إجباره على الرسم دائمًا في الأعلى. إذا قمت بتشغيل اللعبة الآن ، فستظهر في GameScene
، لكنها لن تكون قابلة للنقر بعد.
الآن بعد أن تم وضع الزر ، نحتاج إلى أن نكون قادرين على التفاعل معه. في الوقت الحالي ، المكان الوحيد الذي لدينا فيه تفاعل في GameScene
هو عندما نتفاعل مع umbrellaSprite
. في مثالنا ، ستحظى HUD بالأولوية على المظلة ، بحيث لا يضطر المستخدمون إلى تحريك المظلة بعيدًا عن الطريق للخروج. يمكننا إنشاء نفس الوظائف في HudNode.swift
لتقليد وظيفة اللمس في GameScene.swift
. أضف الكود التالي إلى HudNode.swift
:
func touchBeganAtPoint(point: CGPoint) { let containsPoint = quitButton.contains(point) if quitButtonPressed && !containsPoint { //Cancel the last click quitButtonPressed = false quitButton.texture = quitButtonTexture } else if containsPoint { quitButton.texture = quitButtonPressedTexture quitButtonPressed = true } } func touchMovedToPoint(point: CGPoint) { if quitButtonPressed { if quitButton.contains(point) { quitButton.texture = quitButtonPressedTexture } else { quitButton.texture = quitButtonTexture } } } func touchEndedAtPoint(point: CGPoint) { if quitButton.contains(point) { //TODO tell the gamescene to quit the game } quitButton.texture = quitButtonTexture }
الكود أعلاه يشبه إلى حد كبير الكود الذي أنشأناه لـ MenuScene
. الفرق هو أن هناك زرًا واحدًا فقط لتتبعه ، حتى نتمكن من التعامل مع كل شيء ضمن طرق اللمس هذه. أيضًا ، نظرًا لأننا سنعرف موقع اللمس في GameScene
، يمكننا فقط التحقق مما إذا كان الزر يحتوي على نقطة اللمس.
انتقل إلى GameScene.swift
، واستبدل touchesBegan(_ touches with event:)
و touchesMoved(_ touches: with event:)
بالشفرة التالية:
override func touchesBegan(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { hudNode.touchBeganAtPoint(point: point) if !hudNode.quitButtonPressed { umbrellaNode.setDestination(destination: point) } } } override func touchesMoved(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { hudNode.touchMovedToPoint(point: point) if !hudNode.quitButtonPressed { umbrellaNode.setDestination(destination: point) } } } override func touchesEnded(_ touches: Set, with event: UIEvent?) { let touchPoint = touches.first?.location(in: self) if let point = touchPoint { hudNode.touchEndedAtPoint(point: point) } }
هنا ، تتعامل كل طريقة مع كل شيء بنفس الطريقة تقريبًا. نخبر HUD أن المستخدم قد تفاعل مع المشهد. بعد ذلك ، نتحقق مما إذا كان زر الإنهاء يلتقط اللمسات حاليًا. إذا لم يكن كذلك ، فإننا نحرك المظلة. لقد أضفنا أيضًا touchesEnded(_ touches: with event:)
للتعامل مع نهاية النقر لزر الإنهاء ، لكننا ما زلنا لا نستخدمها في umbrellaSprite
.
الآن بعد أن أصبح لدينا زر ، نحتاج إلى طريقة لجعله يؤثر على GameScene
. أضف السطر التالي إلى أعلى HudeNode.swift
:
var quitButtonAction : (() -> ())?
هذا إغلاق عام لا يحتوي على مدخلات ولا مخرجات. سنقوم بتعيين هذا مع الكود في ملف GameScene.swift
عندما نضغط على الزر في HudNode.swift
. بعد ذلك ، يمكننا استبدال TODO
في الكود الذي أنشأناه سابقًا في touchEndedAtPoint(point:)
بهذا:
if quitButton.contains(point) && quitButtonAction != nil { quitButtonAction!() }
الآن ، إذا قمنا بتعيين إغلاق quitButtonAction
، فسيتم استدعاؤه من هذه النقطة.
لإعداد إغلاق quitButtonAction
، نحتاج إلى الانتقال إلى GameScene.swift
. في sceneDidLoad()
، يمكننا استبدال إعداد HUD الخاص بنا بالشفرة التالية:
hudNode.setup(size: size) hudNode.quitButtonAction = { let transition = SKTransition.reveal(with: .up, duration: 0.75) let gameScene = MenuScene(size: self.size) gameScene.scaleMode = self.scaleMode self.view?.presentScene(gameScene, transition: transition) self.hudNode.quitButtonAction = nil } addChild(hudNode)
قم بتشغيل التطبيق ، واضغط على تشغيل ، ثم اضغط على إنهاء. إذا عدت إلى القائمة الرئيسية ، فإن زر الإنهاء الخاص بك يعمل على النحو المنشود. في الخاتمة التي أنشأناها ، قمنا بتهيئة الانتقال إلى MenuScene
. وقمنا بتعيين هذا الإغلاق على عقدة HUD
للتشغيل عند النقر فوق زر الإنهاء. سطر آخر مهم هنا هو عندما قمنا بتعيين quitButtonAction
على nil
. والسبب في ذلك هو حدوث دورة الاحتفاظ. يحتوي المشهد على إشارة إلى HUD حيث تحتفظ HUD بإشارة إلى المشهد. نظرًا لوجود مرجع لكلا الكائنين ، فلن يتم التخلص من أي منهما عندما يحين وقت جمع البيانات المهملة. في هذه الحالة ، في كل مرة ندخل فيها إلى GameScene
، سيتم إنشاء مثيل آخر منه ولن يتم إصداره أبدًا. هذا مضر بالأداء ، وسوف ينفد التطبيق في النهاية من الذاكرة. هناك عدد من الطرق لتجنب ذلك ، ولكن في حالتنا يمكننا فقط إزالة الإشارة إلى GameScene
من HUD ، وسيتم إنهاء المشهد و HUD بمجرد أن نعود إلى MenuScene
. لدى Krakendev شرح أعمق لأنواع المراجع وكيفية تجنب هذه الدورات.
الآن ، انتقل إلى GameViewController.swift
، وقم بإزالة الأسطر الثلاثة التالية من التعليمات البرمجية أو التعليق عليها:
view.showsPhysics = true view.showsFPS = true view.showsNodeCount = true
مع اختفاء بيانات التصحيح ، تبدو اللعبة جيدة حقًا! تهانينا: نحن حاليًا في المرحلة التجريبية! تحقق من الكود النهائي من اليوم على جيثب.
افكار اخيرة
هذا هو الدرس الأخير من برنامج تعليمي من ثلاثة أجزاء ، وإذا وصلت إلى هذا الحد ، فقد قمت بالكثير من العمل على لعبتك. في هذا البرنامج التعليمي ، انتقلت من مشهد لا يحتوي على أي شيء على الإطلاق ، إلى لعبة مكتملة. تهاني! في الدرس الأول ، أضفنا الأرضية وقطرات المطر والخلفية والعفاريت المظلة. لقد لعبنا أيضًا مع الفيزياء وتأكدنا من أن قطرات المطر لدينا لا تتراكم. بدأنا باكتشاف الاصطدام وعملنا على التخلص من العقد حتى لا تنفد الذاكرة لدينا. أضفنا أيضًا بعض تفاعل المستخدم من خلال السماح للمظلة بالتحرك نحو المكان الذي يلمس فيه المستخدم الشاشة.
في الدرس الثاني ، أضفنا القطة والطعام ، جنبًا إلى جنب مع طرق التفريخ المخصصة لكل منهما. قمنا بتحديث اكتشاف الاصطدام لدينا للسماح للقطط والعفاريت الغذائية. لقد عملنا أيضًا على حركة القط. اكتسبت القطة هدفًا: تناول كل جزء من الطعام المتاح. أضفنا رسومًا متحركة بسيطة للقط وأضفنا تفاعلات مخصصة بين القط والمطر. أخيرًا ، أضفنا المؤثرات الصوتية والموسيقى لجعلها تبدو وكأنها لعبة كاملة.
في هذا الدرس الأخير ، أنشأنا شاشة عرض علوية لتثبيت ملصق درجاتنا ، بالإضافة إلى زر الإنهاء الخاص بنا. لقد تعاملنا مع الإجراءات عبر العقد وتمكين المستخدم من الإنهاء باستخدام رد اتصال من عقدة HUD. أضفنا أيضًا مشهدًا آخر يمكن للمستخدم بدء تشغيله ويمكنه العودة إليه بعد النقر على زر إنهاء. تعاملنا مع عملية بدء اللعبة والتحكم في الصوت في اللعبة.
أين أذهب من هنا
لقد خصصنا الكثير من الوقت للوصول إلى هذا الحد ، ولكن لا يزال هناك الكثير من العمل الذي يمكن أن ندخله في هذه اللعبة. لا يزال RainCat يواصل التطوير ، وهو متوفر في متجر التطبيقات. فيما يلي قائمة بالرغبات التي يجب إضافتها. تمت إضافة بعض العناصر ، بينما لا يزال البعض الآخر معلقًا:
- أضف الرموز وشاشة البداية.
- قم بإنهاء القائمة الرئيسية (المبسطة للدرس التعليمي).
- أصلح الحشرات ، بما في ذلك قطرات المطر المارقة وتكاثر الطعام المتعدد.
- إعادة بناء وتحسين الكود.
- قم بتغيير لوحة ألوان اللعبة بناءً على النتيجة.
- قم بتحديث الصعوبة بناءً على النتيجة.
- حرك القطة عندما يكون الطعام فوقها مباشرة.
- دمج Game Center.
- منح الائتمان (بما في ذلك الائتمان المناسب للمقطوعات الموسيقية).
تتبع على GitHub لأنه سيتم إجراء هذه التغييرات في المستقبل. إذا كانت لديك أي أسئلة حول الكود ، فلا تتردد في مراسلتنا على [email protected] ويمكننا مناقشتها. إذا حظيت موضوعات معينة بالاهتمام الكافي ، فربما يمكننا كتابة مقال آخر يناقش الموضوع.
شكرا!
أود أن أشكر جميع الأشخاص الذين ساعدوا في عملية إنشاء اللعبة وتطوير المقالات التي تتوافق معها.
- كاثرين رو للفن الأولي والتصميم والتحرير ونشر المقالات في المرآب الخاص بنا.
- Morgan Wheaton للحصول على تصميم القائمة النهائية ولوحات الألوان (والتي ستبدو رائعة بمجرد أن أنفذ هذه الميزات بالفعل - ترقبوا ذلك).
- نيكي كلارك للحصول على الرؤوس والفواصل الرائعة في المقالات وللمساعدة في تحرير المقالات.
- Laura Levisay لجميع صور GIF الرائعة في المقالات ولإرسال صور GIF لطيفة إليّ للحصول على الدعم المعنوي.
- Tom Hudson للمساعدة في تحرير المقالات ومن لولاها لما تم إنشاء هذه السلسلة على الإطلاق.
- Lani DeGuire للمساعدة في تحرير المقالات ، والتي كانت عبارة عن الكثير من العمل.
- Jeff Moon للمساعدة في تحرير الدرس الثالث وتنس الطاولة. الكثير من تنس الطاولة.
- Tom Nelson للمساعدة في التأكد من أن البرنامج التعليمي يعمل كما ينبغي.
على محمل الجد ، استغرق الأمر الكثير من الأشخاص لإعداد كل شيء لهذه المقالة وإصدارها في المتجر.
شكرا لكل من قرأ هذه الجملة أيضا.