Jak zbudować grę SpriteKit w Swift 3 (część 3)

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ Czy zastanawiałeś się kiedyś, jak stworzyć grę SpriteKit? Czy przyciski wydają się większym zadaniem, niż powinny? Czy zastanawiałeś się kiedyś, jak zachować ustawienia w grze? Tworzenie gier nigdy nie było łatwiejsze na iOS od czasu wprowadzenia SpriteKit. W trzeciej części tej trzyczęściowej serii zakończymy naszą grę RainCat i dokończymy wprowadzenie do SpriteKit. Jeśli przegapiłeś poprzednią lekcję, możesz nadrobić zaległości, pobierając kod na GitHub. Pamiętaj, że ten samouczek wymaga Xcode 8 i Swift 3.

Czy zastanawiałeś się kiedyś, czego potrzeba, aby stworzyć grę SpriteKit? Czy przyciski wydają się większym zadaniem, niż powinny? Czy zastanawiałeś się kiedyś, jak zachować ustawienia w grze? Tworzenie gier nigdy nie było łatwiejsze na iOS od czasu wprowadzenia SpriteKit. W trzeciej części tej trzyczęściowej serii zakończymy naszą grę RainCat i dokończymy wprowadzenie do SpriteKit.

Jeśli przegapiłeś poprzednią lekcję, możesz nadrobić zaległości, pobierając kod na GitHub. Pamiętaj, że ten samouczek wymaga Xcode 8 i Swift 3.

Dalsza lektura na SmashingMag: Link

  • Grywalizacja i UX: gdzie użytkownicy wygrywają lub przegrywają
  • Zabawny projekt UX: budowanie lepszej gry
  • Łączenie projektowania UX i psychologii w celu zmiany zachowań użytkowników
Raincat, lekcja 3
RainCat, lekcja 3
Więcej po skoku! Kontynuuj czytanie poniżej ↓

To jest lekcja trzecia naszej przygody z RainCat. Na poprzedniej lekcji spędziliśmy długi dzień na przemyślaniu prostych animacji, zachowań kotów, szybkich efektów dźwiękowych i muzyki w tle.

Dzisiaj skupimy się na:

  • wyświetlacz heads-up (HUD) do punktacji;
  • menu główne — z przyciskami;
  • opcje wyciszania dźwięków;
  • opcja zakończenia gry.

Jeszcze więcej aktywów

Zasoby do ostatniej lekcji są dostępne w serwisie GitHub. Ponownie przeciągnij obrazy do Assets.xcassets , tak jak zrobiliśmy to w poprzednich lekcjach.

Heads-up!

Potrzebujemy sposobu na utrzymanie wyniku. W tym celu możemy stworzyć heads-up display (HUD). To będzie całkiem proste; będzie to SKNode zawierający wynik i przycisk wyjścia z gry. Na razie skupimy się tylko na partyturze. Czcionka, której będziemy używać, to Pixel Digivolve, którą można pobrać na Dafont.com. Podobnie jak w przypadku korzystania z obrazów lub dźwięków, które nie należą do Ciebie, przed użyciem zapoznaj się z licencją czcionki. Ten stwierdza, że ​​jest bezpłatny do użytku osobistego, ale jeśli naprawdę podoba ci się czcionka, możesz przekazać darowiznę autorowi ze strony. Nie zawsze możesz zrobić wszystko sam, więc oddawanie się tym, którzy pomogli ci po drodze, jest miłe.

Następnie musimy dodać do projektu niestandardową czcionkę. Ten proces może być trudny za pierwszym razem.

Pobierz i przenieś czcionkę do folderu projektu, w folderze „Czcionki”. Robiliśmy to kilka razy w poprzednich lekcjach, więc przejdziemy przez ten proces trochę szybciej. Dodaj do projektu grupę o nazwie Fonts i dodaj plik Pixel digivolve.otf .

