Создание игры для iOS. Часть 3

Привет!
Итак, в прошлой части мы узнали, что такое git и начали создавать игровое поле. Сегодня продолжим разрабатывать поле, для чего напишем пару вспомогательных классов. На их примере мы разберем несколько протоколов и перегрузку операторов, что поможет сильно сократить объем кода и повысить его читабельность. Для тех, кто все пропустил, вот часть 1 и часть 2. Го!

С момента написания прошлого поста я решил внести немного изменений в игровое поле, например, соотношение размеров поля будет 4 х 6, чтобы оптимизировать размер узлов, так как узлы были слишком маленькими. Для этого я поменял числовое значение параметра ratio в файле FieldScene.swift на (4, 6). 

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

Этот пункт внедрить тоже несложно:

В файле Constants.swift меняем значение MENU_HEIGHT на

let TOP_MENU_HEIGHT = CGF(50.0)
let BOTTOM_MENU_HEIGHT = CGF(70.0)

В файле Marking.swift  меняем инициализатор на вот этот:

init(size: CGSize, ratio: (Int, Int)) {
        
        let tmp_x = (size.width - MIN_SPACE * 2) / ratio.0
        let tmp_y = (size.height - MIN_SPACE * 2 - TOP_MENU_HEIGHT - BOTTOM_MENU_HEIGHT) / ratio.1
        
        self.side = min(tmp_x, tmp_y)
        self.frame = (
            (size.width - self.side * ratio.0) / 2,
            BOTTOM_MENU_HEIGHT + ((size.height - BOTTOM_MENU_HEIGHT - TOP_MENU_HEIGHT - self.side * ratio.1) / 2)
        )
        
    }

В файле GameViewController.swift в самом начале класса (внутри) написать:

override var prefersStatusBarHidden: Bool { return true }

Теперь о вспомогательных классах, для этого в папке Grasp создаем папку Supportive, а в ней файл Supportive Classes.swift

 Для начала напишем класс FID, с помощью которого будет однозначно задаваться позиция каждого нашего узла. В нем будут поля x и y, которые будут означать индексы узла по горизонтали и вертикали соответственно.

class FID {
    var x : Int = Int()
    var y : Int = Int()

    init() {}
}

Инициализировать его можно будет по двум значениям типа Int или по паре (Int, Int)

init(_ x: Int, _ y: Int)  {
    self.x = x
    self.y = y
}

// Черточки перед x и y означают, что эти переменные не будут отображться при вызове, например, вместо fid = FID(x: 0, y: 0) будет fid = FID(0, 0)
   
init(fid: (Int, Int)) {
    self.x = fid.0  
    self.y = fid.1
}

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

И прописать в нем hashValue вот таким образом:

var hashValue: Int {
        return Int(x * 1e5 + y)
}

// 1e5 = 100000

теперь нужно добавить в него оператор сравнения, чтобы наш класс удовлетворял протоколу Equatable:

static func == (l: FID, r: FID) -> Bool {
        return l.x == r.x && l.y == r.y
}

В итоге наш класс выглядит так:

class FID : Hashable {
    var x : Int = Int()
    var y : Int = Int()
    
    var hashValue: Int {
        return Int(x * 1e5 + y)
    }
    
    
    static func == (l: FID, r: FID) -> Bool {
        return l.x == r.x && l.y == r.y
    }
    
    
    init() {}
    init(_ x: Int, _ y: Int) {
        self.x = x
        self.y = y
    }
    init(_ fid: (Int, Int)) {
        self.x = fid.0
        self.y = fid.1
    }
}

А еще нам нужен класс Direction, который будет отвечать за направление, он будет очень похож на класс FID, но x и y в нем могут принимать значения только в промежутке [-1, 1] и модуль их суммы всегда равен 1, проще говоря только значения (-1, 0); (0, -1); (1, 0); (0, 1).

class Direction : Hashable {
    var x : Int = Int()
    var y : Int = Int()
    
    var hashValue: Int {
        return Int(x * 1e5 + y)
    }
    
    
    static func == (l: Direction, r: Direction) -> Bool {
        return l.x == r.x && l.y == r.y
    }
    
    
    init() {}
    init(_ x: Int, _ y: Int) {
        self.x = x
        self.y = y
    }
}

