概述

SpriteKit制作瓦片地图游戏,深入了解2D游戏制作过程

详细

说实话这个2D游戏实战的入门看的我脑浆子都沸腾了, 好多新的概念涌入, 没做过游戏开发的我表示真的难以接受, 吸收效率与之前相比也下降好多, 不过越往后学, 就能够加深对之前知识的掌握, 这可能也是看书的好处吧, 今天我也把对瓦片地图的一些学习经验记录下来供大家探讨.

说实话, 我很推荐Ray家的资源, 由浅入深手把手的教学, 内容前后呼应, 看几本书就能涵盖国内4个月培训班的课程体系. 遵循本系列一贯的风格, 我们还是从基础的API开始看起, 对API掌握熟练的话, 多敲两个Demo就能够基本的上手任何项目了.

一、瓦片地图技术要点

1、SKTileMapNode

@available(iOS 10.0, *)
open class SKTileMapNode : SKNode, NSCopying, NSCoding
public init(tileSet: SKTileSet, columns: Int, rows: Int, tileSize: CGSize)
open var numberOfColumns: Int
open var numberOfRows: Int
open var tileSize: CGSize
open var mapSize: CGSize { get }
open var tileSet: SKTileSet
open var colorBlendFactor: CGFloat
open func tileDefinition(atColumn column: Int, row: Int) -> SKTileDefinition?
open func tileGroup(atColumn column: Int, row: Int) -> SKTileGroup?
open func setTileGroup(_ tileGroup: SKTileGroup?, forColumn column: Int, row: Int)
open func tileColumnIndex(fromPosition position: CGPoint) -> Int
open func tileRowIndex(fromPosition position: CGPoint) -> Int
open func centerOfTile(atColumn column: Int, row: Int) -> CGPoint
  • init(tileSet: SKTileSet, columns: Int, rows: Int, tileSize: CGSize) 瓦片地图节点的初始化方法

  • numberOfColumns 瓦片地图的列数

  • numberOfRows 瓦片地图的行数

  • tileSize 瓦片地图中每个瓦片的尺寸

  • mapSize 瓦片地图的尺寸

  • tileSet 瓦片地图的瓦片集

  • colorBlendFactor 瓦片的渲染着色

  • tileDefinition(atColumn column: Int, row: Int) -> SKTileDefinition? 根据列数和行数返回瓦片定义

  • tileGroup(atColumn column: Int, row: Int) -> SKTileGroup? 根据列数和行数返回瓦片组

  • setTileGroup(_ tileGroup: SKTileGroup?, forColumn column: Int, row: Int) 根据列数和行数设置瓦片组

  • tileColumnIndex(fromPosition position: CGPoint) -> Int 根据瓦片位置返回瓦片在瓦片地图中列数下标

  • tileRowIndex(fromPosition position: CGPoint) -> Int 根据瓦片位置返回瓦片在瓦片地图中行数下标

2、SKTileSet

@available(iOS 10.0, *)
open class SKTileSet : NSObject, NSCopying, NSCoding
public init(tileGroups: [SKTileGroup])
open var tileGroups: [SKTileGroup]
open var name: String?
open var defaultTileGroup: SKTileGroup?
open var defaultTileSize: CGSize
open var type: SKTileSetType @available(iOS 10.0, *)
public enum SKTileSetType : UInt {
case grid
case isometric
case hexagonalFlat
case hexagonalPointy
}
  • init(tileGroups: [SKTileGroup]) 根据瓦片组初始化瓦片集

  • tileGroups 瓦片组

  • name 瓦片集的标识

  • defaultTileGroup 瓦片集默认瓦片组

  • defaultTileSize 瓦片集默认瓦片尺寸

  • type 瓦片集类型 - 网格, 等值, 六边形

3、SKTileGroup