Teraz nadchodzi trudna część. Jeśli przegapisz tę część, prawdopodobnie nie będziesz mógł użyć czcionki. Musimy go dodać do naszego pliku Info.plist . Ten plik znajduje się w lewym okienku Xcode. Kliknij go, a zobaczysz listę nieruchomości (lub plist ). Kliknij listę prawym przyciskiem myszy i kliknij „Dodaj wiersz”.

Dodaj wiersz
Dodaj wiersz do plist .

Gdy pojawi się nowy wiersz, wprowadź następujące informacje:

 Fonts provided by application

Następnie pod Item 0 musimy dodać nazwę naszej czcionki. plist powinien wyglądać następująco:

Pixel digivolve.otf
Czcionka dodana do plist pomyślnie!

Czcionka powinna być gotowa do użycia! Powinniśmy zrobić szybki test, aby upewnić się, że działa zgodnie z przeznaczeniem. Przejdź do GameScene.swift i w sceneDidLoad dodaj następujący kod na górze funkcji:

 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)

Czy to działa?

Witaj świecie!
Testowanie naszego SKLabelNode . O nie! Etykieta „Hello World” powraca.

Jeśli to działa, to wszystko zrobiłeś poprawnie. Jeśli nie, to coś jest nie tak. Code With Chris ma bardziej szczegółowy przewodnik rozwiązywania problemów, ale pamiętaj, że dotyczy on starszej wersji Swift, więc będziesz musiał wprowadzić drobne poprawki, aby wprowadzić go do Swift 3.

Teraz, gdy możemy załadować niestandardowe czcionki, możemy zacząć od naszego HUDa. Usuń etykietę „Hello World”, ponieważ użyliśmy jej tylko po to, aby upewnić się, że nasza czcionka się załaduje. HUD będzie SKNode , działającym jak pojemnik na nasze elementy HUD. Jest to ten sam proces, który zastosowaliśmy podczas tworzenia węzła w tle w lekcji pierwszej.

Utwórz plik HudNode.swift przy użyciu zwykłych metod i wprowadź następujący kod:

 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)" } }

Zanim zrobimy cokolwiek innego, otwórz Constants.swift i dodaj następujący wiersz na dole pliku — użyjemy go do pobrania i utrwalenia najlepszego wyniku:

 let ScoreKey = "RAINCAT_HIGHSCORE"

W kodzie mamy pięć zmiennych, które odnoszą się do tablicy wyników. Pierwsza zmienna to rzeczywisty SKLabelNode , którego używamy do prezentacji etykiety. Następna jest nasza zmienna do przechowywania aktualnego wyniku; następnie zmienna, która ma najlepszy wynik. Ostatnia zmienna to wartość logiczna, która mówi nam, czy aktualnie prezentujemy wysoki wynik (używamy tego do ustalenia, czy musimy uruchomić SKAction , aby zwiększyć skalę tablicy wyników i pokolorować ją na żółtą podłogę).

Pierwsza funkcja, setup(size:) , służy tylko do ustawienia wszystkiego. Skonfigurowaliśmy SKLabelNode w taki sam sposób, jak wcześniej. Klasa SKNode nie ma żadnych właściwości rozmiaru, dlatego musimy utworzyć sposób ustawiania rozmiaru w celu pozycjonowania naszej etykiety scoreNode . Pobieramy również aktualny najlepszy wynik z UserDefaults . Jest to szybki i łatwy sposób na zapisywanie małych porcji danych, ale nie jest to bezpieczne. Ponieważ nie martwimy się o bezpieczeństwo w tym przykładzie, UserDefaults jest w porządku.

W naszym addPoint() zwiększamy bieżącą zmienną score i sprawdzamy, czy użytkownik uzyskał wysoki wynik. Jeśli mają wysoki wynik, zapisujemy ten wynik w UserDefaults i sprawdzamy, czy obecnie pokazujemy najlepszy wynik. Jeśli użytkownik uzyskał wysoki wynik, możemy animować rozmiar i kolor scoreNode .

W funkcji resetPoints() ustawiamy aktualny wynik na 0 . Następnie musimy sprawdzić, czy pokazujemy najwyższy wynik, i w razie potrzeby zresetować rozmiar i kolor do wartości domyślnych.