Вместо него можно было бы использовать FID, но так как они имеют разное предназначения, мы их четко разделяем

Что ж, давайте теперь начнем делать нашы узлы, для этого создадим в паке Field папку Cell, а в ней файл Cell.swift

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

var  fid : FID = FID()
var ways : [Direction : Bool] = [Direction : Bool]()

Инициализировать узел будем по размеру и fid:

init(size: CGSize, fid: FID) {
        
        super.init(texture: SKTexture(imageNamed: "Cell"), color: UIC.clear, size: size)
	// эту строчку далее разберем подробнее
        
        self.fid = fid
        
        for i in -1...1 {
            for j in -1...1 {
                if (i != j) {
                    self.ways[Direction(i, j)] = true
                }
            }
        }
	// полагаем, что из текущего узла можно перейти во все соседние
}

Строчка

super.init(texture: SKTexture(imageNamed: "Cell"), color: UIC.clear, size: size) означает, что мы инициализируем родительский класс SKSpriteNode с помощью текстуры, определяемой изображением Cell, и заданного размера size.

Изображение Cell мы создадим с помощью фотошопа, создаем новый файл, ставим размер 512Х512, с прозрачным фоном:

 

 

В инструментах выбираем эллипс, даем ему размеры нашего изображения и центруем его:

 

 

 

Сохраняем в формате .png, важно чтобы не было сжатия,

Перетаскиваем получившееся изображение в Assets.xassets

 

На данном этапе класс Field будет выглядеть так:

class Field {

    var scheme : [[Cell]] = [[Cell]]()

   

   

    init() {}

   

    //    init(source: JSON) {}

   

    init(width: Int, height: Int, cellSize: CGSize) {

       

        for i in 0 ..< width {

            var column = [Cell]()

            for j in 0 ..< height {

                let c = Cell(size: cellSize, fid: FID(i, j))

                column.append(c)

            }

            scheme.append(column)

        }

    }

   

   

}

Мы просто заполняем массив scheme  нашими узлами.

Теперь давайте визуализируем всю нашу работу, для этого перепишем файл FieldScene.swift:

class FieldScene: SKScene {
    
    final var ratio : (Int, Int) = (4, 6)
    var mark : Marking = Marking()
    var field: Field = Field()
    
    override init(size: CGSize) {
        super.init(size: size)
        
        
        self.mark = Marking(size: size, ratio: self.ratio)
        self.backgroundColor = UIC.white
        
        field = Field(width: ratio.0, height: ratio.1, cellSize: CGSize(width: mark.side * 0.6, height: mark.side * 0.6))
        
        let top_menu = SKSN(color: UIC.blue, size: CGSize(width: screen.width, height: TOP_MENU_HEIGHT))
        top_menu.anchorPoint = CGPoint(x: 0, y: 0)
        top_menu.position = CGPoint(x: 0, y: screen.height - TOP_MENU_HEIGHT)
        self.addChild(top_menu)
        
        for var i in 0..<ratio.0 {
            for var j in 0..<ratio.1 {
                field.scheme[i][j].position.x = mark.frame.0 + (i + 0.5) * mark.side
                field.scheme[i][j].position.y = mark.frame.1 + (j + 0.5) * mark.side
                field.scheme[i][j].anchorPoint = CGPoint(x: 0.5, y: 0.5)
                self.addChild(field.scheme[i][j])
            }
        }
        
        let bottom_menu = SKSN(color: UIC.darkGray, size: CGSize(width: screen.width, height: BOTTOM_MENU_HEIGHT))
        bottom_menu.position = CGPoint(x: 0, y: 0)
        bottom_menu.anchorPoint = CGPoint(x: 0, y: 0)
        self.addChild(bottom_menu)
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

Здесь мы заменили StatusBar  на TopMenu и стали отображать узлы как конкретные объекты из класса Field.

Запускаем, в итоге получаем такую картину:

Ну ВОТ, стало гораздо наряднее.

В следующей части мы разберем очень полезный инструмент под названием JSON, разнообразим наш ландшафт и создадим зачаточную версию нашей карты. А чтобы прочитать статью сразу после выхода - подписывайся на обновления, встретимся через неделю. Пока!

Подписаться на обновления