Pulling subclass from String


#1

Hi, Paul

I got stuck on something, while working on a game.
I have the following situation.
I have a custom class, let’s say something like Enemy.
Then I have Subclasses like Wizard: Enemy , Thief: Enemy …etc.
They each share some stuff, but they have their own as well. Each class have a function inside that
will calcultate it’s stats based on the level, so it’s something like:

func getStats(forLevel: Int) {

armor = Int(armorBase*((forLevel + 1) / 2)) and so on...

}

So my problem comes here.
I store what enemy is what type and level in pList as Arrays and Dictionaries.
However when I get the information that’s the only way I figured it out:

First I get the Data for enemy 1 , if present:

if let enemyData = levelToLoad[1] as? NSDictionary {
            enemy1class = enemyData["type"] as? String
            enemy1lvl = enemyData["level"] as? Int
            enemy1 = true
            println("Enemy: \(enemy1class), level: \(enemy1lvl)")
            
            println("Number of enemies is: \(numberOfEnemies)")
        }

And then I am trying to use it:

let enemy1New: AnyClass! = NSClassFromString(enemy1class)

However I can’t use the function within the enemy, because it was declared as AnyClass!, not the actual class it is…

enemy1New.updateStats?

I was discussing this with somebody, who told me I can make a protocol that includes, let’s say the getStats() and then have AnyClass conform to that protocol. However I am not exactly sure if that will work or if that is the best decision.

Let me know if you have ideas, many thanks !


#2
  1. You need to make that Enemy base class and use methods that other subclasses will override.
  2. You need to conform to NSCoding, which existing Sprite Kit SKNode’s conform to for saving/loading.

Then you need a level or game format, and within that you’ll have arrays of either Enemy objects and any neutral/good objects.

It could look like:

  • GameCharacter
  • PlayerCharacter
  • EnemyCharacter

Then enemy would have more subclasses with specific values/attributes/methods.

  • EnemyCharacter
    • Thief
    • Wizard

If you use Array and Dictionary, you can specify types and don’t have to deal with AnyObject types.

Here’s a brief example that should point you in the right direction. It’s a little tricky with the fact that SKSpriteNode’s have failable initializers and they already conform to NSCoding. Otherwise it is pretty straight forward if you remember doing the work in the Swift course.

import UIKit
import SpriteKit


// SKSpriteNode already conforms to NSCoding, you just need to override 
// Otherwise you'll see an error message: "Non-failable initializer requirement 'init(coder:)' cannot be satisfied ..."
class GameCharacter: SKSpriteNode {  // Don't need to add NSCoding after SKSpriteNode
    
    // all objects have health (any attribute that is common to your game)
    var health: Double = 100  // might need accuracy for attacks – up to you.
    
    // Base class method to override
    func getArmorStat(level: Int) -> Int {
        return 1 + level * 10
    }
    
    // NSCoding for saving/loading
    
    // SpriteKitNode can fail, so we have a failable initializer

    required init?(coder aDecoder: NSCoder) {
        // restore any subclass data

        health = aDecoder.decodeDoubleForKey("healthKey")
        
        // Set any default values for non-savable information

        super.init(coder: aDecoder) // Must restore base class for SpriteKit!
    }
    
    override func encodeWithCoder(aCoder: NSCoder) {
        super.encodeWithCoder(aCoder)
        
        // save the health data
        aCoder.encodeDouble(health, forKey: "healthKey") // must match key (constants strings are preferred)
    }
    
}

class EnemyCharacter: GameCharacter {
    
    // create new variables here to store enemy specific data
    var attackDamage: Double = 10.0  // set a default value, or use init()
    
    override func getArmorStat(level: Int) -> Int {
        return super.getArmorStat(level) * 2 // 100% increase by default
    }
    
    func attack(otherCharacter: GameCharacter) {
        otherCharacter.health -= attackDamage
    }
    
    // TODO: conform to NSCoding by overriding methods
}

class Theif: EnemyCharacter {
    
    // TODO: conform to NSCoding
}

// SKNode conforms to NSCoding already

class LevelObjects: SKNode {  // You might just use NSObject, NSCoding instead of SKNode
    
    var enemyArray: [EnemyCharacter]
    
    required init?(coder aDecoder: NSCoder) {
        
        // restore game data

        if let enemies = aDecoder.decodeObjectForKey("enemyArrayKey") as [EnemyCharacter]? {
            // loaded enemy array
            enemyArray = enemies
        } else {
            // create a new array to start using
            enemyArray = [EnemyCharacter]()
        }
        
        // Restore any base class data
        super.init(coder: aDecoder)
    }
    
    override func encodeWithCoder(aCoder: NSCoder) {
        super.encodeWithCoder(aCoder)
        
        // save data
        
        aCoder.encodeObject(enemyArray, forKey: "enemyArrayKey")
    }
    
    
}

#3

Great, thanks! I’ll see where that will take me :smiley:
I was trying to make a simpler game, but my years of gaming experience keep setting the bar higher for myself :smiley:


#4

Hi, @PaulSolt

Unfortunately, I can’t quite figure it out :sob:
I rewatched the NSCoding tutorial and reviewed the app with NSCoding and NSKeyedArchiver, but it’s still confusing to me.
I understand pretty much how I can save a characrter of type Thief and then load it back up.
However in my case there is no saving.
I just have an NSDictionary that says for Enemy1: type = thief, level = 2.
That data is not saved, I manually input this data in the plist file.
The idea is that I want to have only one battle scene and one .swift file for it, but load multiple level battles with it.
So for level 2 I load the battle scene if there is battle and the battle logic will look into the plist and get the enemies. I just need to be able to look in there and convert each of these to the correct class.
So let’s say I have Thief, 2 ; Warrior, 3 . I need to create: 2 objects, enemy1: subclassed from Thief, with override of level = 2, and enemy2, subclassed from Warrior, with override of level = 3.
Once I do that I want to either have the classes automatically recalculate all theirs stats based on the level ( preferably ) or run a class function that updates the stats, based on the level.
Not sure if that help :slight_smile: Thanks again.