@available(iOS 10.0, *)
open class SKTileGroup : NSObject, NSCopying, NSCoding
open class func empty() -> Self
public init(tileDefinition: SKTileDefinition)
public init(rules: [SKTileGroupRule])
open var name: String?
  • empty() 返回一个空的瓦片组

  • init(tileDefinition: SKTileDefinition) 根据瓦片定义初始化瓦片组

  • init(rules: [SKTileGroupRule]) 根据瓦片组规则初始化瓦片组

  • name 瓦片组的标识

4、SKTileGroupRule

@available(iOS 10.0, *)
open class SKTileGroupRule : NSObject, NSCopying, NSCoding
public init(adjacency: SKTileAdjacencyMask, tileDefinitions: [SKTileDefinition])
open var adjacency: SKTileAdjacencyMask
open var tileDefinitions: [SKTileDefinition]
open var name: String?
  • init(adjacency: SKTileAdjacencyMask, tileDefinitions: [SKTileDefinition]) 根据瓦片链接和瓦片定义初始化瓦片组规则

  • adjacency 瓦片链接

  • tileDefinitions 瓦片规则

  • name 瓦片组规则标识

5、SKTileDefinition

@available(iOS 10.0, *)
open class SKTileDefinition : NSObject, NSCopying, NSCoding
public init(texture: SKTexture)
public init(textures: [SKTexture], normalTextures: [SKTexture], size: CGSize, timePerFrame: CGFloat)
open var userData: NSMutableDictionary?
open var name: String?
open var size: CGSize
open var timePerFrame: CGFloat
open var rotation: SKTileDefinitionRotation
open var flipVertically: Bool
open var flipHorizontally: Bool
  • init(texture: SKTexture) 根据纹理初始化瓦片定义

  • init(textures: [SKTexture], normalTextures: [SKTexture], size: CGSize, timePerFrame: CGFloat) 根据纹理集合, 尺寸, 和帧率初始化瓦片定义

  • userData 瓦片定义的用户数据

  • name 瓦片定义的标识

  • timePerFrame 瓦片定义的帧率

  • rotation 瓦片定义的旋转规则

  • flipVertically 是否垂直翻转

  • flipHorizontally 是否水平翻转

二、程序实现

API, 了解一些基本的就够了, 如果要深究可以打开头文件逐个尝试, 我们现在就来实现一个小游戏, 这个游戏中包含了3个场景, 控制人物在规定时间内消灭所有的害虫, 我们着手进行游戏的开发吧!

1、step1 设置游戏场景的属性

class GameScene: SKScene {
var background: SKTileMapNode! //背景瓦片地图节点
var obstaclesTileMap: SKTileMapNode? //障碍物瓦片地图节点
var bugsprayTileMap: SKTileMapNode? //杀虫喷剂瓦片地图节点 var bugsNode = SKNode() //害虫的节点 var player = Player() //玩家的节点
var hud = HUD() //文字说明 var firebugCount: Int = 0 //高级害虫的节点数 var timeLimit: Int = 10 //时间限制
var elapsedTime: Int = 0 //经过时间
var startTime: Int? //开始时间 var currentLevel: Int = 1 //当前关卡等级 var gameState: GameState = .initial { //游戏状态默认为初始状态
didSet {
hud.updateGameState(from: oldValue, to: gameState) //更新游戏状态
}
}
...
}

2、step2 加载游戏场景的初始化设置

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
background =
childNode(withName: "background") as! SKTileMapNode //通过节点名读取背景瓦片地图节点
obstaclesTileMap = childNode(withName: "obstacles")
as? SKTileMapNode //通过节点名读取障碍物瓦片地图节点 if let timeLimit =
userData?.object(forKey: "timeLimit") as? Int {
self.timeLimit = timeLimit //通过节点的用户数据设置每个场景的时间限制
}
// 1
let savedGameState = aDecoder.decodeInteger(forKey: "Scene.gameState") //解档保存游戏状态
if let gameState = GameState(rawValue: savedGameState), gameState == .pause { //当解档保存游戏状态为暂停时
self.gameState = gameState //赋值游戏状态
firebugCount = aDecoder.decodeInteger(
forKey: "Scene.firebugCount") //解档高级害虫数
elapsedTime = aDecoder.decodeInteger(
forKey: "Scene.elapsedTime") //解档经过时间
currentLevel = aDecoder.decodeInteger(
forKey: "Scene.currentLevel") //解档当前关卡等级
// 2
player = childNode(withName: "Player") as! Player //根据节点名读取玩家节点
hud = camera!.childNode(withName: "HUD") as! HUD //根据节点名读取文字说明
bugsNode = childNode(withName: "Bugs")! //根据节点名读取害虫节点
bugsprayTileMap = childNode(withName: "Bugspray")
as? SKTileMapNode //通过节点名读取杀虫喷雾瓦片地图节点
} addObservers() //添加观察者
} deinit {
NotificationCenter.default.removeObserver(self) //移除观察者
}

