• Simple Games Using SpriteKit


    在ios7,苹果引入了SpriteKit,一个高性能渲染2D的框架。不像中心库(专注于画图)或中心动画(专注于动画过度),SpriteKit专注于不同领域-video games,它是苹果首次涉足ios的图形游戏编程的时代。在发布ios7盒OS X10.9(Mavericks.   2013年WWDC发布)的同时,为了写程序更为简单提供了相同的API在两个平台,尽管苹果从未像SpriteKit提供了一个框架,它有明显的相似之处是Cocos2D等各种开源库。如果你使用的是Cocos2D或类似的过去,你会感觉很熟悉。

    现在创建一个工程并选择Game template名为:TextShooter 

    (sks文件只是标准的归档文件,你可以用NSKeyedUnarchiver和NSKeyedArchiver类来写和读)

    xcode会为你初始化一些方法例如:

     override func viewDidLoad() {
            super.viewDidLoad()
            
            if let view = self.view as! SKView? {
                // 初始化'GameScene.sks'
                if let scene = SKScene(fileNamed: "GameScene") {
                    // 让缩放比例填充整个窗口Set the scale mode to scale to fit the window
                    scene.scaleMode = .aspectFill
                    
                    // 加载这个场景(新场景取代旧场景)
                    view.presentScene(scene)
                }
                //当运行时,忽视父子类的关系
                view.ignoresSiblingOrder = true
                //在右下角显示FPS的值
                view.showsFPS = true
           //在右下角显示结点(node)的个数 view.showsNodeCount = true } }

     了解完xcode自动初始化的代码,接下来我们自己手动初始化我们自己想要的,选择GameScence.swift,我们不需要didMoveToView()这个方法,现在改成:

        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            for touch in touches {
                let location = touch.location(in: self)//获取当前位置
            }
        }

    xcode自带一个GameScene.sks,里面没有我们想要的属性,所以得自己创建,需要添加属性为当前游戏等级数,生活玩家的数量,一个标志,让我们知道等级是否完成,修改GameScene.swift:    private var levelNumber: Int //等级制度    private var playerLives: Int //玩家血    private var finished = false //当前游戏是否结束    class func scene(size:CGSize, levelNumber:Int) -> GameScene {        return GameScene(size: size, levelNumber: levelNumber)}

    override convenience init(size:CGSize) {
            self.init(size: size, levelNumber: 1)
    }
    /*
    创建名为SKLabelNode类的两个实例,并选择一个字体,设置一个文本值,指定一些对齐
    */ init(size:CGSize, levelNumber:Int) { self.levelNumber
    = levelNumber self.playerLives = 5 super.init(size: size) backgroundColor = SKColor.lightGray() let lives = SKLabelNode(fontNamed: "Courier")//指定字体 lives.fontSize = 16 lives.fontColor = SKColor.black() lives.name = "LivesLabel" lives.text = "Lives: (playerLives)" lives.verticalAlignmentMode = .top lives.horizontalAlignmentMode = .right lives.position = CGPoint(x: frame.size.width, y: frame.size.height) addChild(lives) let level = SKLabelNode(fontNamed: "Courier") level.fontSize = 16 level.fontColor = SKColor.black() level.name = "LevelLabel" level.text = "Level (levelNumber)" level.verticalAlignmentMode = .top level.horizontalAlignmentMode = .left level.position = CGPoint(x: 0, y: frame.height) addChild(level) } required init?(coder aDecoder: NSCoder) { levelNumber = aDecoder.decodeInteger(forKey: "level") playerLives = aDecoder.decodeInteger(forKey: "playerLives") super.init(coder: aDecoder) }
    /*
    required的使用规则:
    required修饰符只能用于修饰类初始化方法
    当子类含有异于父类的初始化方法时(初始化方法参数类型和数量异于父类),子类必须要实现父类的required初始化方法,并且也要使用required修饰符而不是override
    当子类没有初始化方法时,可以不用实现父类的required初始化方法
    */
    override func encode(with aCoder: NSCoder) { aCoder.encode(Int(levelNumber), forKey: "level") aCoder.encode(playerLives, forKey: "playerLives") }
    /*
    我们给每个label命名,是因为init(coder:)和encode(with aCoder:)方法需要,所有SpriteKit结点,包括SKScene都遵循NSCoding协议
    */

     我们配置了两个SKLabelNode,是时候让它们现身了,选择GameView.swift并添加以下代码:

    override func viewDidLoad() {
            super.viewDidLoad()
            let scene = GameScene(size: view.frame.size, levelNumber: 1)
            
            //configure the view
            let skView = self.view as! SKView
            skView.showsFPS = true
            skView.showsNodeCount = true
            
            //Sprite Kit applies additional optimizations to improve rendering performance
            skView.ignoresSiblingOrder = true
            
            //Set the scale mode to scale to fit the window
            scene.scaleMode = .aspectFill
            
            skView.presentScene(scene)
        }

    现在你可以顺便了解下override var prefersStatusBarHidden,return true就是状态栏隐藏,反之。现在可以运行下,正常的结果:

    背景有了,接下来可以添加一些互动了,毕竟是游戏,我们先添加一个发射子弹的头部,创建Cocoa Touch class 并以SKNode为父类,命名为playerNode,添加一下代码:

    import SpriteKit
    
    class PlayerNode: SKNode {
        override init() {
            super.init()
            name = "Player (self)"
            initNodeGraph() //初始化一个结点,内容为"^"(将V旋转180度)作为发射子弹的头部
            
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
        private func initNodeGraph() {
            let label = SKLabelNode(fontNamed: "Courier") //指定字体
            label.fontColor = SKColor.blue
            label.fontSize = 40
            label.text = "v"
            label.zRotation = CGFloat(Double.pi) //绕z轴旋转180度
            label.name = "label"
            self.addChild(label)
        }
    }

    跟刚才添加level,lives一样,在GameScene,swift里实例化(实现)playerNode:

    在addChild(level)后面加上这两行:

            playerNode.position = CGPoint(x: frame.midX, y: frame.height * 0.1)

            addChild(playerNode)

    现在运行你就会看到如下场景:

    现在我们来讨论如何用手指来移动它,这边插个题外话,在web前端里面坐标轴是已左上角为基准,但在SpriteKit我测试了下,添加一个结点并设置position(x:0,y:0),效果如下图所示:

    所以这里的坐标是以左下角为基准。

    我们假设当手指在屏幕下方的0.2部分(以下)滑动的时候就是有意要让发射器移动,下面这段代码就是这个意思:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            for touch in touches {
                let location = touch.location(in: self)
                if location.y < frame.height * 0.2 {
                    let target = CGPoint(x: location.x, y: playerNode.position.y)
                    playerNode.moveToward(target) //移动到手指当前位置
                }
            }
        }

    并且在playerNode类添加:

        func moveToward(_ location: CGPoint) {
            removeAction(forKey: "movement")
            let distance = pointDistance(position, location) //计算当前位置和发射器位置的直线距离
            let screenWidth = UIScreen.main.bounds.size.width 
            let duration = TimeInterval(2 * distance/screenWidth) //转换为时间间隔专用的单位,例如:毫秒
            run(SKAction.move(to: location, duration: duration),
                withKey:"movement")//duration:指定移动过程需要的时间,可以自己指定
    }

    可以发现上面的pointDistance()并没有定义,这边可以创建一个swift文件专门放置计算点或向量之类的算法,我的算法代码如下:

    import UIKit
    
    // Takes a CGVector and a CGFLoat.
    // 返回一个新向量(旧向量的x,y分量乘以参数CGPoint)
    func vectorMultiply(_ v: CGVector, _ m: CGFloat) -> CGVector {
        return CGVector(dx: v.dx * m, dy: v.dy * m)
    }
    // Takes two CGPoints.
    // Returns a CGVector representing a direction from p1 to p2.
    
    func vectorBetweenPoints(_ p1: CGPoint, _ p2: CGPoint) -> CGVector {
        return CGVector(dx: p2.x - p1.x, dy: p2.y - p1.y)
    }
    // Takes a CGVector.
    // Returns a CGFloat containing the length of the vector, calculated using
    // Pythagoras' theorem.
    //√(x^2+y^2)
    func vectorLength(_ v: CGVector) -> CGFloat {
        return CGFloat(sqrtf(powf(Float(v.dx), 2) + powf(Float(v.dy), 2)))
    }
    // Takes two CGPoints. Returns a CGFloat containing the distance between them,
    // calculated with Pythagoras' theorem.
    //√(x^2+y^2)
    func pointDistance(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {
        return CGFloat(
            sqrtf(powf(Float(p2.x - p1.x), 2) + powf(Float(p2.y - p1.y), 2)))
    }

    现在运行可以用手指轻触屏幕下方可以移动发射器了(当然在屏幕下方的0.2部分),并且移动速度也还不错,但在移动的过程中这个发射器什么都不会做,我们可以给它添加一些动作,翻转什么的:

    func moveToward(_ location: CGPoint)
        {
            removeAction(forKey: "movement")
            
            let distance = pointDistance(position, location)
            let screenWidth = UIScreen.main.bounds.size.width
            let duration = TimeInterval(2 * distance/screenWidth) //转换为时间间隔专用的单位,例如:毫秒
            run(SKAction.move(to: location, duration: duration),
                withKey:"movement")                               //duration:指定移动过程需要的时间,可以自己指定
            
            
            let wobbleTime = 0.3
            let halfWobbleTime = wobbleTime/2
            let wobbling = SKAction.sequence([
                SKAction.scaleX(to: 0.2, duration: halfWobbleTime),
                SKAction.scaleX(to: 1.0, duration: halfWobbleTime)
                ])//接收一个action队列(数组)
            let wobbleCount = Int(duration/wobbleTime)
            //当duration大于wobbleTime时才会大于1,才会执行,所以当距离比较近的时候是不会旋转的,这个可以自由发挥
            run(SKAction.repeat(wobbling, count: wobbleCount), withKey: "wobbling")
        
        }

    现在运行的效果就比较好看一点,现在改添加一些敌人了,创建一个父类为SKNode,命名为:EnemyNode,并添加以下代码:

    import SpriteKit
    
    class EnemyNode: SKNode {
            override init() {
                super.init()
                name = "Enemy (self)"
                initNodeGraph()
            }
            required init?(coder aDecoder: NSCoder) {
                super.init(coder: aDecoder)
            }
            private func initNodeGraph() {
                let topRow = SKLabelNode(fontNamed: "Courier-Bold")
                topRow.fontColor = SKColor.brown
                topRow.fontSize = 20
                topRow.text = "x x"
                topRow.position = CGPoint(x: -20, y: 15)//为了不显示在屏幕所以设定-20
                addChild(topRow)
                
                let middleRow = SKLabelNode(fontNamed: "Courier-Bold")
                middleRow.fontColor = SKColor.brown
                middleRow.fontSize = 20
           middleRow.position = CGPoint(x: -20, y: 0)
    middleRow.text = "x" addChild(middleRow) let bottomRow = SKLabelNode(fontNamed: "Courier-Bold") bottomRow.fontColor = SKColor.brown bottomRow.fontSize = 20 bottomRow.text = "x x" bottomRow.position = CGPoint(x: -20, y: -15) addChild(bottomRow)
           //三个SKLabelNode构成一个敌人
            }
    }

    跟刚才一样在GameScene.swift加载该结点,并为这个结点设置一个函数随机生成x,y坐标来来生成敌人:

    在addChild(playerNode)后面添加

    spawnEnemies()//随机生成敌人

    addChild(enemies)

    private func spawnEnemies() {                      
            let count = Int(log(Float(levelNumber))) + levelNumber
            for _ in 0..<count {
                let enemy = EnemyNode()
                let size = frame.size;
                let x = arc4random_uniform(UInt32(size.width * 0.8))
                    + UInt32(size.width * 0.1)                       //随机生成x坐标,范围0.1屏幕宽度~0.8屏幕宽度
                let y = arc4random_uniform(UInt32(size.height * 0.5))
                    + UInt32(size.height * 0.5)                      //随机生成y坐标,范围0.5屏幕高度~0.5屏幕高度
                enemy.position = CGPoint(x: CGFloat(x), y: CGFloat(y))
                enemies.addChild(enemy)
            }
        }

    发射器有了,敌人也有了,现在该弄子弹了,创建一个BulletNode继承于SKNode:

    //
    //  BulletNode.swift
    //  otherGame
    //
    //  Created by 陈金伙 on 2017/4/8.
    //  Copyright © 2017年 cjh. All rights reserved.
    //
    
    import SpriteKit
    class BulletNode: SKNode {
        var thrust:CGVector = CGVector(dx: 0, dy: 0)
        
        override init() {
            super.init()
            let dot = SKLabelNode(fontNamed: "Courier")
            dot.fontColor = SKColor.black
            dot.fontSize = 40
            dot.text = "."
            addChild(dot)
            let body = SKPhysicsBody(circleOfRadius: 1)
            body.isDynamic = true
            body.categoryBitMask = PlayerMissileCategory //用于定义物理主体所属的类别
            body.contactTestBitMask = EnemyCategory      //一个掩码,定义哪些类别的物体引起与这个物理体的交集通
            body.collisionBitMask = EnemyCategory        //定义哪些类别的物理机构可以与这个物理体碰撞
            body.fieldBitMask = GravityFieldCategory     //定义哪些类别的物理领域可以施加力量在这个物理机构
            body.mass = 0.01                             //以千克为单位的物体
            physicsBody = body
            name = "Bullet (self)"
            
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            let dx = aDecoder.decodeFloat(forKey: "thrustX")
            let dy = aDecoder.decodeFloat(forKey: "thrustY")
            thrust = CGVector(dx: CGFloat(dx), dy: CGFloat(dy))
        }
        
        override func encode(with aCoder: NSCoder) {
            super.encode(with: aCoder)
            aCoder.encode(Float(thrust.dx), forKey: "thrustX")
            aCoder.encode(Float(thrust.dy), forKey: "thrustY")
        }
        
        class func bullet(from start: CGPoint, toward destination: CGPoint) -> BulletNode {
            let bullet = BulletNode()
            bullet.position = start
            let movement = vectorBetweenPoints(start, destination)      //差的向量
            let magnitude = vectorLength(movement)                      //两点之间的距离
            let scaledMovement = vectorMultiply(movement, 1/magnitude)  //缩放向量
            let thrustMagnitude = CGFloat(100.0)
            bullet.thrust = vectorMultiply(scaledMovement, thrustMagnitude)//扩大向量,无论屏幕多大都能发射到
            bullet.run(SKAction.playSoundFileNamed("shoot.wav",
                                                   waitForCompletion: false))
            return bulle
        }
        
        func applyRecurringForce() {
            physicsBody!.applyForce(thrust)                             //对物理体的重心施加力量,如果没有这个函数,发射的子弹会向下滑
        }
    
    
    }

    同理的向GameScene.swift添加

    private let playerBullets = SKNode()

    addChild(playerbullets)

    并在

    touchesBegan()中添加else(默认不移动发射器就是发射子弹)

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            for touch in touches {
                let location = touch.location(in: self)
                if location.y < frame.height * 0.2 {
                    let target = CGPoint(x: location.x, y: playerNode.position.y)
                    playerNode.moveToward(target) //移动到手指当前位置
                }
                else {                            //没有移动发射器就默认发射子弹
                    let bullet = BulletNode.bullet(from: playerNode.position, toward: location)//从发射器当前位置发射到手指的位置
                    playerBullets.addChild(bullet)
                }
            }
        }

    现在我们考虑当子弹飞出屏幕时可以让它消失(从内存中撤销)在GameScene的update()添加:

    override func update(_ currentTime: TimeInterval) {
            updatebullets()
        }
        private func updatebullets() {
            var bulletsToRemove:[BulletNode] = []
            for bullet in playerBullets.children as! [BulletNode] {
                
                if !frame.contains(bullet.position) {
                    // 当子弹离开屏幕时放入一个数组
                    bulletsToRemove.append(bullet)
                    continue
                }
                // 对物理体的重心施加力量
                bullet.applyRecurringForce()
            }
            playerBullets.removeChildren(in: bulletsToRemove)
        }

     现在的子弹遇到敌人并没有攻击性,因为我们只给BulletNode指定物理性质,现在该轮到PlayerNode,EnemyNode了,选择EnemyNode,并添加以下代码:

     private func initPhysicsBody() {
                let body = SKPhysicsBody(rectangleOf: CGSize( 40, height: 40))
                body.affectedByGravity = false
                body.categoryBitMask = EnemyCategory
                body.contactTestBitMask = PlayerCategory | EnemyCategory  //发射器和子弹可以与之碰撞
                body.mass = 0.2           //本身重量
                body.angularDamping = 0   //阻力
                body.linearDamping = 0
                body.fieldBitMask = 0
                physicsBody = body
            }

    并在init()里添加刚才我们加的initPhysicsBody(),同理,在PlayerNode添加以下代码:

     private func initPhysicsBody() {
            let body = SKPhysicsBody(rectangleOf: CGSize( 20, height: 20))
            body.affectedByGravity = false
            body.categoryBitMask = PlayerCategory
            body.contactTestBitMask = EnemyCategory
            body.collisionBitMask = 0
            body.fieldBitMask = 0
            physicsBody = body
    }

    并在init()添加initPhysicsBody(),现在可以运行试试看效果,当你把屏幕唯一的敌人打掉之后,你就应该想到下一步该设定升级了,当我们把敌人打出屏幕时,也应该像子弹那样从内存中移除,在GameScene添加以下代码:

    private func updateEnemies() {
            var enemiesToRemove:[EnemyNode] = []
            for node in enemies.children as! [EnemyNode] {
                if !frame.contains(node.position) {
                    enemiesToRemove.append(node)
                    continue
                }
            }
            enemies.removeChildren(in: enemiesToRemove)
        }

    并更新update()函数的内容:

     override func update(_ currentTime: TimeInterval) {
            if finished {
                return
            }
            updatebullets()
            updateEnemies()
            checkForNextlevel()
        }
        private func checkForNextlevel() {  //查看是否还有敌人存活
            if enemies.children.isEmpty {
                goToNextLevel()
            }
        }
        
        private func goToNextLevel() {     //进入下一级
            finished = true
            
            let label = SKLabelNode(fontNamed: "Courier")
            label.text = "Level Complete!"
            label.fontColor = SKColor.blue
            label.fontSize = 32
            label.position = CGPoint(x: frame.size.width * 0.5, y: frame.size.height * 0.5)
            addChild(label)
            
            let nextLevel = GameScene(size: frame.size, levelNumber: levelNumber + 1) //等级不断增加
            nextLevel.playerLives = playerLives                       //生命值不变
            view!.presentScene(nextLevel, transition: SKTransition.flipHorizontal(withDuration: 1.0))
        }

    这个游戏里的每个结点都是模拟现实物理的,所以我们还要考虑当我们用子弹打到第一个敌人时,敌人会被打飞,被打飞的过程中或许会碰撞到另一个敌人又或许会随重力下滑,掉落到发射器上,即玩家生命值减一操作,这就需要委托了,因为委托事件里面有contact事件对当前阶段很好用,添加

    class GameScene: SKScene, SKPhysicsContactDelegate {

    并在init(size:CGSize, levelNumber:Int) { 添加

    physicsWorld.gravity = CGVector(dx: 0, dy: -1) //设置重力向下

    physicsWorld.contactDelegate = self

    这边我们可以先想想碰撞的特效xcode提供自带的文件,创建SpriteKit partical file命名MissleExplosion,并在inspector属性进行调整,这边是我的(随便调的,自由发挥):

    同理再创建一个命名为EnemyExplosion,并在inspector属性进行调整(自由发挥)

    选择GameScene添加以下代码:

    func didBegin(_ contact: SKPhysicsContact) {
            if contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask {
                //一样的种类
                let nodeA = contact.bodyA.node!
                let nodeB = contact.bodyB.node!
            }
            else {
                var attacker: SKNode
                var attackee: SKNode
                
                if contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask {//种类的大小,下面有给图说明
                    // A attack B
                    attacker = contact.bodyA.node!
                    attackee = contact.bodyB.node!
                    
                }
                else {
                    //B attack A
                    attacker = contact.bodyB.node!
                    attackee = contact.bodyA.node!
                }
                if attackee is PlayerNode {
                    playerLives -= 1
                }
                
                //What do we do with the attacker and the attackee?
                attackee.receiveAttacker(attacker, contact: contact)//扩展类的方法,下面有给
                playerBullets.removeChildren(in: [attacker])
                enemies.removeChildren(in: [attacker])
                
            }
        }

    四种大小分别代表不同的种类,在我们给他们的physicBody初始化时就有给他们指定,接下来扩展SKnode类,为什么要扩展SKNode?,因为在SpriteKit每个对象都是一个结点,所以扩展SKNode,可以对敌人,发射器,子弹都好操作,新建一个swift file命名SKNode+Extra并添加以下代码:

    import SpriteKit
    
    extension SKNode {
        func receiveAttacker(_ attacker: SKNode, contact: SKPhysicsContact)
        {
            // Default implementation does nothing
            
            physicsBody!.affectedByGravity = true
            let force = vectorMultiply(attacker.physicsBody!.velocity, contact.collisionImpulse)
            
            let myContact = scene!.convert(contact.contactPoint, to: self)
            physicsBody!.applyForce(force, at: myContact)
            
            let path  = Bundle.main.path(forResource: "MissileExplosion", ofType: "sks")
            
            let explosion = NSKeyedUnarchiver.unarchiveObject(withFile: path!)
                as! SKEmitterNode
            explosion.numParticlesToEmit = 20           //默认为0,无限粒子,这边指定20颗粒子
            explosion.position = contact.contactPoint  //在子弹击中的部位出现粒子
            scene!.addChild(explosion)
        }
        
        func friendlyBumpFrom(_ node: SKNode) {
            // Default implementation does nothing
            physicsBody!.affectedByGravity = true
            
        }
    }

    现在运行你会发现一切都良好,就是尽管敌人掉落到发射器上,玩家的血是没有扣的,我记得明明有添加

    if attackee is PlayerNode {

                    playerLives -= 1

                }

     可是不起作用,其实是有起作用的,不信你可以调试下在后台输出playerLives,只是没有实时更新到界面,

        private var playerLives: Int {

            didSet {

                let lives = childNode(withName: "LivesLabel") as! SKLabelNode

                lives.text = "Lives: (playerLives)"

            }

        }

    更改私有属性变成属性观察者,一旦playerlives有变化就执行didSet里面的代码,现在可以了,但是生命值会一直减,没有尽头的,就像是无敌模式,是时候给这个游戏来个收尾了,

    创建cocoa touch class命名GameOverScene,并以SKScene为父类,添加以下代码:

    import SpriteKit
    
    class GameOverScene: SKScene {
        override init(size: CGSize) {
            super.init(size: size)
            backgroundColor = SKColor.purple
            let text = SKLabelNode(fontNamed: "Courier")
            text.text = "Game Over"
            text.fontColor = SKColor.white
            text.fontSize = 50
            text.position = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
            addChild(text)
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    }

    这就是结束界面,在GameScene里面来实现它:

    private func triggerGameOve() {
            finished = true
            
            let path = Bundle.main.path(forResource:"EnemyExplosion",
                                        ofType: "sks")
            let explosion = NSKeyedUnarchiver.unarchiveObject(withFile: path!)
                as! SKEmitterNode
            explosion.numParticlesToEmit = 200      //当生命值为0时,爆炸变的更大
            explosion.position = playerNode.position
            scene!.addChild(explosion)
            playerNode.removeFromParent()
            
            let transition = SKTransition.doorsOpenVertical(withDuration: 1)
            let gameOver = GameOverScene(size: frame.size)
            view!.presentScene(gameOver, transition: transition)
        }
        
        private func checkForGame() -> Bool { //添加到update(),实时监测
            if playerLives == 0 {
                triggerGameOve()
                return true
            }
            return false
        }
    
        override func update(_ currentTime: TimeInterval) {
            if finished {
                return
            }
            updatebullets()
            updateEnemies()
            if (!checkForGame()) {
                checkForNextlevel()
            }
            
        }

    现在基本可以完了,要是想要美观的话可以搞个开始界面,这边就不搞了。

     要下载全部的代码请到我的github库:https://github.com/TypeInfos/SpriteKit-Game

  • 相关阅读:
    git .gitignore re-include
    excel 排名次
    ssh agent and ssh add for git Permission denied
    Git 仓库 清理 瘦身
    EF Core ThenInclude 2.0自动完成提示有误,坑了一下
    Entity Framework Core 导航属性 加载数据
    .net core mvc 模型绑定 之 json and urlencoded
    HttpClientHelper
    提示错误:“应为“providerInvariantName”参数的非空字符串。”
    关于.NET WebAPI 常见的跨域问题 解决清单
  • 原文地址:https://www.cnblogs.com/doudoublog/p/6679514.html
Copyright © 2020-2023  润新知