ИгрыХабрахабр

[Перевод] Пишем игру «Карточки памяти» на Swift

Кроме того, что она сама по себе хороша, во время работы вы немного больше узнаете о классах и протоколах Swift. В этой статье описывается процесс создания простой игры для тренировки памяти, которая мне очень нравится. Но прежде чем начать, давайте разберемся в самой игре.

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Skillbox рекомендует: Образовательный онлайн-курс «Профессия Java-разработчик».

Как играть в Memory Card

Игра начинается с демонстрации набора карточек. Они лежат «рубашкой» вверх (соответственно, изображением вниз). Когда вы кликаете по любой, на несколько секунд открывается изображение.

Если после открытия первой карты вы переворачиваете вторую и картинки совпадают, обе карточки остаются открытыми. Задача игрока — найти все карточки с одинаковыми картинками. Задача — открыть все. Если не совпадают, карточки снова закрываются.

Структура проекта

Для того, чтобы создать простую версию этой игры нужны следующие компоненты:

  • Один контроллер (One Controller): GameController.swift.
  • Один просмотр (One View): CardCell.swift.
  • Две модели (Two Models): MemoryGame.swift and Card.swift.
  • Main.storyboard для того, чтобы весь набор компонентов был в наличии.

Начинаем с самого простого компонента игры, карточки.

Card.swift

У модели карточки будет три свойства: id для идентификации каждой, логическая переменная shown для уточнения статуса карты (скрыта или открыта) и artworkURL для картинок на карточках.

class Card { var id: String var shown: Bool = false var artworkURL: UIImage!
}

Также будут нужны эти методы для управления взаимодействием пользователя с картами:

Для id генерируем случайный id путем вызова NSUUIS().uuidString. Метод для вывода изображения на карту. Здесь мы сбрасываем все свойства на дефолтные.

init(image: UIImage)

Метод для сравнения id карт.

func equals(_ card: Card) -> Bool { return (card.id == id) }

Метод для создания копии каждой карточки — для того, чтобы получить большее число одинаковых. Этот метод будет возвращать card с аналогичными значениями.

func copy() -> Card { return Card(card: self) } init(card: Card) { self.id = card.id self.shown = card.shown self.artworkURL = card.artworkURL }

И еще один метод нужен для перемешивания карточек на старте. Мы сделаем его расширением класса Array.

extension Array { mutating func shuffle() { for _ in 0...self.count { sort { (_,_) in arc4random() < arc4random() } } }
}

А вот реализация кода для модели Card со всеми свойствами и методами.

class Card { var id: String var shown: Bool = false var artworkURL: UIImage! static var allCards = [Card]() init(card: Card) { self.id = card.id self.shown = card.shown self.artworkURL = card.artworkURL } init(image: UIImage) { self.id = NSUUID().uuidString self.shown = false self.artworkURL = image } func equals(_ card: Card) -> Bool { return (card.id == id) } func copy() -> Card { return Card(card: self) }
} extension Array { mutating func shuffle() { for _ in 0...self.count { sort { (_,_) in arc4random() < arc4random() } } }
}

Идем дальше.

У модели будут такие свойства, как cards (массив карточек на сетке), массив cardsShown с уже открытыми карточками и логическая переменная isPlaying для отслеживания статуса игры. Вторая модель — MemoryGame, здесь задаем сетку 4*4.

class MemoryGame { var cards:[Card] = [Card]() var cardsShown:[Card] = [Card]() var isPlaying: Bool = false
}

Нам также нужно разработать методы для управления взаимодействия пользователя с сеткой.

Метод, который перемешивает карточки в сетке.

func shuffleCards(cards:[Card]) -> [Card] { var randomCards = cards randomCards.shuffle() return randomCards }

Метод для создания новой игры. Здесь мы вызываем первый метод для старта начальной раскладки и инициализируем переменную isPlaying как true.

func newGame(cardsArray:[Card]) -> [Card] { cards = shuffleCards(cards: cardsArray) isPlaying = true return cards }

Если мы хотим перезапустить игру, то устанавливаем переменную isPlaying как false и убираем первоначальную раскладку карточек.

func restartGame() { isPlaying = false cards.removeAll() cardsShown.removeAll() }

Метод для верификации нажатых карточек. Подробнее о нем позже.

func cardAtIndex(_ index: Int) -> Card? { if cards.count > index { return cards[index] } else { return nil } }

Метод, возвращающий позицию определенной карточки.

func indexForCard(_ card: Card) -> Int? { for index in 0...cards.count-1 { if card === cards[index] { return index } } return nil }

Проверка соответствия выбранной карточке эталону.

func unmatchedCardShown() -> Bool { return cardsShown.count % 2 != 0
}

Этот метод читает последний элемент в массиве **cardsShown** и возвращает несоответствующую карточку.

func didSelectCard(_ card: Card?) { guard let card = card else { return } if unmatchedCardShown() { let unmatched = unmatchedCard()! if card.equals(unmatched) { cardsShown.append(card) } else { let secondCard = cardsShown.removeLast() } } else { cardsShown.append(card) } if cardsShown.count == cards.count { endGame() } }

Main.storyboard и GameController.swift

Main.storyboard выглядит примерно так:

В игре все это будет представлено 4*4 collectionView. Изначально в контроллере нужно установить новую игру как viewDidLoad, включая изображения для сетки. Если вы еще не знакомы с collectionView, вот здесь можно получить нужную информацию.

В GameController будет collectionView, на который мы будем ссылаться в качестве IBOutlet. Мы настроим GameController в качестве корневого контроллера приложения. Еще одна ссылка — на кнопку IBAction onStartGame (), это UIButton, ее вы можете увидеть в раскадровке под названием PLAY.

