开场白:
话说这段时间对于用SpriteKit游戏突然产生浓厚兴趣,这不利用空闲时间完成了一个简易版的坦克大战。
额,因为gif图转换太慢了,后期在补上,先上传几张图片看看效果哈! 开始前 开战中
开战中 选择难易程度看到效果图,如果感觉还行,有必要继续看的童鞋继续哈,下面针对比较重要且有难点的地方讲解一下了。
重点一:游戏方向控制盘,具体实现可看下游戏方向盘实现
个人比较喜欢王者荣耀中的操作盘,就按照它的样子模仿吧,但是由于缺少素材,最终出来的效果起码还算及格哈。
这个是游戏手柄部分代码
/**
游戏控制手柄
*/
class MyTankControlHandler: UIView {
///内容显示
var contentView:UIView?
///内部小圈圈
var circelView:MyTankMidCircleView?
///最外层的方向箭头
var directionImageView:UIImageView?
///是否开始触发
var isBeginMove:Bool = false
///是否在中间
var isStandInMiddle:Bool = false
///移动的比例,0-1,越高则移动的越快
var moveRatio:CGFloat = 0
///方向值
var direction:CGFloat = 0
///定时器,用来监听执行isBeginMove
var displayLinkTimer:CADisplayLink? = nil
weak var delegate : MyTankControlHandlerDelegate?
class func gameHandler ()->MyTankControlHandler {
return MyTankControlHandler.init(frame: CGRect(x: 0, y: 0, width: 170, height: 170))
}
来分析一下思路:
1、我们的操作盘用来做什么,最终目的只有一个:将当前方向值告诉外界,外界只要成为代理即可获取方向值。
2、不管是点按还是长按,都会触发,而且还得考虑到手势的位置,假设操作手柄圆圈移动的最大距离为100,那么移动的速度是否根距离有关呢,再者,当手势点在中心多少范围内算不算不移动呢,当移动点距离中心大于等于100的时候,速度达到最快。
3、方向值的确认:这个得好好温习下数学的三角函数的知识了,
//atan 和 atan2 都是求反正切函数,如:有两个点 point(x1,y1), 和 point(x2,y2);
//那么这两个点形成的斜率的角度计算方法分别是:
//float angle = atan( (y2-y1)/(x2-x1) );
//或
//float angle = atan2( y2-y1, x2-x1 );
let angle = atan2(position.y-midY, position.x-midX)
在拖动的时候和touchBegin的时候更新方向值
4、如果比较省事又方便的将方向值进行回传呢: 这里我一开始也走了挺多弯路,因为在拖动手势和touchBegin在统计长按上代码上比较多且又麻烦。最终使用CADisplayLink
定时器,在定时器方法中,只要触发了方向盘就一直回调即可,
@objc func moveUpdateAction () {
///触发了方向盘
if (self.isBeginMove == false){
return
}
///是否当前在中间范围内,则考虑用户当前是否不动
if self.isStandInMiddle == true {
return
}
if self.delegate != nil {
self.delegate?.controlHandlerMove(handler: self, directionValue: self.direction,moveRatio:self.moveRatio)
}
}
5、中间的圆圈怎样才能保证随着手势动起来:
在拖动的时候,根据得到的position,然后判断是否超出边界给予新的position,赋值给圆圈即可:
//当前拖动的位置,在拖动手势回调中
let position = gester.location(in: self.contentView)
//根据中心点、圆的大小、当前拖动位置得到新的位置赋值给圆圈
let resultPoint = self.isCircleIn(center: CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5), rect: (self.contentView?.bounds)!, point: position)
具体的判断方法:得到新的position并赋值给圆圈即可
///判断当前的位置是否超出一定范围内
func isCircleIn(center:CGPoint,rect:CGRect,point:CGPoint) ->CGPoint {
//就是要算出点到圆心的距离是否大于半径
var distance:Float = 0;
var resultPoint:CGPoint = point
var moveRatio :Float = 0
//大圆半径
let radius = Float(rect.size.width*0.5)
//小圆半径
let circleRadicu = Float((self.circelView?.bounds.size.width)!*0.5)
//半径减掉内圆的半径才是最长的移动距离
let calculateRadius = radius - circleRadicu
if(point.x == center.x && point.y == center.y){
distance = 0
}else if(point.y == center.y){
distance = fabsf(Float(center.x - point.x))
if(distance >= calculateRadius) {
if(center.x > point.x){
resultPoint = CGPoint(x: CGFloat(circleRadicu), y: resultPoint.y)
}else{
resultPoint = CGPoint(x: rect.size.width-CGFloat(circleRadicu), y: resultPoint.y)
}
moveRatio = 1
}else{
moveRatio = distance / calculateRadius
}
}else if(point.x == center.x){
distance = fabsf(Float(center.y - point.y))
if(distance >= calculateRadius){
if(center.y > point.y){
resultPoint = CGPoint(x: resultPoint.x, y: CGFloat(circleRadicu))
}else{
resultPoint = CGPoint(x: resultPoint.x, y: rect.size.height-CGFloat(circleRadicu))
}
moveRatio = 1
}else{
moveRatio = distance / calculateRadius
}
}else{
let xValue = fabsf(Float(center.x - point.x))
let yValue = fabsf(Float(center.y - point.y))
distance = hypotf(xValue, yValue)
if(distance >= calculateRadius){
moveRatio = 1
let angle = atan2(point.y-center.y, point.x-center.x)
let sinYValue = sin(angle)*CGFloat(calculateRadius)//正弦,就是y值方向
let cosXValue = cos(angle)*CGFloat(calculateRadius)//余弦,就是x值方式
//print("angle:\(angle) , 垂直:\(yValue),水平:\(xValue)")
//当在右上角方向时,xValue为正 yValue为负
if(cosXValue > 0 && sinYValue < 0){
let fabsY = CGFloat(fabsf(Float(sinYValue)))
resultPoint = CGPoint(x: cosXValue + CGFloat(radius), y: CGFloat(radius) - fabsY)
}else if(xValue > 0 && yValue > 0){//当在右下角方向时,xValue 和 yValue 都为正
resultPoint = CGPoint(x: cosXValue + CGFloat(radius), y: sinYValue + CGFloat(radius))
}else if(cosXValue < 0 && sinYValue > 0){//当在左下角方向时,xValue为负 yValue为正
let fabsX = CGFloat(fabsf(Float(cosXValue)))
resultPoint = CGPoint(x: CGFloat(radius) - fabsX, y: sinYValue + CGFloat(radius))
}else if(cosXValue < 0 && sinYValue < 0){//当在左上角方向时,xValue 和 yValue 都为负
let fabsY = CGFloat(fabsf(Float(sinYValue)))
let fabsX = CGFloat(fabsf(Float(cosXValue)))
resultPoint = CGPoint(x: CGFloat(radius) - fabsX, y: CGFloat(radius) - fabsY)
}
}else{
moveRatio = distance / calculateRadius
}
}
self.moveRatio = CGFloat(moveRatio)
return resultPoint
}
重点二:游戏中的坐标系和我们正常app中的坐标系不一样
坐标系.JPG而且角度也是反的,就比如正常下90度的方向应该是垂直往下,但是在游戏中是垂直往上的,切记哈。
重点三:坦克精灵 class TankSpriteNode: SKSpriteNode
1、坦克根据当前的方向发射炮弹
首先如果创造炮弹:先看一下代码
func createBullte (isEnmy:Bool) ->SKSpriteNode {
let w:CGFloat = 5
let bullte = SKSpriteNode.init(texture: nil, size: CGSize(width: w, height: w))
bullte.name = "bullte"
bullte.physicsBody = SKPhysicsBody.init(circleOfRadius: w*0.5, center: CGPoint(x: 0.5, y: 0.5))
bullte.physicsBody?.categoryBitMask = isEnmy ? bullteEnmyCategory : bullteCategory
if(isEnmy == true){
bullte.physicsBody?.contactTestBitMask = tankCategory | frontierCatetory | bullteCategory
bullte.physicsBody?.collisionBitMask = tankCategory//可以和哪些体发送碰撞
}else{
bullte.physicsBody?.contactTestBitMask = tankEnmyCategory | frontierCatetory | bullteEnmyCategory
}
bullte.physicsBody?.allowsRotation = false
bullte.physicsBody?.isDynamic = true
let shape = SKShapeNode.init(rectOf: CGSize.init(width: w, height: w), cornerRadius: w * 0.5)
shape.position = CGPoint(x: 0.5, y: 0.5)
shape.fillColor = isEnmy ? SKColor.black:SKColor.red
bullte.addChild(shape)
return bullte
}
各种物理体的标识
///本身子弹
let bullteCategory:UInt32 = 0x1 << 4
///敌方子弹
let bullteEnmyCategory:UInt32 = 0x1 << 5
///本身坦克
let tankCategory:UInt32 = 0x1 << 6
///敌方坦克
let tankEnmyCategory:UInt32 = 0x1 << 7
///边界
let frontierCatetory:UInt32 = 0x1 << 8
针对几个比较重要的参数讲解一下
categoryBitMask
就是可以理解给当前的物理体做标记,假设是bullteCategory
contactTestBitMask
在物理场景中可以与哪些其他物体发生联系,假设设置为tankEnmyCategory,那么在场景碰撞后,才会有回调。
collisionBitMask
表示可以与哪些物理体发生碰撞,所以敌方坦克就不能与敌方坦克发生碰撞了,直接可以穿透了哈。
isDynamic
,字面意思表示是否是动态的,这个字段我参透的不够彻底,只是知道设置后有什么效果,这里就不解释了,看官方解释哈:The default value is true. If the value is false, the physics body ignores all forces and impulses applied to it. This property is ignored on edge-based bodies; they are automatically static.
第二步:炮弹发射的操作
///下面注释都有哈
///发射子弹
func sendBullte(isEnmy:Bool) {
let bullte = self.createBullte(isEnmy: isEnmy)
if self.scene == nil {
return
}
///子弹需要从坦克的头部开出,所以子弹相对应坦克的位置也是会变的
var bullteXDistance:CGFloat = 0
var bullteYDistance:CGFloat = 0
self.scene?.addChild(bullte)
///就是默认往上的推力为0.25,可以自己设置,越大速度越快
let defaultValue:CGFloat = 0.25
var xValue:CGFloat = 0
var YValue:CGFloat = 0
if (self.direction == 0){
xValue = defaultValue
bullteXDistance = 20
}else if(self.direction == CGFloat(Double.pi/2)){
YValue = -defaultValue
bullteYDistance = -20
}else if(self.direction == CGFloat(Double.pi) || self.direction == -CGFloat(Double.pi)){
xValue = -defaultValue
bullteXDistance = -20
}else if(self.direction == -CGFloat(Double.pi/2)){
YValue = defaultValue
bullteYDistance = 20
}else{
//假设斜边为1
YValue = -sin(self.direction)*defaultValue//正弦,就是y值方向
xValue = cos(self.direction)*defaultValue//余弦,就是x值方式
bullteYDistance = -sin(self.direction)*20
bullteXDistance = cos(self.direction)*20
}
///相对于坦克的位置
bullte.position = CGPoint(x:self.position.x+bullteXDistance,y:self.position.y+bullteYDistance)
bullte.physicsBody?.applyImpulse(CGVector(dx: xValue, dy: YValue))
}
2、敌方坦克的走位逻辑计算
class TankEnemyTank:TankSpriteNode
继承TankSpriteNode
首先思路是这样的:敌方坦克只能垂直或水平移动,而且速度比我方坦克要慢2.5倍,然后根据难易程度设置几个固定的位置,在将当前我发坦克位置进行计算得到x轴与y轴之间的距离的合,就是坦克移动的总距离了,然后按照每一帧(1/60*2.5)的距离进行计算时间,等执行完之后就通过定时器进行回调,在定时器到达之后如果坦克没有被消灭,则继续轮回重新操作一遍,以下是代码实现部分:
///移动到目的地,就是冲着对方坦克位置去攻击
func moveToDesination(position:CGPoint)->Void {
///首先计算自己的位置与目标位置的最长距离,因为只能自动坦克只能沿着x轴或y轴直线行驶
let selfPosition = self.position
let xMargin = fabsf(Float(selfPosition.x - position.x))
let yMargin = fabsf(Float(selfPosition.y - position.y))
let totalMargin = xMargin + yMargin
let totalDuration = TimeInterval(2.5/60.0*totalMargin)
var resultDuration:TimeInterval = totalDuration
///组装动画集合,方便一起执行
var actions:Array<SKAction> = []
///先移动到和目标x方向一致,为了让它们不至于每次都沿着一个方向,给个随机
let suijiValue = arc4random_uniform(2)
if suijiValue == 0 {
if(selfPosition.x > position.x){
if(self.direction != CGFloat(Double.pi)){
self.direction = CGFloat(Double.pi)
}
}else{
if(self.direction != 0){
self.direction = 0
}
}
let firstDirection = self.direction
///坦克第一次转向
let firstAction = SKAction.rotate(toAngle: -firstDirection, duration: 0.25)
resultDuration += 0.25
actions.append(firstAction)
///移动到第一个点
let firstPosition = CGPoint(x: position.x, y: selfPosition.y)
let firstDuration = totalDuration * TimeInterval(xMargin/totalMargin)
let secondAction = SKAction.move(to: firstPosition, duration: firstDuration)
actions.append(secondAction)
var secondDirection:CGFloat = 0
if(selfPosition.y > position.y){
secondDirection = CGFloat(Double.pi/2)
}else{
secondDirection = -CGFloat(Double.pi/2)
}
///坦克第二次转向
let thirdAction = SKAction.rotate(toAngle: -secondDirection, duration: 0.25)
resultDuration += 0.25
actions.append(thirdAction)
let changDirectionAction = SKAction.run {
self.direction = -secondDirection
}
actions.append(thirdAction)
actions.append(changDirectionAction)
///坦克第二次移动
let fourAction = SKAction.move(to: position, duration: totalDuration-firstDuration)
actions.append(fourAction)
}
let resultAction = SKAction.sequence(actions)
self.run(resultAction)
if(self.timer != nil){
self.timer?.invalidate()
self.timer = nil
}
let timer = Timer.scheduledTimer(withTimeInterval: resultDuration, repeats: false) { (timer:Timer) in
self.moveArriveAction()
}
self.timer = timer
针对游戏难易程度专门设置了一个管理者,并且管理着敌方坦克的重建和初始位置,懒得介绍哈,直接上代码:
/**游戏难易程度管理者*/
class MyTankHardSolver: NSObject ,TankEnemyTankDelegate{
class share {
static let manager = MyTankHardSolver()
}
///难度等级,默认为1级
var hardGrade:Int = 1
//坦克基数,每次创建几个
var baseCount:Int = 2
///设置了一个最大分值,根据难易程度来定
var maxScore:Int = 0
///专门存放坦克
var automaticTankArray:Array<TankEnemyTank> = []
///持有场景对象
weak var gameScene:MyTankeScene!
///总分
var score:Int = 0
func gameStart() {
self.score = 0
self.automaticTankArray.removeAll()
startGameWithDestinationPostion(position: (self.gameScene.myTank?.position)!)
}
func gameOverAction () {
for tank in self.automaticTankArray {
tank.running = false
tank.removeAllActions()
}
}
///初始化操作
func startGameWithDestinationPostion(position:CGPoint) {
let positionList = getTankPositonDataArrayWithGrade()
for value:NSValue in positionList {
let texture = SKTexture(imageNamed: "mytank_tank")
let enmy = TankEnemyTank.init(texture: texture, color: UIColor.clear, size: CGSize(width: tankWidth, height: tankHeight))
let showPosition = value.cgPointValue
enmy.position = showPosition
enmy.beginPosition = showPosition
self.gameScene.addChild(enmy)
enmy.moveToDesination(position: position)
automaticTankArray.append(enmy)
enmy.delegate = self
}
}
/**根据难易程度随便定了几个位置*/
func getTankPositonDataArrayWithGrade () ->Array<NSValue> {
var list:Array<NSValue> = []
let width = self.gameScene.size.width
let height = self.gameScene.size.height
if(width == 0 || height == 0){
return list
}
let firstPoint = CGPoint(x: 50, y: height-tankHeight-44)
let point1Value = NSValue.init(cgPoint: firstPoint)
let secondPoint = CGPoint(x: width-tankWidth, y: 50)
let point2Value = NSValue.init(cgPoint: secondPoint)
list.append(point1Value)
list.append(point2Value)
self.maxScore = 20
if self.hardGrade == 2 {
let thirdPoint = CGPoint(x: 50, y: 50)
let point3Value = NSValue.init(cgPoint: thirdPoint)
let fourPoint = CGPoint(x: width-tankWidth, y: height-tankHeight-44)
let point4Value = NSValue.init(cgPoint: fourPoint)
list.append(point3Value)
list.append(point4Value)
self.maxScore = 40
self.baseCount = 4
}else if (self.hardGrade >= 3){
let thirdPoint = CGPoint(x: 50, y: 50)
let point3Value = NSValue.init(cgPoint: thirdPoint)
let fourPoint = CGPoint(x: width-tankWidth, y: height-tankHeight-44)
let point4Value = NSValue.init(cgPoint: fourPoint)
list.append(point3Value)
list.append(point4Value)
let fivePoint = CGPoint(x: (width-tankWidth)*0.5, y: 50)
let sixPoint = CGPoint(x: (width-tankWidth)*0.5, y: height - 50-44)
let point5Value = NSValue.init(cgPoint: fivePoint)
let point6Value = NSValue.init(cgPoint: sixPoint)
list.append(point5Value)
list.append(point6Value)
self.maxScore = 60
self.baseCount = 6
}
return list
}
//MARK:TankEnemyTankDelegate
func tankEnemyArrive(tank: TankEnemyTank) {
tank.moveToDesination(position: (self.gameScene.myTank?.position)!)
}
//被击中
func tankEnemyKilled(tank: TankEnemyTank) {
self.score += 1
let brokenCount = self.maxScore - self.baseCount
///游戏通关
if(self.score >= self.maxScore){
return
}
///则不需要再创造坦克了
if(self.score > brokenCount){
return
}
for (index,value) in self.automaticTankArray.enumerated() {
if value == tank {
self.automaticTankArray.remove(at: index)
//重新再弄一个出来
let texture = SKTexture(imageNamed: "mytank_tank")
let enmy = TankEnemyTank.init(texture: texture, color: UIColor.clear, size: CGSize(width: tankWidth, height: tankHeight))
let orginPostion = tank.beginPosition!
var showPosition = tank.beginPosition!
///这样的目的是不至于每次都在同一个位置出现
var suijiXValue = CGFloat(arc4random_uniform(40))
var suijiYValue = CGFloat(arc4random_uniform(40))
let boolValue = arc4random_uniform(2)
if(boolValue == 0){
suijiXValue = -CGFloat(suijiXValue)
suijiYValue = -CGFloat(suijiYValue)
}
showPosition = CGPoint(x: showPosition.x+suijiXValue, y: showPosition.y+suijiYValue)
enmy.position = showPosition
enmy.beginPosition = orginPostion
enmy.isHidden = true
self.gameScene.addChild(enmy)
let action = SKAction.wait(forDuration: 2.0)
enmy.run(action)
enmy.isHidden = false
enmy.moveToDesination(position: (self.gameScene.myTank?.position)!)
automaticTankArray.append(enmy)
enmy.delegate = self
break
}
}
}
}
网友评论