Cómo construir un juego de SpriteKit en Swift 3 (Parte 3)
Publicado: 2022-03-10¿Alguna vez te has preguntado qué se necesita para crear un juego de SpriteKit? ¿Los botones parecen una tarea más grande de lo que deberían ser? ¿Alguna vez te has preguntado cómo persistir la configuración en un juego? La creación de juegos nunca ha sido tan fácil en iOS desde la introducción de SpriteKit. En la tercera parte de esta serie de tres partes, terminaremos nuestro juego RainCat y completaremos nuestra introducción a SpriteKit.
Si se perdió la lección anterior, puede ponerse al día obteniendo el código en GitHub. Recuerda que este tutorial requiere Xcode 8 y Swift 3.
Lectura adicional en SmashingMag: Enlace
- Gamificación y UX: donde los usuarios ganan o pierden
- Diseño lúdico de UX: construyendo un mejor juego
- Combinando diseño UX y psicología para cambiar el comportamiento del usuario
Esta es la lección tres en nuestro viaje RainCat. En la lección anterior, tuvimos un largo día con algunas animaciones simples, comportamientos de gatos, efectos de sonido rápidos y música de fondo.
Hoy nos centraremos en lo siguiente:
- pantalla de visualización frontal (HUD) para puntuación;
- menú principal — con botones;
- opciones para silenciar sonidos;
- opción de abandono del juego.
Aún más activos
Los activos de la lección final están disponibles en GitHub. Arrastre las imágenes a Assets.xcassets
nuevamente, tal como lo hicimos en las lecciones anteriores.
¡Aviso!
Necesitamos una forma de llevar la cuenta. Para hacer esto, podemos crear una pantalla de visualización frontal (HUD). Esto será bastante simple; será un SKNode
que contendrá la partitura y un botón para salir del juego. Por ahora, solo nos centraremos en la partitura. La fuente que usaremos es Pixel Digivolve, que puedes conseguir en Dafont.com. Al igual que con el uso de imágenes o sonidos que no son tuyos, lee la licencia de la fuente antes de usarla. Este dice que es gratis para uso personal, pero si realmente te gusta la fuente, puedes donar al autor desde la página. No siempre puedes hacer todo tú mismo, por lo que es bueno retribuir a quienes te han ayudado en el camino.
A continuación, debemos agregar la fuente personalizada al proyecto. Este proceso puede ser complicado la primera vez.
Descargue y mueva la fuente a la carpeta del proyecto, en la carpeta "Fuentes". Hemos hecho esto varias veces en las lecciones anteriores, así que pasaremos por este proceso un poco más rápido. Agregue un grupo llamado Fonts
al proyecto y agregue el archivo Pixel digivolve.otf
.
Ahora viene la parte complicada. Si se pierde esta parte, probablemente no podrá usar la fuente. Necesitamos agregarlo a nuestro archivo Info.plist
. Este archivo está en el panel izquierdo de Xcode. Haga clic en él y verá la lista de propiedades (o plist
). Haga clic derecho en la lista y haga clic en "Agregar fila".
Cuando aparezca la nueva fila, ingrese lo siguiente:
Fonts provided by application
Luego, en el Item 0
, debemos agregar el nombre de nuestra fuente. El plist
debería verse como el siguiente:
¡La fuente debe estar lista para usar! Deberíamos hacer una prueba rápida para asegurarnos de que funciona según lo previsto. Vaya a GameScene.swift
y en sceneDidLoad
agregue el siguiente código en la parte superior de la función:
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)
¿Funciona?
Si funciona, entonces has hecho todo correctamente. Si no, entonces algo anda mal. Code With Chris tiene una guía de solución de problemas más detallada, pero tenga en cuenta que es para una versión anterior de Swift, por lo que tendrá que hacer ajustes menores para actualizarla a Swift 3.
Ahora que podemos cargar fuentes personalizadas, podemos comenzar en nuestro HUD. Elimine la etiqueta "Hello World", porque solo la usamos para asegurarnos de que se cargue nuestra fuente. El HUD será un SKNode
, actuando como un contenedor para nuestros elementos HUD. Este es el mismo proceso que seguimos al crear el nodo de fondo en la lección uno.
Cree el archivo HudNode.swift
utilizando los métodos habituales e ingrese el siguiente código:
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)" } }
Antes de hacer cualquier otra cosa, abra Constants.swift
y agregue la siguiente línea al final del archivo; la usaremos para recuperar y conservar la puntuación más alta:
let ScoreKey = "RAINCAT_HIGHSCORE"
En el código, tenemos cinco variables que pertenecen al marcador. La primera variable es el SKLabelNode
real, que usamos para presentar la etiqueta. La siguiente es nuestra variable para mantener el puntaje actual; luego la variable que tenga la mejor puntuación. La última variable es un booleano que nos dice si actualmente estamos presentando el puntaje más alto (lo usamos para establecer si necesitamos ejecutar una SKAction
para aumentar la escala del marcador y colorearlo con el amarillo del piso).
La primera función, setup(size:)
, está ahí solo para configurar todo. Configuramos el SKLabelNode
de la misma manera que lo hicimos antes. La clase SKNode
no tiene ninguna propiedad de tamaño de forma predeterminada, por lo que debemos crear una forma de establecer un tamaño para posicionar nuestra etiqueta scoreNode
. También estamos obteniendo la puntuación más alta actual de UserDefaults
. Esta es una manera rápida y fácil de guardar pequeños fragmentos de datos, pero no es segura. Debido a que no estamos preocupados por la seguridad para este ejemplo, UserDefaults
está perfectamente bien.
En nuestro addPoint()
, estamos incrementando la variable de score
actual y comprobando si el usuario ha obtenido una puntuación alta. Si tienen un puntaje alto, guardamos ese puntaje en UserDefaults
y verificamos si actualmente estamos mostrando el mejor puntaje. Si el usuario ha logrado una puntuación alta, podemos animar el tamaño y el color de scoreNode
.
En la función resetPoints()
, establecemos la puntuación actual en 0
. Luego, debemos verificar si mostramos la puntuación más alta y restablecer el tamaño y el color a los valores predeterminados si es necesario.
Finalmente, tenemos una pequeña función llamada updateScoreboard
. Esta es una función interna para establecer la puntuación en el texto de scoreNode
. Esto se llama tanto en addPoint()
como resetPoints()
.
Conexión del HUD
Necesitamos probar si nuestro HUD está funcionando correctamente. Vaya a GameScene.swift
y agregue la siguiente línea debajo de la variable foodNode
en la parte superior del archivo:
private let hudNode = HudNode()
Agregue las siguientes dos líneas en la función sceneDidLoad()
, cerca de la parte superior:
hudNode.setup(size: size) addChild(hudNode)
Luego, en la función spawnCat()
, restablece los puntos en caso de que el gato se haya caído de la pantalla. Agregue la siguiente línea después de agregar el sprite del gato a la escena:
hudNode.resetPoints()
A continuación, en la handleCatCollision(contact:)
, necesitamos restablecer la puntuación nuevamente cuando el gato es golpeado por la lluvia. En la declaración de switch
al final de la función, cuando el otro cuerpo es una RainDropCategory
, agregue la siguiente línea:
hudNode.resetPoints()
Finalmente, necesitamos decirle al marcador cuando el usuario ha ganado puntos. Al final del archivo en handleFoodHit(contact:)
, busque las siguientes líneas hasta aquí:
//TODO increment points print("fed cat")
Y reemplázalos con esto:
hudNode.addPoint()
¡Voila!
Debería ver el HUD en acción. Corre y recoge algo de comida. La primera vez que recojas comida, deberías ver que la puntuación se vuelve amarilla y crece en escala. Cuando veas que esto sucede, deja que el gato sea golpeado. Si la puntuación se restablece, ¡sabrás que estás en el camino correcto!
la siguiente escena
¡Así es, nos estamos moviendo a otra escena! De hecho, cuando se complete, esta será la primera pantalla de nuestra aplicación. Antes de hacer cualquier otra cosa, abra Constants.swift
y agregue la siguiente línea al final del archivo; la usaremos para recuperar y conservar la puntuación más alta:
let ScoreKey = "RAINCAT_HIGHSCORE"
Cree la nueva escena, colóquela en la carpeta "Escenas" y MenuScene.swift
. Ingrese el siguiente código en el archivo 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) } }
Debido a que esta escena es relativamente simple, no crearemos ninguna clase especial. Nuestra escena constará de dos botones. Estos podrían ser (y posiblemente merecen ser) su propia clase de SKSpriteNodes
, pero debido a que son lo suficientemente diferentes, no necesitaremos crear nuevas clases para ellos. Este es un consejo importante para cuando crea su propio juego: debe poder determinar dónde detenerse y refactorizar el código cuando las cosas se vuelven complejas. Una vez que haya agregado más de tres o cuatro botones a un juego, podría ser el momento de detenerse y refactorizar el código del botón de menú en su propia clase.
El código anterior no está haciendo nada especial; está estableciendo las posiciones de cuatro sprites. También estamos configurando el color de fondo de la escena, de modo que todo el fondo tenga el valor correcto. Una buena herramienta para generar códigos de color a partir de cadenas HEX para Xcode es UI Color. El código anterior también configura las texturas para los estados de nuestros botones. El botón para iniciar el juego tiene un estado normal y un estado presionado, mientras que el botón de sonido es un interruptor. Para simplificar las cosas para la alternancia, cambiaremos el valor alfa del botón de sonido cuando el usuario presione. También estamos extrayendo y configurando el SKLabelNode
de puntuación más alta.
Nuestro MenuScene
se ve bastante bien. Ahora necesitamos mostrar la escena cuando se carga la aplicación. Vaya a GameViewController.swift
y busque la siguiente línea:
let sceneNode = GameScene(size: view.frame.size)
Reemplázalo con esto:
let sceneNode = MenuScene(size: view.frame.size)
Este pequeño cambio cargará MenuScene
por defecto, en lugar de GameScene
.
Estados del botón
Los botones pueden ser complicados en SpriteKit. Hay muchas opciones de terceros disponibles (incluso hice una yo mismo), pero en teoría solo necesita conocer los tres métodos táctiles:
-
touchesBegan(_ touches: with event:)
-
touchesMoved(_ touches: with event:)
-
touchesEnded(_ touches: with event:)
Cubrimos esto brevemente cuando actualizamos el paraguas, pero ahora necesitamos saber lo siguiente: qué botón se tocó, si el usuario soltó el toque o hizo clic en ese botón, y si el usuario todavía lo está tocando. Aquí es donde entra en juego nuestra variable selectedButton
. Cuando comienza un toque, podemos capturar el botón en el que el usuario comenzó a hacer clic con esa variable. Si arrastran fuera del botón, podemos manejar esto y darle la textura adecuada. Cuando sueltan el toque, podemos ver si todavía están tocando dentro del botón. Si lo son, entonces podemos aplicarle la acción asociada. Agregue las siguientes líneas al final de 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") }
Este es un manejo simple de botones para nuestros dos botones. En touchesBegan(_ touches: with events:)
, comenzamos comprobando si tenemos algún botón actualmente seleccionado. Si lo hacemos, necesitamos restablecer el estado del botón a no presionado. Luego, debemos verificar si se presiona algún botón. Si se presiona uno, mostrará el estado resaltado del botón. Luego, establecemos selectedButton
en el botón para usar en los otros dos métodos.
En touchesMoved(_ touches: with events:)
, verificamos qué botón se tocó originalmente. Luego, verificamos si el toque actual todavía está dentro de los límites del botón selectedButton
y actualizamos el estado resaltado desde allí. El estado resaltado de startButton
cambia la textura a la textura del estado presionado, donde el estado resaltado de soundButton
tiene el valor alfa del sprite establecido en 50%.
Finalmente, en touchesEnded(_ touches: with event:)
, verificamos nuevamente qué botón está seleccionado, si hay alguno, y luego si el toque aún está dentro de los límites del botón. Si se satisfacen todos los casos, llamamos a handleStartButtonClick()
o handleSoundButtonClick()
para el botón correcto.
Un tiempo para la acción
Ahora que tenemos el comportamiento básico de los botones, necesitamos un evento que se active cuando se haga clic en ellos. El botón más fácil de implementar es startButton
. Al hacer clic, solo necesitamos presentar GameScene
. Actualice handleStartButtonClick()
en la función MenuScene.swift
al siguiente código:
func handleStartButtonClick() { let transition = SKTransition.reveal(with: .down, duration: 0.75) let gameScene = GameScene(size: size) gameScene.scaleMode = scaleMode view?.presentScene(gameScene, transition: transition) }
Si ejecuta la aplicación ahora y presiona el botón, ¡el juego comenzará!
Ahora necesitamos implementar el interruptor de silencio. Ya tenemos un administrador de sonido, pero debemos poder decirle si el silenciamiento está activado o desactivado. En Constants.swift
, necesitamos agregar una clave para persistir cuando el silenciamiento está activado. Agregue la siguiente línea:
let MuteKey = "RAINCAT_MUTED"
Usaremos esto para guardar un valor booleano en UserDefaults
. Ahora que está configurado, podemos movernos a SoundManager.swift
. Aquí es donde verificaremos y configuraremos UserDefaults
para ver si el silenciamiento está activado o desactivado. En la parte superior del archivo, debajo de la variable trackPosition
, agregue la siguiente línea:
private(set) var isMuted = false
Esta es la variable que el menú principal (y cualquier otra cosa que reproduzca sonido) verifica para determinar si se permite el sonido. Lo inicializamos como false
, pero ahora debemos verificar UserDefaults
para ver lo que quiere el usuario. Reemplace la función init()
con lo siguiente:
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) }
Ahora que tenemos un valor predeterminado para isMuted
, necesitamos la capacidad de cambiarlo. Agregue el siguiente código en la parte inferior de 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 }
Este método alternará nuestra variable silenciada, así como también actualizará UserDefaults
. Si no se silencia el nuevo valor, comenzará la reproducción de la música; si el nuevo valor está silenciado, la reproducción no comenzará. De lo contrario, detendremos la reproducción de la pista actual. Después de esto, necesitamos editar la instrucción if
en startPlaying()
.
Encuentra la siguiente línea:
if audioPlayer == nil || audioPlayer?.isPlaying == false {
Y reemplázalo con esto:
if !isMuted && (audioPlayer == nil || audioPlayer?.isPlaying == false) {
Ahora, si el silenciamiento está desactivado y el reproductor de audio no está configurado o el reproductor de audio actual ya no se está reproduciendo, reproduciremos la siguiente pista.
Desde aquí, podemos regresar a MenuScene.swift
para finalizar nuestro botón de silencio. Reemplace handleSoundbuttonClick()
con el siguiente código:
func handleSoundButtonClick() { if SoundManager.sharedInstance.toggleMute() { //Is muted soundButton.texture = soundButtonTextureOff } else { //Is not muted soundButton.texture = soundButtonTexture } }
Esto alterna el sonido en SoundManager
, verifica el resultado y luego establece la textura de manera apropiada para mostrarle al usuario si el sonido está silenciado o no. ¡Casi terminamos! Solo necesitamos establecer la textura inicial del botón en el lanzamiento. En sceneDidLoad()
, busque la siguiente línea:
soundButton = SKSpriteNode(texture: soundButtonTexture)
Y reemplázalo con esto:
soundButton = SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ? soundButtonTextureOff : soundButtonTexture)
El ejemplo anterior usa un operador ternario para establecer la textura correcta.
Ahora que la música está conectada, podemos movernos a CatSprite.swift
para desactivar el maullido del gato cuando el silenciamiento está activado. En hitByRain()
, podemos agregar la siguiente instrucción if
después de eliminar la acción de caminar:
if SoundManager.sharedInstance.isMuted { return }
Esta declaración devolverá si el usuario ha silenciado la aplicación. Debido a esto, ignoraremos por completo nuestros efectos de sonido currentRainHits
, maxRainHits
y maullidos.
Después de todo eso, ahora es el momento de probar nuestro botón de silencio. Ejecute la aplicación y verifique si está reproduciendo y silenciando los sonidos correctamente. Silencia el sonido, cierra la aplicación y vuelve a abrirla. Asegúrese de que la configuración de silencio persista. Tenga en cuenta que si simplemente silencia y vuelve a ejecutar la aplicación desde Xcode, es posible que no haya dado suficiente tiempo para que UserDefaults
se guarde. Juega y asegúrate de que el gato nunca maúlle cuando estés silenciado.
Salir del juego
Ahora que tenemos el primer tipo de botón para el menú principal, podemos entrar en un asunto complicado agregando el botón de salida a nuestra escena de juego. Algunas interacciones interesantes pueden surgir con nuestro estilo de juego; actualmente, el paraguas se moverá a donde sea que el usuario toque o mueva su toque. Obviamente, el paraguas que se mueve hacia el botón de salida cuando el usuario intenta salir del juego es una experiencia de usuario bastante pobre, por lo que intentaremos evitar que esto suceda.
El botón de salida que estamos implementando imitará el botón de inicio del juego que agregamos anteriormente, y gran parte del proceso permanecerá igual. El cambio estará en cómo manejamos los toques. Obtenga sus activos quit_button
y quit_button_pressed
en el archivo Assets.xcassets
y agregue el siguiente código al archivo HudNode.swift
:
private var quitButton : SKSpriteNode! private let quitButtonTexture = SKTexture(imageNamed: "quit_button") private let quitButtonPressedTexture = SKTexture(imageNamed: "quit_button_pressed")
Esto manejará nuestra referencia quitButton
, junto con las texturas que estableceremos para los estados del botón. Para asegurarnos de que no actualicemos el paraguas sin darnos cuenta mientras intentamos salir, necesitamos una variable que le diga al HUD (y a la escena del juego) que estamos interactuando con el botón de salir y no con el paraguas. Agregue el siguiente código debajo de la variable booleana showingHighScore
:
private(set) var quitButtonPressed = false
Nuevamente, esta es una variable que solo HudNode
puede configurar pero que otras clases pueden verificar. Ahora que nuestras variables están configuradas, podemos agregar el botón al HUD. Agregue el siguiente código a la función 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)
El código anterior configurará el botón de salida con la textura de nuestro estado no presionado. También estamos configurando la posición en la esquina superior derecha y configurando zPosition
en un número alto para obligarlo a dibujar siempre en la parte superior. Si ejecuta el juego ahora, aparecerá en GameScene
, pero aún no se podrá hacer clic en él.
Ahora que el botón ha sido posicionado, necesitamos poder interactuar con él. En este momento, el único lugar donde tenemos interacción en GameScene
es cuando interactuamos con umbrellaSprite
. En nuestro ejemplo, el HUD tendrá prioridad sobre el paraguas, de modo que los usuarios no tengan que mover el paraguas para salir. Podemos crear las mismas funciones en HudNode.swift
para imitar la funcionalidad táctil en GameScene.swift
. Agregue el siguiente código a 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 }
El código anterior se parece mucho al código que creamos para MenuScene
. La diferencia es que solo hay un botón para realizar un seguimiento, por lo que podemos manejar todo dentro de estos métodos táctiles. Además, debido a que conoceremos la ubicación del toque en GameScene
, podemos verificar si nuestro botón contiene el punto de toque.
Vaya a GameScene.swift
y reemplace los touchesBegan(_ touches with event:)
y touchesMoved(_ touches: with event:)
con el siguiente código:
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) } }
Aquí, cada método maneja todo prácticamente de la misma manera. Le estamos diciendo al HUD que el usuario ha interactuado con la escena. Luego, verificamos si el botón de salir actualmente está capturando los toques. Si no es así, movemos el paraguas. También agregamos la touchesEnded(_ touches: with event:)
para controlar el final del clic del botón de salida, pero aún no la usamos para umbrellaSprite
.
Ahora que tenemos un botón, necesitamos una forma de que afecte a GameScene
. Agregue la siguiente línea en la parte superior de HudeNode.swift
:
var quitButtonAction : (() -> ())?
Este es un cierre genérico que no tiene entrada ni salida. Configuraremos esto con código en el archivo GameScene.swift
y lo llamaremos cuando hagamos clic en el botón en HudNode.swift
. Luego, podemos reemplazar TODO
en el código que creamos anteriormente en la touchEndedAtPoint(point:)
con esto:
if quitButton.contains(point) && quitButtonAction != nil { quitButtonAction!() }
Ahora, si configuramos el cierre quitButtonAction
, se llamará desde este punto.
Para configurar el cierre quitButtonAction
, debemos movernos a GameScene.swift
. En sceneDidLoad()
, podemos reemplazar nuestra configuración de HUD con el siguiente código:
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)
Ejecute la aplicación, presione reproducir y luego presione salir. Si está de vuelta en el menú principal, entonces su botón de salida está funcionando según lo previsto. En el cierre que creamos, inicializamos una transición a MenuScene
. Y configuramos este cierre en el nodo HUD
para que se ejecute cuando se hace clic en el botón Salir. Otra línea importante aquí es cuando establecemos quitButtonAction
en nil
. La razón de esto es que se está produciendo un ciclo de retención. La escena tiene una referencia al HUD donde el HUD tiene una referencia a la escena. Debido a que hay una referencia a ambos objetos, ninguno se eliminará cuando llegue el momento de la recolección de basura. En este caso, cada vez que entremos y salgamos de GameScene
, se creará otra instancia del mismo y nunca se liberará. Esto es malo para el rendimiento y la aplicación eventualmente se quedará sin memoria. Hay varias formas de evitar esto, pero en nuestro caso, podemos simplemente eliminar la referencia a GameScene
del HUD, y la escena y el HUD finalizarán una vez que regresemos a MenuScene
. Krakendev tiene una explicación más profunda de los tipos de referencia y cómo evitar estos ciclos.
Ahora, vaya a GameViewController.swift
y elimine o comente las siguientes tres líneas de código:
view.showsPhysics = true view.showsFPS = true view.showsNodeCount = true
Con los datos de depuración fuera del camino, ¡el juego se ve realmente bien! Felicitaciones: ¡Actualmente estamos en versión beta! Consulte el código final de hoy en GitHub.
Pensamientos finales
Esta es la lección final de un tutorial de tres partes, y si llegaste hasta aquí, acabas de trabajar mucho en tu juego. En este tutorial, pasaste de una escena que no tenía absolutamente nada a un juego completo. ¡Felicitaciones! En la lección uno, agregamos los sprites del piso, las gotas de lluvia, el fondo y el paraguas. También jugamos con la física y nos aseguramos de que nuestras gotas de lluvia no se acumularan. Comenzamos con la detección de colisiones y trabajamos en la selección de nodos para no quedarnos sin memoria. También agregamos algo de interacción con el usuario al permitir que el paraguas se mueva hacia donde el usuario toca en la pantalla.
En la lección dos, agregamos el gato y la comida, junto con métodos de desove personalizados para cada uno de ellos. Actualizamos nuestra detección de colisiones para permitir el gato y los sprites de comida. También trabajamos el movimiento del gato. El gato obtuvo un propósito: comer toda la comida disponible. Agregamos una animación simple para el gato y agregamos interacciones personalizadas entre el gato y la lluvia. Finalmente, agregamos efectos de sonido y música para que se sienta como un juego completo.
En esta última lección, creamos una pantalla de visualización frontal para contener nuestra etiqueta de puntuación, así como nuestro botón de salida. Manejamos acciones en todos los nodos y permitimos que el usuario saliera con una devolución de llamada desde el nodo HUD. También agregamos otra escena a la que el usuario puede iniciar y volver después de hacer clic en el botón Salir. Manejamos el proceso para iniciar el juego y controlar el sonido en el juego.
A dónde ir desde aquí
Dedicamos mucho tiempo para llegar tan lejos, pero aún queda mucho trabajo por hacer en este juego. RainCat aún continúa en desarrollo y está disponible en la App Store. A continuación se muestra una lista de deseos y necesidades que deben agregarse. Se han agregado algunos de los elementos, mientras que otros aún están pendientes:
- Agregue iconos y una pantalla de inicio.
- Finalice el menú principal (simplificado para el tutorial).
- Corrija errores, incluidas las gotas de lluvia rebeldes y el desove de alimentos múltiples.
- Refactorizar y optimizar el código.
- Cambia la paleta de colores del juego según la puntuación.
- Actualiza la dificultad según la puntuación.
- Anima al gato cuando la comida esté justo encima de él.
- Integra Game Center.
- Dar crédito (incluido el crédito adecuado para las pistas de música).
Realice un seguimiento en GitHub porque estos cambios se realizarán en el futuro. Si tiene alguna pregunta sobre el código, no dude en enviarnos un mensaje a [email protected] y podemos discutirlo. Si ciertos temas reciben suficiente atención, tal vez podamos escribir otro artículo discutiendo el tema.
¡Gracias!
Quiero agradecer a todas las personas que ayudaron en el proceso de creación del juego y en el desarrollo de los artículos que lo acompañan.
- Cathryn Rowe Por el arte inicial, el diseño y la edición, y por publicar los artículos en nuestro Garage.
- Morgan Wheaton Por el diseño final del menú y las paletas de colores (que se verán geniales una vez que implemente estas características, estén atentos).
- Nikki Clark Por los impresionantes encabezados y separadores de los artículos y por su ayuda con la edición de los artículos.
- Laura Levisay Por todos los increíbles GIF en los artículos y por enviarme lindos GIF de gatos como apoyo moral.
- Tom Hudson Por su ayuda con la edición de los artículos y sin los cuales esta serie no se habría realizado en absoluto.
- Lani DeGuire Por su ayuda con la edición de los artículos, que fue un montón de trabajo.
- Jeff Moon Para ayudar a editar la lección tres y el ping-pong. Mucho ping-pong.
- Tom Nelson Por ayudar a asegurar que el tutorial funcione como debería.
En serio, se necesitó un montón de gente para tener todo listo para este artículo y lanzarlo a la tienda.
Gracias a todos los que leen esta frase, también.