В этой статье описывается процесс создания простой игры для тренировки памяти, которая мне очень нравится. Кроме того, что она сама по себе хороша, во время работы вы немного больше узнаете о классах и протоколах 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) {
self.id = NSUUID().uuidString
self.shown = false
self.artworkURL = image
}
Метод для сравнения 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() }
}
}
}
Идем дальше.
Вторая модель — MemoryGame, здесь задаем сетку 4*4. У модели будут такие свойства, как cards (массив карточек на сетке), массив cardsShown с уже открытыми карточками и логическая переменная isPlaying для отслеживания статуса игры.
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 выглядит примерно так:
Изначально в контроллере нужно установить новую игру как viewDidLoad, включая изображения для сетки. В игре все это будет представлено 4*4 collectionView. Если вы еще не знакомы с collectionView, вот здесь можно получить нужную информацию.
Мы настроим GameController в качестве корневого контроллера приложения. В GameController будет collectionView, на который мы будем ссылаться в качестве IBOutlet. Еще одна ссылка — на кнопку 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 рекомендует:
- Практический курс «Мобильный разработчик PRO».
- Прикладной онлайн-курс «Аналитик данных Python».
- Двухлетний практический курс «Я — веб-разработчик PRO».
infund
Странно, оригинальная статья вроде свежая, уже Swift 5.0 на дворе, а начиная с 4.2 у массивов есть shuffled() из коробки, без вот этих вот экстеншнов и arc4random(). Ну и использование NSUUID().uuidString для id плиточки… Слегка ту мач, я бы сказал.