Wreszcie mamy małą funkcję o nazwie updateScoreboard . Jest to wewnętrzna funkcja ustawiająca partyturę na tekst scoreNode . Jest to wywoływane zarówno w addPoint() , jak i resetPoints() .

Podłączanie HUD

Musimy sprawdzić, czy nasz HUD działa poprawnie. Przejdź do GameScene.swift i dodaj następujący wiersz poniżej zmiennej foodNode na górze pliku:

 private let hudNode = HudNode()

Dodaj następujące dwa wiersze w funkcji sceneDidLoad() , u góry:

 hudNode.setup(size: size) addChild(hudNode)

Następnie w funkcji spawnCat() zresetuj punkty na wypadek, gdyby kot spadł z ekranu. Dodaj następujący wiersz po dodaniu duszka kota do sceny:

 hudNode.resetPoints()

Następnie w funkcji handleCatCollision(contact:) musimy ponownie zresetować wynik, gdy w kota spadnie deszcz. W instrukcji switch na końcu funkcji — gdy inne ciało jest RainDropCategory — dodaj następujący wiersz:

 hudNode.resetPoints()

Na koniec musimy poinformować tablicę wyników, kiedy użytkownik zdobył punkty. Na końcu pliku w handleFoodHit(contact:) znajdź następujące wiersze aż do tego miejsca:

 //TODO increment points print("fed cat")

I zastąp je tym:

 hudNode.addPoint()

Voila!

HUD odblokowany!
HUD odblokowany!

Powinieneś zobaczyć HUD w akcji. Biegaj i zbieraj trochę jedzenia. Gdy po raz pierwszy zbierzesz jedzenie, powinieneś zobaczyć, że wynik zmienia kolor na żółty i rośnie w skali. Kiedy zobaczysz, że to się dzieje, niech kot zostanie trafiony. Jeśli wynik się zresetuje, będziesz wiedział, że jesteś na dobrej drodze!

Wysoki wynik!
Najwyższy wynik w historii (… w momencie pisania tego tekstu)!

Następna scena

Zgadza się, przechodzimy do innej sceny! W rzeczywistości po zakończeniu będzie to pierwszy ekran naszej aplikacji. Zanim zrobisz cokolwiek innego, otwórz Constants.swift i dodaj następujący wiersz na dole pliku — użyjemy go do pobrania i utrwalenia najlepszego wyniku:

 let ScoreKey = "RAINCAT_HIGHSCORE"

Utwórz nową scenę, umieść ją w folderze „Sceny” i nazwij ją MenuScene.swift . Wprowadź następujący kod w pliku 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) } }

Ponieważ ta scena jest stosunkowo prosta, nie będziemy tworzyć żadnych klas specjalnych. Nasza scena będzie składać się z dwóch przycisków. Mogą to być (i prawdopodobnie zasługują na to) ich własna klasa SKSpriteNodes , ale ponieważ są wystarczająco różne, nie będziemy musieli tworzyć dla nich nowych klas. To ważna wskazówka, gdy tworzysz własną grę: musisz być w stanie określić, gdzie zatrzymać i zrefaktoryzować kod, gdy sytuacja się komplikuje. Po dodaniu do gry więcej niż trzech lub czterech przycisków może nadszedł czas, aby zatrzymać i zmienić kod przycisku menu na jego własną klasę.

Powyższy kod nie robi nic specjalnego; ustawia pozycje czterech duszków. Ustawiamy również kolor tła sceny, aby całe tło miało poprawną wartość. Fajnym narzędziem do generowania kodów kolorów z ciągów HEX dla Xcode jest UI Color. Powyższy kod ustawia również tekstury dla naszych stanów przycisków. Przycisk uruchamiający grę ma stan normalny i wciśnięty, natomiast przycisk dźwiękowy jest przełącznikiem. Aby uprościć przełączanie, będziemy zmieniać wartość alfa przycisku dźwięku po naciśnięciu przez użytkownika. Wyciągamy również i ustawiamy wysoko punktowany SKLabelNode .