3、step3 当场景移动到屏幕时的设置

override func didMove(to view: SKView) {
if gameState == .initial { //当游戏状态为初始状态时
addChild(player) //添加玩家到场景
setupWorldPhysics() //添加物理世界
createBugs() //添加害虫
setupObstaclePhysics() //添加障碍物
if firebugCount > 0 { //如果有高级害虫
createBugspray(quantity: firebugCount + 10) //添加杀虫喷雾
}
setupHUD() //添加文字说明
gameState = .start //设置游戏状态为开始状态
}
setupCamera() //添加摄像头
}

4、step4 物理世界的设置

func setupWorldPhysics() {
background.physicsBody =
SKPhysicsBody(edgeLoopFrom: background.frame) //设置边缘物理体
background.physicsBody?.categoryBitMask = PhysicsCategory.Edge //设置物理体标识为边缘 physicsWorld.contactDelegate = self //物理世界代理 }

5、step5 创建害虫的设置

func createBugs() {
guard let bugsMap = childNode(withName: "bugs")
as? SKTileMapNode else { return } //校验害虫瓦片地图节点
// 1
for row in 0..<bugsMap.numberOfRows { //逐行遍历害虫瓦片地图
for column in 0..<bugsMap.numberOfColumns { //逐列遍历害虫瓦片地图
// 2
guard let tile = tile(in: bugsMap,
at: (column, row))
else { continue } //校验瓦片地图中的每个瓦片
// 3
let bug: Bug
if tile.userData?.object(forKey: "firebug") != nil { //从用户数据中判断是否为高级害虫
bug = Firebug() //将害虫设置为高级害虫
firebugCount += 1 //高级害虫书自增
} else {
bug = Bug() //将害虫设置为普通害虫
}
bug.position = bugsMap.centerOfTile(atColumn: column,
row: row) //从害虫瓦片地图中读取位置并赋值
bugsNode.addChild(bug) //添加节点
bug.moveBug() //移动害虫
}
}
// 4
bugsNode.name = "Bugs" //设置害虫节点标识
addChild(bugsNode) //添加父节点到场景
// 5
bugsMap.removeFromParent() //删除害虫瓦片地图地图节点
}

6、step6 添加障碍物的设置

func setupObstaclePhysics() {
guard let obstaclesTileMap = obstaclesTileMap else { return } //校验障碍物瓦片地图节点
// 1
for row in 0..<obstaclesTileMap.numberOfRows {
for column in 0..<obstaclesTileMap.numberOfColumns {
// 2
guard let tile = tile(in: obstaclesTileMap,
at: (column, row))
else { continue }
guard tile.userData?.object(forKey: "obstacle") != nil
else { continue }
// 3
let node = SKNode() //创建节点
node.physicsBody = SKPhysicsBody(rectangleOf: tile.size) 根据瓦片尺寸创建物理体
node.physicsBody?.isDynamic = false //不进入物理世界
node.physicsBody?.friction = 0 //摩擦系数为0
node.physicsBody?.categoryBitMask =
PhysicsCategory.Breakable //设置物理体标识 node.position = obstaclesTileMap.centerOfTile(
atColumn: column, row: row)
obstaclesTileMap.addChild(node)
}
}
}

