Статьи

Создайте игру-головоломку тетромино, используя Swift — рисование объектов

GameScene

Теперь пришло время нарисовать что-то на экране. Класс GameScene который расширяет SKScene уже объявлен для нас по умолчанию в GameScene.swift . Обновите его определение следующим кодом:

 class GameScene: SKScene { override func didMoveToView(view: SKView) { /* Setup your scene here */ let block = SKSpriteNode(color: SKColor.orangeColor(), size: CGSize(width: 50, height: 50)) block.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame)) self.addChild(block) } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { /* Called when a touch begins */ for touch: AnyObject in touches { } } override func update(currentTime: CFTimeInterval) { /* Called before each frame is rendered */ } } 

Давайте сосредоточим наше обсуждение на единственном didMoveToView методе в этом классе, метод didMoveToView который мы SKScene из SKScene . Как описано в комментарии, именно здесь мы должны настроить нашу сцену.

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

 let block = SKSpriteNode(color: SKColor.orangeColor(), size: CGSize(width: 50, height: 50)) 

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

 block.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame)) 

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

 self.addChild(block) 

Запустите ( ⌘ + R ) нашу игру, чтобы увидеть одинокий оранжевый блок в центре экрана.

блок

В следующем разделе мы опишем и нарисуем семь форм тетромино.

Рисование тетромино

Согласно Википедии, тетромино — это геометрическая фигура, состоящая из четырех квадратов, соединенных ортогонально. Существует несколько различных типов тетромино, и среди них мы будем использовать односторонний тетромино, который имеет семь различных форм:

T

S

Z

О

L

J

я

Каждое тетромино может быть представлено буквой, которая абстрактно напоминает его форму. Мы используем буквы I, O, T, J, L, S и Z для представления каждого из этих тетромино. Мы также используем цвета, чтобы еще больше различать формы и, конечно же, добавлять немного глазных конфет.

Класс Тетромино