Nasza MenuScene wygląda całkiem nieźle. Teraz musimy pokazać scenę, w której aplikacja się ładuje. Przejdź do GameViewController.swift i znajdź następujący wiersz:

 let sceneNode = GameScene(size: view.frame.size)

Zastąp to tym:

 let sceneNode = MenuScene(size: view.frame.size)

Ta niewielka zmiana domyślnie załaduje MenuScene zamiast GameScene .

Nasza nowa scena!
Nasza nowa scena! Zwróć uwagę na 1,0 klatki na sekundę: nic się nie porusza, więc nie musisz niczego aktualizować.

Stany przycisków

Przyciski w SpriteKit mogą być trudne. Dostępnych jest wiele opcji innych firm (nawet sam je stworzyłem), ale teoretycznie wystarczy znać trzy metody dotykowe:

  • touchesBegan(_ touches: with event:)
  • touchesMoved(_ touches: with event:)
  • touchesEnded(_ touches: with event:)

Omówiliśmy to krótko podczas aktualizacji parasola, ale teraz musimy wiedzieć, który przycisk został dotknięty, czy użytkownik zwolnił stuknięcie lub kliknął ten przycisk i czy użytkownik nadal go dotyka. W tym miejscu do gry wkracza nasza zmienna selectedButton . Gdy rozpocznie się dotyk, możemy przechwycić przycisk, który użytkownik zaczął klikać za pomocą tej zmiennej. Jeśli przeciągną poza przycisk, poradzimy sobie z tym i nadamy mu odpowiednią teksturę. Kiedy zwolnią dotyk, możemy wtedy zobaczyć, czy nadal dotykają przycisku. Jeśli tak, możemy zastosować do niego powiązaną akcję. Dodaj następujące wiersze na dole 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") }

To jest prosta obsługa naszych dwóch przycisków. W touchesBegan(_ touches: with events:) zaczynamy od sprawdzenia, czy mamy aktualnie zaznaczone przyciski. Jeśli to zrobimy, musimy zresetować stan przycisku na niewciśnięty. Następnie musimy sprawdzić, czy został wciśnięty jakikolwiek przycisk. Jeśli zostanie naciśnięty, pokaże podświetlony stan przycisku. Następnie ustawiamy selectedButton na przycisk do użycia w pozostałych dwóch metodach.

W touchesMoved(_ touches: with events:) sprawdzamy, który przycisk został pierwotnie dotknięty. Następnie sprawdzamy, czy bieżący dotyk nadal znajduje się w granicach selectedButton i z tego miejsca aktualizujemy podświetlony stan. Podświetlony stan startButton zmienia teksturę na teksturę stanu wciśniętego, gdzie podświetlony stan soundButton ma wartość alfa ikonki ustawioną na 50%.

Na koniec w touchesEnded(_ touches: with event:) ponownie sprawdzamy, który przycisk jest zaznaczony, jeśli w ogóle, a następnie, czy dotyk nadal znajduje się w granicach przycisku. Jeśli wszystkie przypadki są spełnione, wywołujemy handleStartButtonClick() lub handleSoundButtonClick() dla właściwego przycisku.

Czas na działanie

Teraz, gdy podstawowe zachowanie przycisków jest wyłączone, potrzebujemy zdarzenia, które zostanie wywołane po ich kliknięciu. Najłatwiejszym do zaimplementowania przyciskiem jest startButton . Po kliknięciu wystarczy nam tylko zaprezentować GameScene . Zaktualizuj handleStartButtonClick() w funkcji MenuScene.swift do następującego kodu:

 func handleStartButtonClick() { let transition = SKTransition.reveal(with: .down, duration: 0.75) let gameScene = GameScene(size: size) gameScene.scaleMode = scaleMode view?.presentScene(gameScene, transition: transition) }

Jeśli teraz uruchomisz aplikację i naciśniesz przycisk, gra się rozpocznie!