7、step7 添加杀虫喷雾的设置

func createBugspray(quantity: Int) {
// 1
let tile = SKTileDefinition(texture:
SKTexture(pixelImageNamed: "bugspray")) //创建瓦片定义
// 2
let tilerule = SKTileGroupRule(adjacency:
SKTileAdjacencyMask.adjacencyAll, tileDefinitions: [tile]) //创建瓦片组规则
// 3
let tilegroup = SKTileGroup(rules: [tilerule]) //创建瓦片组
// 4
let tileSet = SKTileSet(tileGroups: [tilegroup]) //创建瓦片集 // 5
let columns = background.numberOfColumns //读取背景瓦片地图节点的列数
let rows = background.numberOfRows //读取背景瓦片地图节点的行数
bugsprayTileMap = SKTileMapNode(tileSet: tileSet,
columns: columns,
rows: rows,
tileSize: tile.size) //创建新的瓦片地图节点
// 6
for _ in 1...quantity {
let column = Int.random(min: 0, max: columns-1) //随机列数
let row = Int.random(min: 0, max: rows-1) //随机行数
bugsprayTileMap?.setTileGroup(tilegroup,
forColumn: column, row: row) //在新额的瓦片地图节点上随机生成瓦片组
}
// 7
bugsprayTileMap?.name = "Bugspray" //设置瓦片地图节点的标识
addChild(bugsprayTileMap!) //将瓦片地图添加到场景 }

8、step8 添加摄像头设置

func setupCamera() {
guard let camera = camera, let view = view else { return } let zeroDistance = SKRange(constantValue: 0)
let playerConstraint = SKConstraint.distance(zeroDistance,
to: player) //对玩家进行约束
// 1
let xInset = min(view.bounds.width/2 * camera.xScale,
background.frame.width/2)
let yInset = min(view.bounds.height/2 * camera.yScale,
background.frame.height/2) // 2
let constraintRect = background.frame.insetBy(dx: xInset,
dy: yInset)
// 3
let xRange = SKRange(lowerLimit: constraintRect.minX,
upperLimit: constraintRect.maxX)
let yRange = SKRange(lowerLimit: constraintRect.minY,
upperLimit: constraintRect.maxY) let edgeConstraint = SKConstraint.positionX(xRange, y: yRange)
edgeConstraint.referenceNode = background
// 4
camera.constraints = [playerConstraint, edgeConstraint]
}

9、step9 获取瓦片的一些帮助方法

func tile(in tileMap: SKTileMapNode,
at coordinates: TileCoordinates)
-> SKTileDefinition? {
return tileMap.tileDefinition(atColumn: coordinates.column,
row: coordinates.row)
} func tileCoordinates(in tileMap: SKTileMapNode,
at position: CGPoint) -> TileCoordinates {
let column = tileMap.tileColumnIndex(fromPosition: position)
let row = tileMap.tileRowIndex(fromPosition: position)
return (column, row)
} func tileGroupForName(tileSet: SKTileSet, name: String)
-> SKTileGroup? {
let tileGroup = tileSet.tileGroups
.filter { $0.name == name }.first
return tileGroup
}

10、step10 点击场景的设置

override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let touch = touches.first else { return }
switch gameState {
// 1
case .start: //开始状态
gameState = .play //切换成游戏状态
isPaused = false //开始
startTime = nil
elapsedTime = 0
// 2
case .play: //游戏状态
player.move(target: touch.location(in: self)) //移动玩家
case .win: //获胜状态
transitionToScene(level: currentLevel + 1) //切换场景
case .lose: //落败状态
transitionToScene(level: 1) //切换场景
case .reload: //唤醒状态
// 1
if let touchedNode =
atPoint(touch.location(in: self)) as? SKLabelNode {
// 2
if touchedNode.name == HUDMessages.yes { //如果点击的节点是YES
isPaused = false
startTime = nil
gameState = .play
// 3
} else if touchedNode.name == HUDMessages.no { //如果点击的节点是NO
transitionToScene(level: 1)
}
}
default:
break
}
}