#5

Wow, ok I think I goofed on this one. Misunderstood, but I still think saving the data to archives could be beneficial, but you need a quick way to change level formats that’s human writable (not .plist).

I would probably use JSON or just plain text to describe objects in level.

Here’s some code stubs to get you started.

class GameObject: NSObject {
    
    var level: Int = 0
    
}

class EnemyCharacter: GameObject {
    
}

class Thief: EnemyCharacter {
    
}

class LevelManager: NSObject {
    
    func parseLine(line: String) -> GameObject? {
        var gameObject: GameObject?
        
        var lineParts = line.componentsSeparatedByString(" ")
        if lineParts.count >= 2 {
            
            var type: String = lineParts[0] // first part of string (brittle quick test)
            var attribute: String = lineParts[1] // second part of string, etc.
            
            if type == "Thief" {
                if let level = attribute.toInt() {
                    var thief = Thief()
                    // set any thief attributes
                    
                    // return the thief as a gameObject
                    gameObject = thief
                    
                    // set any game object properties
                    gameObject?.level = level
                } // else { // invalid attribute for a thief
                
            }
            // else if ... Assassin ...
            
        } else {
            println("malformed line (error in data)!")
        }

        
        return gameObject  // only returns a valid character if line was parsed
    }
 
    
    func parseFile(path: String) -> [GameObject]? {
        
        // TODO: process each line
        
        // TODO: return all game objects in a list, etc.

        return nil
    }
}

Your data file would have the format like this:

Thief 20
Thief 5
Assassin 3

#6

Great ! Thanks !
That’s what I thought :slight_smile: I was afraid I’ll have to do multiple if statement checks for each class.
That should work just fine for my project.
I am wondering how they do it for big games where you might have hundreds of different possible classes.
The only other option is to build each level separate defining each enemy on the map.


#7

The naive approach is always the best place to start. Just write the code and make it work as quickly as possible.

You could do a registration system to add all the objects automatically, but it really doesn’t save that much code, but might make it less error prone.

The big upside is that you can add new classes and as long as you implement the methods, you only have to register a new object and you can create new objects.

The following code may reduce bugs when you forget to add parsing logic to the LevelManager.swift file. This code is an abstract factory design pattern. You can read more below.

#Base classes

Your base class would have to have two methods that all subclasses would implement.

class GameObject: NSObject {
    
    var level: Int = 0
    
    // might not need attributes -> use an optional
    // otherwise use a non-optional if it's required
    func create(attributeString: String?) -> GameObject? {
        return nil //TODO
    }
    
    func isType(typeString: String) -> Bool {
        return false // TODO
    }
}

class EnemyCharacter: GameObject {
    // TODO (any special data for a enemy or functionality)
}

#Enemy.swift class
You would then override these methods in the subclasses.

class Thief: EnemyCharacter {
    
    // Sample override from Thief class
    override func isType(typeString: String) -> Bool {
        return typeString == "Thief"
    }
    
    override func create(attributeString: String?) -> GameObject? {
        var gameObject: GameObject?
        if let level = attributeString?.toInt() {
            var thief = Thief()
            thief.level = level
            
            // setup any other thief attributes
            
            // return the resulting game object
            gameObject = thief
        }
        return gameObject
    }
}

Then you would register any game objects in an array in your LevelManager.swift file, and you can then parse the string type and ask any of the registered game objects if they can parse the data.

class LevelManager2: NSObject {
    var gameObjects = [GameObject]()
    
    func registerType(gameObject: GameObject) {
        gameObjects.append(gameObject)
    }
    
    func parseLine(line: String) -> GameObject? {
        var result: GameObject?
        
        // split line into type and then attributes (can be multiple parts)
        var lineParts = line.componentsSeparatedByString(" ")
        if lineParts.count >= 2 {
            
            var type: String = lineParts[0] // first part of string (brittle quick test)
            var attribute: String = lineParts[1] // second part of string, etc.
            
            for gameObject in gameObjects {
                if gameObject.isType(type) {
                    result = gameObject.create(attribute)
                }
            }
        }
        return result
    }
}

#Summary
What you’ve implemented if you use this type of code is a factory pattern. It’s an abstract factory pattern since the LevelManager doesn’t know anything about the implementation details of the subclasses. It doesn’t even have to know they exist.

Your GameViewController might control the LevelManager, and that’s where the different types of game objects can be registered.

class GameViewController: UIViewController {
    
    // level manager
    var levelManager: LevelManager2!
    
    override func viewDidLoad() {
        
        // setup the level manager with game objects
        levelManager = LevelManager2()

        // Add as many as you like
        levelManager.registerType(Thief())
        
    }
    
    func parseFile(path: String) {
        // TODO: parse the file
        
        var text = "load something from a file"// load file from string
        var lines = text.componentsSeparatedByString("\n")
        for line in lines {
            levelManager.parseLine(line)
        }
    }
}

You can read a little on this type of design pattern here: http://en.wikipedia.org/wiki/Abstract_factory_pattern

#Use case on the App Store
I used this when I created Artwork Evolution on the App Store. It is an artwork creation app that I’ve used to create abstract fractal-like artwork (which I’ve sold around the country).

The app uses math expressions that are parsed from files and then turned into images. Then it saves them back to a file.