Teraz musimy zaimplementować przełącznik wyciszenia. Mamy już menedżera dźwięku, ale musimy być w stanie określić, czy wyciszanie jest włączone, czy wyłączone. W Constants.swift musimy dodać klucz, który będzie utrzymywał się, gdy włączone jest wyciszanie. Dodaj następujący wiersz:

 let MuteKey = "RAINCAT_MUTED"

Wykorzystamy to do zapisania wartości logicznej w UserDefaults . Teraz, gdy to jest skonfigurowane, możemy przejść do SoundManager.swift . Tutaj sprawdzimy i ustawimy UserDefaults , aby zobaczyć, czy wyciszanie jest włączone, czy wyłączone. U góry pliku, pod zmienną trackPosition , dodaj następujący wiersz:

 private(set) var isMuted = false

Jest to zmienna, którą sprawdza menu główne (i wszystko inne, co będzie odtwarzać dźwięk), aby określić, czy dźwięk jest dozwolony. Inicjujemy go jako false , ale teraz musimy sprawdzić UserDefaults , aby zobaczyć, czego chce użytkownik. Zastąp funkcję init() następującą:

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

Teraz, gdy mamy domyślną wartość isMuted , potrzebujemy możliwości jej zmiany. Dodaj następujący kod na dole 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 }

Ta metoda przełączy naszą zmienną wyciszoną, a także zaktualizuje UserDefaults . Jeśli nowa wartość nie jest wyciszona, rozpocznie się odtwarzanie muzyki; jeśli nowa wartość zostanie wyciszona, odtwarzanie nie rozpocznie się. W przeciwnym razie zatrzymamy odtwarzanie bieżącego utworu. Następnie musimy edytować instrukcję if w startPlaying() .