Давайте создадим новый класс для представления наших тетромино.

  1. Нажмите ⌘ + N, чтобы открыть окно выбора шаблона файла, и выберите Swift File из группы источников iOS. Нажмите Далее, чтобы сохранить файл.

    Новый файл

  2. Назовите файл Tetromino и нажмите «Создать».

    Новый файл

  3. Добавьте следующие перечисления в файл Tetromino.swift после оператора import :

     enum Direction { case Left, Right, Down, None } enum Rotation { case Counterclockwise, Clockwise } enum Shape { case I, O, T, J, L, S, Z static func randomShape() -> Shape { let shapes: [Shape] = [.I, .O, .T, .J, .L, .S, .Z] let count = UInt32(shapes.count) let randShape = Int(arc4random_uniform(count)) return shapes[randShape] } } 

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

    Перечисление Rotation определяет значение против часовой стрелки или по часовой стрелке.

    Перечисление Shape определяет семь отдельных односторонних тетромино, которые мы будем использовать в нашей игре. Одним из преимуществ перечислений в Swift является возможность определять свои собственные методы, которые включают в себя как тип (определенный со static ключевым словом), так и методы экземпляра .

    Глядя на анимацию геймплея выше, мы можем легко сделать вывод, что тетромино генерируются случайным образом. Поэтому для удобства давайте реализуем это непротиворечивое поведение как метод типа с именем randomShape . Мы используем встроенный метод arc4random_uniform , передавая общее количество фигур для генерации случайного числа. Затем мы используем этот номер для доступа к элементу из массива shapes .

  4. Добавьте растровые данные, представляющие фигуры. Данные bitmaps довольно сложны, но не пугайтесь. Это просто тип словаря, который использует Shape как ключ к значению трехмерного массива. Эти массивы представляют все конфигурации вращения формы.

     let bitmaps: [Shape: [[[Int]]]] = [ .I: [[[0, 1], [0, 1], [0, 1], [0, 1]], [[0, 0, 0, 0], [1, 1, 1, 1]]], .O: [[[2, 2], [2, 2]]], .T: [[[3, 3, 3], [0, 3, 0], [0, 0, 0]], [[3, 0], [3, 3], [3, 0]], [[0, 0, 0], [0, 3, 0], [3, 3, 3]], [[0, 0, 3], [0, 3, 3], [0, 0, 3]]], .J: [[[0, 4, 4], [0, 4, 0], [0, 4, 0]], [[4, 0, 0], [4, 4, 4], [0, 0, 0]], [[0, 4, 0], [0, 4, 0], [4, 4, 0]], [[0, 0, 0], [4, 4, 4], [0, 0, 4]]], .L: [[[0, 5, 0], [0, 5, 0], [0, 5, 5]], [[0, 0, 5], [5, 5, 5], [0, 0, 0]], [[5, 5, 0], [0, 5, 0], [0, 5, 0]], [[0, 0, 0], [5, 5, 5], [5, 0, 0]]], .S: [[[0, 6, 0], [0, 6, 6], [0, 0, 6]], [[0, 0, 0], [0, 6, 6], [6, 6, 0]], [[0, 6, 0], [0, 6, 6], [0, 0, 6]], [[0, 0, 0], [0, 6, 6], [6, 6, 0]]], .Z: [[[0, 7, 0], [7, 7, 0], [7, 0, 0]], [[0, 0, 0], [7, 7, 0], [0, 7, 7]], [[0, 7, 0], [7, 7, 0], [7, 0, 0]], [[0, 0, 0], [7, 7, 0], [0, 7, 7]]] ] 

    Мы, конечно, можем реализовать алгоритмы вращения матрицы здесь для динамического поворота растрового изображения формы, но это было бы слишком вычислительно дорого для наших простых нужд. В любом случае, не так много конфигураций, поэтому их «жесткое» кодирование предпочтительнее. Кроме того, мы можем использовать SKAction SpriteKit, чтобы вращать спрайты под заданным углом, но наша игра не работает по структуре данных.

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

     .T: [[ [3, 3, 3], [0, 3, 0], [0, 0, 0]], [ [3, 0], [3, 3], [3, 0]], [ [0, 0, 0], [0, 3, 0], [3, 3, 3]], [ [0, 0, 3], [0, 3, 3], [0, 0, 3] ]], [/code] 

    Массивы и словари

    Наши bitmap данные являются хорошим примером типа коллекции Swift. Это словарь, содержащий коллекцию массивов. Массивы и словари хранят упорядоченные и неупорядоченные коллекции значений соответственно. Доступ к элементам массива и словаря (и их установка) осуществляется с помощью индексной записи . Он использует квадратные скобки ( [] ), которые могут содержать значение индекса, начинающееся с нуля для массивов, или ключ, в нашем случае ключ Shape , для словарей.

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

     bitmaps[.T] 

    Optionals

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

  5. Давайте определим класс Tetromino.

     class Tetromino { let shape = Shape.randomShape() var bitmap: [[Int]] { let bitmapSet = bitmaps[shape]! return bitmapSet[rotationalState] } var position = (x: 0, y: 0) private var rotationalState: Int init() { let bitmapSet = bitmaps[shape]! let count = UInt32(bitmapSet.count) rotationalState = Int(arc4random_uniform(count)) } func moveTo(direction: Direction) { switch direction { case .Left: --position.x case .Right: ++position.x case .Down: ++position.y case .None: break } } func rotate(rotation: Rotation = .Counterclockwise) { switch rotation { case .Counterclockwise: let bitmapSet = bitmaps[shape]! if rotationalState + 1 == bitmapSet.count { rotationalState = 0 } else { ++rotationalState } case .Clockwise: let bitmapSet = bitmaps[shape]! if rotationalState == 0 { rotationalState = bitmapSet.count - 1 } else { --rotationalState } } } } 

    Инициализация свойства

    Свойства экземпляра класса в Swift должны быть определены со значением по умолчанию, таким как свойство неизменяемой shape или им должно быть присвоено значение во время инициализации, как в случае rotationalState . Это означает, что нам нужно реализовать инициализатор для установки значения rotationalState используя ключевое слово init .

    Опять же, мы сталкиваемся с let bitmapSet = bitmaps[shape]! в выражении let bitmapSet = bitmaps[shape]! , Доступ к элементу словаря возвращает необязательный тип, поэтому нам нужен способ доступа к значению, содержащемуся в нем, если он есть. Мы используем восклицательный знак, чтобы развернуть необязательное значение и раскрыть базовое значение.

    Наконец, мы используем arc4random_uniform чтобы назначить случайное начальное вращательное состояние для тетромино.

    Вычисленные Свойства

    Свойство bitmap нашего класса определяется как вычисляемое свойство только для чтения. Этот тип свойства не хранит значение. Вместо этого он действует как функция, которая выдает правильное значение при каждом обращении к нему. Вычисляемые свойства также могут дополнительно получать значения, которые он использует для установки других свойств в классе.

    Кортеж

    Кортеж — это составное значение, состоящее из нуля или более значений разных типов. Кортежи в swift записываются как значения, разделенные запятыми, с необязательными именами, содержащимися в скобках. Здесь мы используем свойство position , кортеж типа (Int, Int) , чтобы отслеживать местоположение x и y этого тетромино на игровой доске.

    Условные заявления

    Наш класс определяет два метода, которые контролируют движение и вращение тетромино. Одним из них является метод moveTo который принимает параметр типа Direction , перечисление, которое мы определили ранее. Этот метод использует оператор switch для проверки всех возможных значений направления, чтобы иметь возможность соответствующим образом регулировать положение тетромино.

    В отличие от операторов switch в большинстве других языков, Swift не позволяет switch к следующему случаю, когда разрыв не встречается. Это добавляет уровень защиты от непреднамеренного запуска неправильных обработчиков из-за пропущенных разрывов. Это также необходимо для обработки всех случаев. Это означает, что вам придется включить регистр по default для всех других значений, если у вас нет конечного набора регистров, как в наших перечислениях.

    Метод rotate принимает параметр типа Rotation но устанавливает значение по умолчанию — .Counterclockwise . Он также использует оператор switch для проверки обоих направлений вращения. Традиционно игрок может вращать кирпичи в обоих направлениях. Но в нашей игре мы позволим игроку вращаться только в одном, чтобы все было просто. Наша цель для обработки обоих заключается в том, чтобы иметь возможность вернуть тетромино в его предыдущее состояние, когда произошло столкновение. Мы обсудим обнаружение столкновений более подробно позже.

    Помимо оператора switch , rotate также использует операторы if для проверки условий массива вне границ. Оператор if принимает условие и выполняет набор операторов, содержащихся в фигурных скобках, если условие истинно. В отличие от других языков, условия в операторе if Swift не нужно заключать в скобки. Однако пара скобок обязательна, даже если она пуста или имеет только один оператор для выполнения.