11、step11 切换场景的设置

func transitionToScene(level: Int) {
// 1
guard let newScene = SKScene(fileNamed: "Level\(level)")
as? GameScene else {
fatalError("Level: \(level) not found")
}
// 2
newScene.currentLevel = level
view!.presentScene(newScene,
transition: SKTransition.flipVertical(withDuration: 0.5))
}

12、step12 刷帧

override func update(_ currentTime: TimeInterval) {
if gameState != .play {
isPaused = true //如果不是游戏状态就暂停刷帧
return
} if !player.hasBugspray {
updateBugspray() //如果玩家没有杀虫喷雾, 就进行更新
}
advanceBreakableTile(locatedAt: player.position) //更新障碍物的物理体状态
updateHUD(currentTime: currentTime) //更新文字说明
checkEndGame() //检查是否达到胜负条件
}

13、step13 更新杀虫喷雾

func updateBugspray() {
guard let bugsprayTileMap = bugsprayTileMap else { return }
let (column, row) = tileCoordinates(in: bugsprayTileMap,
at: player.position)
if tile(in: bugsprayTileMap, at: (column, row)) != nil {
bugsprayTileMap.setTileGroup(nil, forColumn: column,
row: row)
player.hasBugspray = true
}
}

14、step14 更新障碍物的物理体状态

func advanceBreakableTile(locatedAt nodePosition: CGPoint) {
guard let obstaclesTileMap = obstaclesTileMap else { return }
// 1
let (column, row) = tileCoordinates(in: obstaclesTileMap,
at: nodePosition)
// 2
let obstacle = tile(in: obstaclesTileMap,
at: (column, row))
//3
guard let nextTileGroupName =
obstacle?.userData?.object(forKey: "breakable") as? String
else { return }
// 4
if let nextTileGroup =
tileGroupForName(tileSet: obstaclesTileMap.tileSet,
name: nextTileGroupName) {
obstaclesTileMap.setTileGroup(nextTileGroup,
forColumn: column, row: row) //设置新的瓦片组到瓦片地图中
}
}

15、step15 更新文字说明

func updateHUD(currentTime: TimeInterval) {
// 1
if let startTime = startTime {
// 2
elapsedTime = Int(currentTime) - startTime
} else {
// 3
startTime = Int(currentTime) - elapsedTime
}
// 4
hud.updateTimer(time: timeLimit - elapsedTime) //对文字说明进行更新
}

16、step16 检查是否达到胜负条件

func checkEndGame() {
if bugsNode.children.count == 0 { //是否消灭全部害虫
player.physicsBody?.linearDamping = 1
gameState = .win
} else if timeLimit - elapsedTime <= 0 { //是否时间用完
player.physicsBody?.linearDamping = 1
gameState = .lose
}
}

17、step17 物理世界代理的设置

extension GameScene : SKPhysicsContactDelegate {
func remove(bug: Bug) { //消灭害虫
bug.removeFromParent()
background.addChild(bug)
bug.die()
hud.updateBugCount(with: bugsNode.children.count)
} func didBegin(_ contact: SKPhysicsContact) {
let other = contact.bodyA.categoryBitMask
== PhysicsCategory.Player ?
contact.bodyB : contact.bodyA switch other.categoryBitMask {
case PhysicsCategory.Bug:
if let bug = other.node as? Bug {
remove(bug: bug) //当玩家接触到普通害虫, 消灭普通害虫
}
case PhysicsCategory.Firebug:
if player.hasBugspray {
if let firebug = other.node as? Firebug {
remove(bug: firebug)
player.hasBugspray = false //当玩家手持杀虫喷雾接触高级害虫才能消灭高级害虫
}
}
case PhysicsCategory.Breakable:
if let obstacleNode = other.node {
// 1
advanceBreakableTile(locatedAt: obstacleNode.position) //更新障碍物
// 2
obstacleNode.removeFromParent() //删除原障碍物
} default:
break
}
if let physicsBody = player.physicsBody {
if physicsBody.velocity.length() > 0 {
player.checkDirection() //进行玩家方向的设置
}
}
}
}