Znajdź następujący wiersz:

 if audioPlayer == nil || audioPlayer?.isPlaying == false {

I zastąp to tym:

 if !isMuted && (audioPlayer == nil || audioPlayer?.isPlaying == false) {

Teraz, jeśli wyciszenie jest wyłączone, a odtwarzacz audio nie jest ustawiony lub bieżący odtwarzacz audio nie jest już odtwarzany, odtworzymy następny utwór.

Stąd możemy wrócić do MenuScene.swift , aby zakończyć nasz przycisk wyciszenia. Zastąp handleSoundbuttonClick() następującym kodem:

 func handleSoundButtonClick() { if SoundManager.sharedInstance.toggleMute() { //Is muted soundButton.texture = soundButtonTextureOff } else { //Is not muted soundButton.texture = soundButtonTexture } }

To przełącza dźwięk w SoundManager , sprawdza wynik, a następnie odpowiednio ustawia teksturę, aby pokazać użytkownikowi, czy dźwięk jest wyciszony, czy nie. Prawie skończyliśmy! Musimy tylko ustawić początkową teksturę przycisku podczas uruchamiania. W sceneDidLoad() znajdź następujący wiersz:

 soundButton = SKSpriteNode(texture: soundButtonTexture)

I zastąp to tym:

 soundButton = SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ? soundButtonTextureOff : soundButtonTexture)

Powyższy przykład używa operatora potrójnego do ustawienia prawidłowej tekstury.

Teraz, gdy muzyka jest już podłączona, możemy przejść do CatSprite.swift , aby wyłączyć miauczenie kota, gdy włączone jest wyciszenie. W funkcji hitByRain() możemy dodać następującą instrukcję if po usunięciu akcji chodzenia:

 if SoundManager.sharedInstance.isMuted { return }

To oświadczenie zwróci informację, czy użytkownik wyciszył aplikację. Z tego powodu całkowicie zignorujemy nasze currentRainHits , maxRainHits i miauczące efekty dźwiękowe.

Po tym wszystkim nadszedł czas, aby wypróbować nasz przycisk wyciszania. Uruchom aplikację i sprawdź, czy odpowiednio odtwarza i wycisza dźwięki. Wycisz dźwięk, zamknij aplikację i otwórz ją ponownie. Upewnij się, że ustawienie wyciszenia trwa. Pamiętaj, że jeśli po prostu wyciszysz i ponownie uruchomisz aplikację z Xcode, być może nie masz wystarczająco dużo czasu na zapisanie UserDefaults . Zagraj w grę i upewnij się, że kot nigdy nie miauczy, gdy jesteś wyciszony.

Testowanie funkcjonalności przycisków.

Wyjście z gry

Teraz, gdy mamy już pierwszy typ przycisku w menu głównym, możemy wejść w trudne sprawy, dodając przycisk wyjścia do naszej sceny gry. Niektóre interesujące interakcje mogą wymyślić nasz styl gry; obecnie parasol przesunie się tam, gdzie użytkownik dotknie lub przesunie dotyk. Oczywiście parasol przesuwający się do przycisku wyjścia, gdy użytkownik próbuje wyjść z gry, jest dość słabym doświadczeniem użytkownika, więc postaramy się temu zapobiec.

Wdrażany przez nas przycisk zakończenia będzie naśladował przycisk rozpoczęcia gry, który dodaliśmy wcześniej, przy czym większość procesu pozostanie bez zmian. Zmiana będzie dotyczyła tego, jak radzimy sobie z dotknięciami. Pobierz quit_button i quit_button_pressed do pliku Assets.xcassets i dodaj następujący kod do pliku HudNode.swift :

 private var quitButton : SKSpriteNode! private let quitButtonTexture = SKTexture(imageNamed: "quit_button") private let quitButtonPressedTexture = SKTexture(imageNamed: "quit_button_pressed")

To obsłuży nasze referencje quitButton , wraz z teksturami, które ustawimy dla stanów przycisków. Aby mieć pewność, że nie zaktualizujemy przypadkowo parasolki podczas próby wyjścia, potrzebujemy zmiennej, która poinformuje HUD (i scenę gry), że wchodzimy w interakcję z przyciskiem wyjścia, a nie z parasolką. Dodaj następujący kod poniżej zmiennej logicznej showingHighScore :

 private(set) var quitButtonPressed = false

Ponownie, jest to zmienna, którą może ustawić tylko HudNode , ale którą inne klasy mogą sprawdzić. Teraz, gdy nasze zmienne są skonfigurowane, możemy dodać przycisk do HUDa. Dodaj następujący kod do funkcji 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)

Powyższy kod ustawi przycisk zakończenia z teksturą naszego stanu niewciśniętego. Ustawiamy również pozycję w prawym górnym rogu i ustawiamy pozycję zPosition na wysoką liczbę, aby zmusić ją do rysowania zawsze na górze. Jeśli uruchomisz grę teraz, pojawi się ona w GameScene , ale nie będzie można na nią jeszcze kliknąć.

Przycisk Zakończ
Zwróć uwagę na nowy przycisk wyjścia w naszym HUD.

Teraz, gdy przycisk został ustawiony, musimy mieć możliwość interakcji z nim. W tej chwili jedynym miejscem, w którym mamy interakcję w GameScene , jest interakcja z umbrellaSprite . W naszym przykładzie HUD będzie miał priorytet nad parasolką, dzięki czemu użytkownicy nie będą musieli odsuwać parasola, aby wyjść. Możemy stworzyć te same funkcje w HudNode.swift , aby naśladować funkcję dotykową w GameScene.swift . Dodaj następujący kod do 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 }

Powyższy kod jest bardzo podobny do kodu, który stworzyliśmy dla MenuScene . Różnica polega na tym, że istnieje tylko jeden przycisk do śledzenia, więc możemy obsłużyć wszystko w ramach tych metod dotykowych. Ponadto, ponieważ poznamy lokalizację dotyku w GameScene , możemy po prostu sprawdzić, czy nasz przycisk zawiera punkt dotyku.

Przejdź do GameScene.swift i zastąp metody touchesBegan(_ touches with event:) i touchesMoved(_ touches: with event:) następującym kodem:

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

Tutaj każda metoda obsługuje wszystko w podobny sposób. Informujemy HUD, że użytkownik wszedł w interakcję ze sceną. Następnie sprawdzamy, czy przycisk zakończenia aktualnie rejestruje dotknięcia. Jeśli nie, to przesuwamy parasol. Dodaliśmy również funkcję touchesEnded(_ touches: with event:) , która obsługuje koniec kliknięcia przycisku wyjścia, ale nadal nie używamy jej w przypadku umbrellaSprite .

Kliknięcie przycisku wyjścia nie spowoduje przesunięcia parasola, ale kliknięcie w innym miejscu już.

Teraz, gdy mamy przycisk, potrzebujemy sposobu, aby wpłynął na GameScene . Dodaj następujący wiersz na górze HudeNode.swift :

 var quitButtonAction : (() -> ())?

Jest to ogólne zamknięcie, które nie ma wejścia ani wyjścia. Ustawimy to za pomocą kodu w pliku GameScene.swift i wywołamy go, gdy klikniemy przycisk w HudNode.swift . Następnie możemy zastąpić TODO w kodzie, który stworzyliśmy wcześniej w funkcji touchEndedAtPoint(point:) tym:

 if quitButton.contains(point) && quitButtonAction != nil { quitButtonAction!() }

Teraz, jeśli ustawimy zamknięcie quitButtonAction , zostanie ono wywołane od tego momentu.

Aby skonfigurować zamknięcie quitButtonAction , musimy przejść do GameScene.swift . W sceneDidLoad() możemy zastąpić naszą konfigurację HUD następującym kodem:

 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)