Класс GameScene

Давайте теперь обновим наш класс GameScene для отображения случайных тетромино со следующим кодом:

 import SpriteKit let colors: [SKColor] = [ SKColor.lightGrayColor(), SKColor.cyanColor(), SKColor.yellowColor(), SKColor.magentaColor(), SKColor.blueColor(), SKColor.orangeColor(), SKColor.greenColor(), SKColor.redColor(), SKColor.darkGrayColor() ] let blockSize: CGFloat = 18.0 class GameScene: SKScene { override func didMoveToView(view: SKView) { /* Setup your scene here */ self.anchorPoint = CGPoint(x: 0, y: 1.0) } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { /* Called when a touch begins */ for touch: AnyObject in touches { drawTetrominoAtPoint(touch.locationInNode(self)) } } override func update(currentTime: CFTimeInterval) { /* Called before each frame is rendered */ } func drawTetrominoAtPoint(location: CGPoint) { let t = Tetromino() for row in 0..<t.bitmap.count { for col in 0..<t.bitmap[row].count { if t.bitmap[row][col] > 0 { let block = t.bitmap[row][col] let square = SKSpriteNode(color: colors[block], size: CGSize(width: blockSize, height: blockSize)) square.anchorPoint = CGPoint(x: 1.0, y: 0) square.position = CGPoint(x: col * Int(blockSize) + col, y: -row * Int(blockSize) + -row) square.position.x += location.x square.position.y += location.y self.addChild(square) } } } } } 