18、step18 观察者的设置

extension GameScene {
func applicationDidBecomeActive() {
print("* applicationDidBecomeActive")
if gameState == .pause {
gameState = .reload //重新进入, 进行游戏重载
}
} func applicationWillResignActive() {
print("* applicationWillResignActive")
isPaused = true
if gameState != .lose {
gameState = .pause //暂停游戏进程
}
} func applicationDidEnterBackground() {
print("* applicationDidEnterBackground")
if gameState != .lose {
saveGame() //进入后台保存游戏进度
}
} func addObservers() {
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidBecomeActive),
name: .UIApplicationDidBecomeActive, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationWillResignActive),
name: .UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidEnterBackground),
name: .UIApplicationDidEnterBackground, object: nil)
} }

19、step19 游戏的存储设置

extension GameScene {
func saveGame() {
// 1
let fileManager = FileManager.default
guard let directory =
fileManager.urls(for: .libraryDirectory,
in: .userDomainMask).first
else { return }
// 2
let saveURL = directory.appendingPathComponent("SavedGames")
// 3
do {
try fileManager.createDirectory(atPath: saveURL.path,
withIntermediateDirectories: true,
attributes: nil)
} catch let error as NSError {
fatalError(
"Failed to create directory: \(error.debugDescription)")
}
// 4
let fileURL = saveURL.appendingPathComponent("saved-game")
print("* Saving: \(fileURL.path)")
// 5
NSKeyedArchiver.archiveRootObject(self, toFile: fileURL.path) //文件处理器新建路径并归档
} override func encode(with aCoder: NSCoder) { /对关键属性的归档
aCoder.encode(firebugCount,
forKey: "Scene.firebugCount")
aCoder.encode(elapsedTime,
forKey: "Scene.elapsedTime")
aCoder.encode(gameState.rawValue,
forKey: "Scene.gameState")
aCoder.encode(currentLevel,
forKey: "Scene.currentLevel")
super.encode(with: aCoder)
} class func loadGame() -> SKScene? { //重新加载存储游戏进程
print("* loading game")
var scene: SKScene?
// 1
let fileManager = FileManager.default
guard let directory =
fileManager.urls(for: .libraryDirectory,
in: .userDomainMask).first
else { return nil }
// 2
let url = directory.appendingPathComponent(
"SavedGames/saved-game")
// 3
if FileManager.default.fileExists(atPath: url.path) {
scene = NSKeyedUnarchiver.unarchiveObject( //根据路径进行解档游戏进程
withFile: url.path) as? GameScene
_ = try? fileManager.removeItem(at: url)
}
return scene
} }

三、运行效果与文件截图

1、运行效果

2、文件截图

PestControl文件里的截图:

PestControl.xcodeproj文件里的截图:

四、其他补充

Notice: 忽略了一些节点的设置, 但不影响瓦片地图的理解.

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