Uruchom aplikację, naciśnij play, a następnie naciśnij quit. Jeśli jesteś z powrotem w menu głównym, przycisk zakończenia działa zgodnie z przeznaczeniem. W utworzonym przez nas zamknięciu zainicjowaliśmy przejście do MenuScene . I ustawiamy to zamknięcie w węźle HUD , aby było uruchamiane po kliknięciu przycisku wyjścia. Kolejna ważna linia jest tutaj, kiedy ustawiamy quitButtonAction na nil . Powodem tego jest to, że występuje cykl zatrzymywania. Scena zawiera odniesienie do HUD, gdzie HUD zawiera odniesienie do sceny. Ponieważ istnieje odwołanie do obu obiektów, żaden z nich nie zostanie usunięty, gdy nadejdzie czas na wyrzucanie elementów bezużytecznych. W takim przypadku za każdym razem, gdy wchodzimy i wychodzimy z GameScene , zostanie utworzona kolejna jego instancja i nigdy nie zostanie wydana. To źle wpływa na wydajność, a aplikacji w końcu zabraknie pamięci. Jest wiele sposobów, aby tego uniknąć, ale w naszym przypadku możemy po prostu usunąć odniesienie do GameScene z HUD, a scena i HUD zostaną zakończone, gdy wrócimy do MenuScene . Krakendev ma głębsze wyjaśnienie typów referencyjnych i sposobów unikania tych cykli.

Teraz przejdź do GameViewController.swift i usuń lub skomentuj następujące trzy wiersze kodu:

 view.showsPhysics = true view.showsFPS = true view.showsNodeCount = true

Po usunięciu danych debugowania gra wygląda naprawdę dobrze! Gratulacje: Jesteśmy obecnie w fazie beta! Sprawdź ostateczny kod od dzisiaj na GitHub.

Końcowe przemyślenia

To jest ostatnia lekcja trzyczęściowego samouczka, a jeśli dotarłeś tak daleko, wykonałeś po prostu dużo pracy nad swoją grą. W tym samouczku przeszedłeś ze sceny, w której nie było absolutnie nic, do ukończonej gry. Gratulacje! W pierwszej lekcji dodaliśmy podłogę, krople deszczu, tło i parasole. Bawiliśmy się również fizyką i upewniliśmy się, że nasze krople deszczu nie gromadzą się. Zaczęliśmy od wykrywania kolizji i pracowaliśmy nad usuwaniem węzłów, aby nie zabrakło nam pamięci. Dodaliśmy również trochę interakcji z użytkownikiem, umożliwiając parasolowi poruszanie się w kierunku miejsca, w którym użytkownik dotyka ekranu.