После нашего оператора import мы создали неизменный массив цветных объектов, которые будут использоваться для оживления наших скучных форм. Удобно, что у нас есть предустановленные значения SKColor (просто оболочка для UIColor ), которые мы можем использовать повторно. Как будто они созданы для такой игры, так как они близко соответствуют стандартным цветам официальной игры.

Мы устанавливаем размер каждого блока, а затем приступаем к обновлению класса GameScene . Мы заменяем предыдущее содержимое метода didMoveToView одной строкой кода, которая перемещает опорную точку из нижнего левого угла сцены по умолчанию вплоть до верхнего левого угла. Это существенно перемещает начало координат (0, 0) в этот конкретный угол.

В touchesBegan мы должны пройти через все возможные точки касания из-за того, что мы работаем на мультитач-устройстве. Но так как мы отключили функцию мультитач для этой игры, будет зарегистрировано только первое касание. Затем мы вызываем метод drawTetrominoAtPoint передавая местоположение касания.

Опорные точки и происхождение

В методе drawTetrominoAtPoint мы начинаем с создания тетромино со случайной формой и состоянием вращения. Система координат SpriteKit использует традиционную декартову систему с началом координат (0, 0), которое привязано к определенному местоположению, которое называется anchorPoint . Это отличается от других систем координат экрана, где значение y увеличивается сверху вниз, а начало координат по умолчанию находится в верхнем левом углу.

Это противоречит тому, как определяются наши игровые массивы. Индексы растровых массивов начинаются с 0, практически представляя верхнюю строку, а затем увеличиваются на единицу при переходе вниз по одной строке за раз. Таким образом, чтобы иметь возможность удобно обойти эту ситуацию, мы переместили опорную точку представления из ее местоположения по умолчанию, как описано ранее. Это означает, что мы начинаем с исходного значения y, равного нулю, и движемся вниз вдоль отрицательной оси y. Мы должны будем компенсировать это путем отрицания значений нашего индекса строки, используя унарный оператор - (минус), чтобы иметь возможность правильно расположить блок вдоль оси y:

 square.position = CGPoint(x: col * Int(blockSize) + col, y: -row * Int(blockSize) + -row) 

Для петель

Форма тетромино в нашей игре представлена ​​для удобства в виде растрового изображения. Растровые изображения имеют значения строк и столбцов (x и y), которые представляют пиксели двухмерных изображений. Мы извлекаем данные каждого пикселя, просматривая все строки и столбцы растрового изображения. Swift также имеет свое собственное выражение, как и во многих других языках. В нашем методе drawTetrominoAtPoint у нас есть вложенная реализация цикла for in для доступа к каждому элементу растрового изображения.

Нам нужен доступ к каждому индексу растрового изображения, поэтому мы определяем его как диапазон целых чисел от 0 до, но не включая общее количество строк или столбцов. Мы используем оператор полуоткрытого диапазона .. to express this. If we want to include the total count we'll have to use the closed range operator .. to express this. If we want to include the total count we'll have to use the closed range operator ... чтобы определить диапазон. Текущее значение диапазона сохраняется в любой переменной, введенной после ключевого слова for .

Элемент растрового изображения формы может иметь либо нулевое, либо ненулевое значение. Мы исключаем отображение пикселей с нулевым значением при помощи условного оператора if . Мы приступаем к созданию каждого блочного спрайта, используя тот же процесс, который мы использовали в предыдущем разделе при рисовании блока. Цветовой индекс рассчитывается по значению блока, создавая согласованную цветовую схему для всего набора тетромино.

Положение каждого блока вычисляется на основе длины его стороны и его индекса в массиве. Для достижения эффекта «пробел между блоками» мы добавляем разрыв с шириной в одну точку, используя текущее значение строки или столбца. Мы также хотим, чтобы тетромино было нарисовано рядом с областью, к которой мы прикасались, поэтому мы корректируем положение, добавляя значение местоположения, которое мы получили из параметра. Установка точки привязки блочных спрайтов в правом нижнем углу также помогла центрировать тетромино.

Запустите ( ⌘ + R ) нашу игру и коснитесь любого места на экране симулятора, чтобы нарисовать случайные тетромино и создать собственное произведение искусства, например:

тетромино

В следующем выпуске мы перейдем к забавной части, игровой механике!