[SpriteKit] 制作瓦片地图小游戏的更多相关文章

  1. 用C#制作推箱子小游戏

    思路分析: 一.制作一个地图 二.地图中放置墙.箱子.人.目标等 三.让小人动起来完成推箱子动作 游戏制作: 1.按照上述地图制作一个地图  (12行×13列) 地图可以看做是行和列组成的,即可以看做 ...

  2. 制作动画或小游戏——CreateJS事件(二)

    在Canvas中如果要添加事件,就需要计算坐标来模拟各种事件,而EaselJS中已经封装好了多个事件,只需调用即可. 一.事件 1)点击 事件是绑定在Shape类中的,click事件与DOM中的意思是 ...

  3. 制作一个 JavaScript 小游戏

    简评: 作者学习了编程两个月,边学边做了一个 JavaScript 小游戏,在文中总结了自己在这个过程中的一些体会,希望能给其他初学者一些帮助. 对于很多想学编程但一直没下定决心的同学来说,最大的问题 ...

  4. 制作动画或小游戏——CreateJS基础类(一)

    前面曾经记录过Canvas的基础知识<让自己也能使用Canvas>,在实际使用中,用封装好的库效率会高点. 使用成熟的库还能对基础知识有更深入的理解,CreateJS是基于HTML5开发的 ...

  5. IOS学习之路五(SpriteKit 开发飞机大战小游戏一)

    参考SpriteKit 创建游戏的教程今天自己动手做了一下,现在记录一下自己怎么做的,今天之做了第一步,一共有三个部分. 第一步,项目搭建. 项目所用图片资源:点击打开链接 1.在Xcode打开之后, ...

  6. Python制作塔防小游戏

    开发工具 Python版本:3.6.4 相关模块: pygame模块: 以及一些Python自带的模块.

  7. 原生JS制作贪吃蛇小游戏

    感情都在代码里,来,干了!... <!doctype html> <html> <head> <meta http-equiv="Content-T ...

  8. 用cocos2d 2.1制作一个过河小游戏(4): 游戏主逻辑BaseLayer设计

    前段时间一直在忙.没有时间更新博客.今天还是抽点时间把最后一小部分游戏的实现放上来吧. BaseLayer.h: #import <GameKit/GameKit.h> #import & ...

  9. 利用python 5分钟制作一款小游戏

    1.安装pygame 在命令行cmd中输入:pip install pygame ( 注:如果安装不成功,需要输入:python -m pip install --user --upgrade pip ...

随机推荐

  1. #pragma mark 添加分割线 及 其它类似标记 - 转

    #pragma marks Comments containing: MARK: TODO: FIXME: !!!: ???: 除了使用 #pragma mark -添加分割线之外, 以上几种标记均可 ...

  2. Tomcat 7 的七大新特性

    英文原文:Top 7 Features in Tomcat 7: The New and the Improved Tomcat的7引入了许多新功能,并对现有功能进行了增强.很多文章列出了Tomcat ...

  3. SharePoint 2013 升级

    原文地址:https://www.nothingbutsharepoint.com/sites/devwiki/articles/Pages/SharePoint-2013-Upgrade.aspx ...

  4. 【BZOJ】【3612】【HEOI 2014】平衡

    DP 唉我还是too naive 这是个整数划分题…… 我想的DP方式是f[i][j][k]表示前 i 个数拼出 j 用了 k 个数的方案数…… 转移当然是比较直观…… 但是只能得30分QAQ 正确的 ...

  5. Android安卓手机游戏开发

    成都传智播客Java培训,免费学Android安卓手机游戏开发,安卓android开发课程包括Android安卓应用开发和Android安卓游戏开发两个方向,可是偏向游戏开发. 依据"199 ...

  6. 第一章 Java代码执行流程

    说明:本文主要参考自<分布式Java应用:基础与实践> 1.Java代码执行流程 第一步:*.java-->*.class(编译期) 第二步:从*.class文件将其中的内容加载到内 ...

  7. Longest Common Prefix leetcode java

    题目: Write a function to find the longest common prefix string amongst an array of strings. 题解: 解题思路是 ...

  8. Butter Knife 黄油刀

    简介 Github:https://github.com/JakeWharton/butterknife  文档 特点: 采用注解的方式实现强大的View绑定和Click事件处理功能,简化代码,提升开 ...

  9. 网上收集:跟着 8 张思维导图学习 Javascript【转】

    学习的道路就是要不断的总结归纳,好记性不如烂笔头,so,下面将po出8张javascript相关的思维导图. 思维导图小tips:思维导图又叫心智图,是表达发射性思维的有效的图形思维工具 ,它简单却又 ...

  10. 新鲜出炉!9个超高分辨率的iPhone 6原型素材打包下载

    iPhone 6 出场,设计师又有得忙活了,但是新鲜的资源你们在哪里?!今天我们收集了一组精致的iPhone 6 模型素材,超高分辨率,多种视图,全都打包完毕,点一下就可以拿回家!赶紧来取吧!——   ...