W lekcji drugiej dodaliśmy kota i jedzenie wraz z niestandardowymi metodami tarła dla każdego z nich. Zaktualizowaliśmy nasze wykrywanie kolizji, aby uwzględnić kota i duszki z jedzeniem. Pracowaliśmy również nad ruchem kota. Kot zyskał cel: zjedz każdą możliwą ilość jedzenia. Dodaliśmy prostą animację dla kota i dodaliśmy niestandardowe interakcje między kotem a deszczem. Na koniec dodaliśmy efekty dźwiękowe i muzykę, aby gra wyglądała jak kompletna.

W ostatniej lekcji stworzyliśmy wyświetlacz heads-up, w którym znajduje się nasza etykieta wyniku oraz przycisk zakończenia. Zajmowaliśmy się działaniami w różnych węzłach i umożliwiliśmy użytkownikowi wyjście za pomocą wywołania zwrotnego z węzła HUD. Dodaliśmy również kolejną scenę, do której użytkownik może się uruchomić i do której może wrócić po kliknięciu przycisku Zakończ. Zajęliśmy się procesem uruchamiania gry i kontrolowania dźwięku w grze.

Gdzie iść stąd?

Poświęciliśmy dużo czasu, aby zajść tak daleko, ale wciąż jest dużo pracy, którą można włożyć w tę grę. RainCat wciąż się rozwija i jest dostępny w App Store. Poniżej znajduje się lista potrzeb i potrzeb, które należy dodać. Niektóre elementy zostały dodane, podczas gdy inne wciąż oczekują:

  • Dodaj ikony i ekran powitalny.
  • Sfinalizuj menu główne (uproszczone dla samouczka).
  • Napraw błędy, w tym nieuczciwe krople deszczu i wielokrotne odradzanie się jedzenia.
  • Refaktoryzuj i optymalizuj kod.
  • Zmień paletę kolorów gry na podstawie wyniku.
  • Zaktualizuj poziom trudności na podstawie wyniku.
  • Animuj kota, gdy jedzenie jest tuż nad nim.
  • Zintegruj Game Center.
  • Podaj kredyt (w tym właściwy kredyt utworów muzycznych).

Śledź na GitHub, ponieważ te zmiany zostaną wprowadzone w przyszłości. Jeśli masz jakiekolwiek pytania dotyczące kodu, napisz do nas na adres [email protected], a my możemy o tym porozmawiać. Jeśli pewne tematy przyciągną wystarczającą uwagę, może napiszemy kolejny artykuł omawiający ten temat.

Dziękuję!

Chciałbym podziękować wszystkim osobom, które pomogły w procesie tworzenia gry i opracowywania artykułów z nią związanych.

  • Cathryn Rowe Za wstępną grafikę, projektowanie i edycję oraz za publikowanie artykułów w naszym garażu.
  • Morgan Wheaton Ostateczny projekt menu i palety kolorów (które będą wyglądały niesamowicie, gdy zaimplementuję te funkcje — bądź na bieżąco).
  • Nikki Clark Za wspaniałe nagłówki i przekładki w artykułach oraz za pomoc w redagowaniu artykułów.
  • Laura Levisay Za wszystkie niesamowite GIF-y w artykułach i za przesłanie mi słodkich GIF-ów z kotami w celu wsparcia moralnego.
  • Tom Hudson Za pomoc w redagowaniu artykułów i bez kogo ta seria w ogóle by nie powstała.
  • Lani DeGuire Za pomoc w redagowaniu artykułów, co było ogromem pracy.
  • Jeff Moon Za pomoc w redagowaniu lekcji trzeciej i ping-ponga. Dużo ping-ponga.
  • Tom Nelson Za pomoc w upewnieniu się, że samouczek działa tak, jak powinien.

Poważnie, przygotowanie wszystkiego do tego artykułu i wypuszczenie go do sklepu zajęło mnóstwo ludzi.

Dziękuję również wszystkim, którzy czytają to zdanie.