Немного о реализации контроллеров:

  • Сначала инициализируем два главных объекта — сетку (the grid): game = MemoryGame(), и на набор карточек: cards = [Card]().
  • Устанавливаем начальные переменные как viewDidLoad, это первый метод, который вызывается в процессе работы игры.
  • collectionView устанавливаем как hidden, поскольку все карты скрыты до того момента, пока пользователь не нажмет PLAY.
  • Как только нажимаем PLAY, стартует раздел onStartGame IBAction, и мы выставляем свойство collectionView isHidden как false, чтобы карточки могли стать видимыми.
  • Каждый раз, когда пользователь выбирает карточку, вызывается метод didSelectItemAt. В методе мы вызываем didSelectCard для реализации основной логики игры.

Вот финальная реализация GameController:

class GameController: UIViewController { @IBOutlet weak var collectionView: UICollectionView! let game = MemoryGame() var cards = [Card]() override func viewDidLoad() { super.viewDidLoad() game.delegate = self collectionView.dataSource = self collectionView.delegate = self collectionView.isHidden = true APIClient.shared.getCardImages { (cardsArray, error) in if let _ = error { // show alert } self.cards = cardsArray! self.setupNewGame() } } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if game.isPlaying { resetGame() } } func setupNewGame() { cards = game.newGame(cardsArray: self.cards) collectionView.reloadData() } func resetGame() { game.restartGame() setupNewGame() } @IBAction func onStartGame(_ sender: Any) { collectionView.isHidden = false }
} // MARK: - CollectionView Delegate Methods
extension GameController: UICollectionViewDelegate, UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return cards.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CardCell", for: indexPath) as! CardCell cell.showCard(false, animted: false) guard let card = game.cardAtIndex(indexPath.item) else { return cell } cell.card = card return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let cell = collectionView.cellForItem(at: indexPath) as! CardCell if cell.shown { return } game.didSelectCard(cell.card) collectionView.deselectItem(at: indexPath, animated:true) }
}

Теперь давайте немного остановимся на важных протоколах.

Протоколы

Работа с протоколами — основа основ программирования на Swift. Протоколы дают возможность задать правила для класса, структуры или перечисления. Этот принцип позволяет писать модульный и расширяемый код. Фактически это шаблон, который мы уже реализуем для collectionView в GameController. Теперь сделаем собственный вариант. Синтаксис будет выглядеть так:

protocol MemoryGameProtocol { //protocol definition goes here
}

Мы знаем, что протокол позволяет определить правила или инструкции для реализации класса, поэтому давайте подумаем, какими они должны быть. Всего нужно четыре.

  • Начало игры: memoryGameDidStart.
  • Нужно перевернуть карточку рубашкой вниз: memoryGameShowCards.
  • Нужно перевернуть карточку рубашкой вверх: memoryGameHideCards.
  • Завершение игры: memoryGameDidEnd.

Все четыре метода реализуем для основного класса, а это GameController.

memoryGameDidStart

Когда этот метод запущен, игра должна начаться (пользователь нажимает PLAY). Здесь просто перезагрузим контент, вызвав collectionView.reloadData (), что приведет к перемешиванию карт.

func memoryGameDidStart(_ game: MemoryGame) { collectionView.reloadData()
}

memoryGameShowCards

Вызываем этот метод из collectionSDViewSelectItemAt. Сначала он показывает выбранную карту. Затем проверяет, есть ли в массиве cardsShown несопоставленная карта (если число cardsShown нечетное). Если такая есть, выбранная карта сравнивается с ней. Если картинки одинаковые, обе карты добавляются к cardsShown и остаются открытыми. Если разные, карта уходит из cardsShown, и обе переворачиваются рубашкой вверх.

memoryGameHideCards

Если карты не соответствуют друг другу, вызывается этот метод, и картинки карточек скрываются.

shown = false.

memoryGameDidEnd

Когда вызывается этот метод, означает, что все карты уже открыты и находятся в списке cardsShown: cardsShown.count = cards.count, так что игра окончена. Метод вызывается специально после того, как мы вызвали endGame (), чтобы установить isPlaying var в false, после чего показывается сообщение о завершении игры. Также alertController используется в качестве индикатора для контроллера. Вызывается viewDidDisappear и игра сбрасывается.

Вот как все это выглядит в GameController:

extension GameController: MemoryGameProtocol { func memoryGameDidStart(_ game: MemoryGame) { collectionView.reloadData() } func memoryGame(_ game: MemoryGame, showCards cards: [Card]) { for card in cards { guard let index = game.indexForCard(card) else { continue } let cell = collectionView.cellForItem( at: IndexPath(item: index, section:0) ) as! CardCell cell.showCard(true, animted: true) } } func memoryGame(_ game: MemoryGame, hideCards cards: [Card]) { for card in cards { guard let index = game.indexForCard(card) else { continue } let cell = collectionView.cellForItem( at: IndexPath(item: index, section:0) ) as! CardCell cell.showCard(false, animted: true) } } func memoryGameDidEnd(_ game: MemoryGame) { let alertController = UIAlertController( title: defaultAlertTitle, message: defaultAlertMessage, preferredStyle: .alert ) let cancelAction = UIAlertAction( title: "Nah", style: .cancel) { [weak self] (action) in self?.collectionView.isHidden = true } let playAgainAction = UIAlertAction( title: "Dale!", style: .default) { [weak self] (action) in self?.collectionView.isHidden = true self?.resetGame() } alertController.addAction(cancelAction) alertController.addAction(playAgainAction) self.present(alertController, animated: true) { } resetGame() }
}


Собственно, вот и все. Вы можете использовать этот проект для создания собственного варианта игры.

Удачного кодинга!

Skillbox рекомендует:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть