以一定速度在屏幕上随机移动物体

Posted

技术标签:

【中文标题】以一定速度在屏幕上随机移动物体【英文标题】:Moving objects random across the screen with a certain speed 【发布时间】:2017-05-25 08:48:11 【问题描述】:

我需要让我的对象在屏幕上随机移动。当它们移动时,该对象正在寻找一定半径范围内的另一个对象。

我找到了下面的链接,并实现了 getDuration 函数。但我和主题所有者有同样的故障。我可以看到它应该可以通过删除正在运行的操作来修复。

Moving an object across the screen at a certain speed.(Sprite Kit)

视频:https://www.youtube.com/watch?v=jHE5RC-mvwU

但我现在尝试了几种解决方案,但我无法使其工作。当我终止动作时,我的对象会停止移动。

谁能告诉我,在哪里杀死我的动作,在 moveToWaypoint 中创建?我需要我的对象移动到一个随机的航点,但如果一个特定的对象进入半径范围内,那么它应该为壁橱对象设置一个新的航点并再次开始操作。

代码:

///Called in update method
func move(scene: GameplayScene) 
    checkForWaypoint()
    checkForFood(scene: scene)
    moveToWaypoint()


///Creates a new waypoint
func createWaypoint() 
     waypoint = CGPoint(x: randomBetweenNumbers(firstNum: minX, secondNum: maxX), y: randomBetweenNumbers(firstNum: minX, secondNum: maxX))


override func checkForFood(scene: GameplayScene) 
    var closesObject: SKNode? = nil

    scene.enumerateChildNodes(withName: "Food") 
        node, _ in
        let distance = node.position.distanceFromCGPoint(point: self.position)

        if distance < self.FOOD_RANGE  
            closesObject = node
        
    

    if hungryState == HungryState.hungry 
        if closesObject != nil 
            waypoint = closesObject?.position
            moveState = MoveState.movingToFood
         else 
            if moveState == MoveState.movingToFood 
                createWaypoint()
            
        
    


///Moves the object to the waypoint
func moveToWaypoint () 
    let action = SKAction.move(to: waypoint!, duration: getDuration(pointA: position, pointB: waypoint!, speed: 25))
    self.run(action)


///Calcuate a speed between to cordinates
private func getDuration(pointA:CGPoint,pointB:CGPoint,speed:CGFloat)-> TimeInterval 
    let xDist = (pointB.x - pointA.x)
    let yDist = (pointB.y - pointA.y)
    let distance = sqrt((xDist * xDist) + (yDist * yDist));
    let duration : TimeInterval = TimeInterval(distance/speed)
    return duration

编辑:

Gamescene 类的更新函数

override func update(_ currentTime: TimeInterval) 
    moveFish()


private func moveFish() 
    for node in self.children 
        if node.name != nil 
            switch node.name 
            case "Fish"?:
                let fishToMove = node as! Fish

                fishToMove.move(scene: self)

            default:
                break
            
        
    

【问题讨论】:

你能说明如何从场景的 update() 方法中调用 move() 方法吗?我只是感兴趣它是严格调用每一帧还是有一些自定义逻辑?如果答案是“是的,我每帧都调用 move 方法......因此我每帧都创建 SKAction”,那将是你的第一个错误。您将要实现的目标应该很容易。也可以使用物理接触检测来完成。但这只是我:) 它是在场景中的update()方法中调用的,是的。你能给我一些细节,如何解决这个@Whirlwind?这是我的第一个 SpriteKit 应用,所以我还是新手 :) 我只问了几个问题,你没有回答 :) 尝试仔细阅读我提出的问题,以便继续调试。 我刚刚更新了@Whirlwind 的问题。我添加了更新函数和 moveFish() - 它调用了 move() 函数。 好吧,我个人只会在需要时执行操作,而不是每次都执行。关于错误本身......实际问题是什么? (抱歉,我目前没有时间阅读其他问题)。 【参考方案1】:

您在此处尝试解决的问题可以通过两种方式完成。首先是使用物理,其次当然是没有它。我决定不使用物理,因为这显然是你的做法。

简而言之,这些示例中的鱼在没有食物的情况下使用SKAction 移动。当食物在范围内时,使用 update: 方法将鱼移动到它们的目标。当鱼吃掉它时,它会继续使用SKAction 移动。

此外,在此之前,我从 *** 借用了一些有用的扩展,将来您可能会发现它们很有用:

import SpriteKit
import GameplayKit

//Extension borrowed from here : https://***.com/a/40810305
extension ClosedRange where Bound : FloatingPoint 
    public func random() -> Bound 
        let range = self.upperBound - self.lowerBound
        let randomValue = (Bound(arc4random_uniform(UINT32_MAX)) / Bound(UINT32_MAX)) * range + self.lowerBound
        return randomValue
    

//Extension borrowed from here : https://***.com/a/37760551
extension CGRect 
    func randomPoint() -> CGPoint 
        let origin = self.origin
        return CGPoint(x:CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x,
                       y:CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y)
    


//Extension borrowed from here:  https://***.com/a/33292919

extension CGPoint 
    func distance(point: CGPoint) -> CGFloat 
        return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
    

现在有一个像你这样的 Fish 类,它的方法很少,还有一个物理体,仅用于检测食物和鱼之间的接触,但这一切都来自物理。这是 Collider 结构,以防您想知道我是如何定义它的:

struct Collider
            static let food : UInt32 = 0x1 << 0
            static let fish : UInt32 = 0x1 << 1
            static let wall : UInt32 = 0x1 << 2
        

现在回到 Fish 类...我已将 cmets 放入代码中,所以我想不需要解释这些方法的作用。代码如下:

class Fish:SKSpriteNode
    private let kMovingAroundKey = "movingAround"
    private let kFishSpeed:CGFloat = 4.5
    private var swimmingSpeed:CGFloat = 100.0
    private let sensorRadius:CGFloat = 100.0
    private weak var food:SKSpriteNode! = nil //the food node that this fish currently chase

    override init(texture: SKTexture?, color: UIColor, size: CGSize) 
        super.init(texture: texture, color: color, size: size)

        physicsBody = SKPhysicsBody(rectangleOf: size)
        physicsBody?.affectedByGravity = false
        physicsBody?.categoryBitMask = Collider.fish
        physicsBody?.contactTestBitMask = Collider.food
        physicsBody?.collisionBitMask = 0x0 //No collisions with fish, only contact detection
        name = "fish"

        let sensor = SKShapeNode(circleOfRadius: 100)
        sensor.fillColor = .red
        sensor.zPosition = -1
        sensor.alpha = 0.1
        addChild(sensor)
    

    func getDistanceFromFood()->CGFloat? 


        if let food = self.food 

            return self.position.distance(point: food.position)
        
        return nil

    

    func lock(food:SKSpriteNode)

        //We are chasing a food node at the moment
        if let currentDistanceFromFood = self.getDistanceFromFood() 

            if (currentDistanceFromFood > self.position.distance(point: food.position))
                //chase the closer food node
                 self.food = food
                self.stopMovingAround()
            //else, continue chasing the last locked food node

        //We are not chasing the food node at the moment
        else
             //go and chase then
             if food.position.distance(point: self.position) <= self.sensorRadius 

                self.food = food
                self.stopMovingAround()
            
        
    

    //Helper method. Not used currently. You can use this method to prevent chasing another (say closer) food while already chasing one
    func isChasing(food:SKSpriteNode)->Bool

        if self.food != nil 

            if self.food == food 
                return true
            
        

        return false
    

    func stopMovingAround()

        if self.action(forKey: kMovingAroundKey) != nil
           removeAction(forKey: kMovingAroundKey)
        
    


    //MARK: Chasing the food
    //This method is called many times in a second
    func chase(within rect:CGRect)

        guard let food = self.food else 

            if action(forKey: kMovingAroundKey) == nil 
                self.moveAround(within: rect)
            
            return
        

        //Check if food is in the water
        if rect.contains(food.frame.origin) 

            //Take a detailed look in my *** answer of how chasing works : https://***.com/a/36235426

            let dx = food.position.x - self.position.x
            let dy = food.position.y - self.position.y

            let angle = atan2(dy, dx)

            let vx = cos(angle) * kFishSpeed
            let vy = sin(angle) * kFishSpeed

            position.x += vx
            position.y += vy

        
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

    func moveAround(within rect:CGRect)

        if scene != nil 

            //Go randomly around the screen within view bounds
            let point = rect.randomPoint()

            //Formula: time = distance / speed
            let duration = TimeInterval(point.distance(point: position) / self.swimmingSpeed)
            let move = SKAction.move(to: point, duration: duration)
            let block = SKAction.run 
                [unowned self] in

                self.moveAround(within: rect)
            
            let loop = SKAction.sequence([move,block])

            run(loop, withKey: kMovingAroundKey)
        
    

所以基本上,有一些方法可以在鱼不追逐食物的时候四处移动。还有一种方法可以停止这个(无限)动作(SKAction)。最重要的方法是chase(within rect:) 方法。该方法在场景的 update() 方法中调用,并定义鱼将(尝试)追逐食物的方式和时间。

现在是游戏场景:

//MARK: GameScene
class GameScene: SKScene, SKPhysicsContactDelegate 

    private var nodesForRemoval:[SKNode] = []
    private var water = SKSpriteNode()

    override func didMove(to view: SKView) 

        physicsWorld.contactDelegate = self
        physicsWorld.gravity = CGVector(dx: 0.0, dy: -0.5)
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
        physicsBody?.categoryBitMask = Collider.wall
        physicsBody?.contactTestBitMask = 0x0
        physicsBody?.collisionBitMask = Collider.fish | Collider.food
        self.backgroundColor = .white

        //Water setup
        water = SKSpriteNode(color: .blue, size: CGSize(width: frame.width, height: frame.height - 150))
        water.position = CGPoint(x: 0, y: -75)
        water.alpha = 0.3
        addChild(water)
        water.zPosition = 4

        //Fish one
        let fish = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
        addChild(fish)
        fish.position = CGPoint(x: frame.midX-50, y: frame.minY + 100)
        fish.zPosition = 5

        fish.moveAround(within: water.frame)

        //Fish two
        let fish2 = Fish(texture: nil, color: .black, size:CGSize(width: 20, height: 20))
        addChild(fish2)
        fish2.position = CGPoint(x: frame.midX+50, y: frame.minY + 100)
        fish2.zPosition = 5

        fish2.moveAround(within: water.frame)

    


    func feed(at position:CGPoint, with food:SKSpriteNode)

        food.position = CGPoint(x: position.x, y: frame.size.height/2 - food.frame.size.height)
        addChild(food)
    

    //MARK: Food factory :)
    func getFood()->SKSpriteNode

        let food = SKSpriteNode(color: .purple, size: CGSize(width: 10, height: 10))

        food.physicsBody = SKPhysicsBody(rectangleOf: food.frame.size)
        food.physicsBody?.affectedByGravity = true
        food.physicsBody?.categoryBitMask = Collider.food
        food.physicsBody?.contactTestBitMask =  Collider.fish
        food.physicsBody?.collisionBitMask = Collider.wall
        food.physicsBody?.linearDamping = (0.1 ... 0.95).random()
        food.name = "food"
        return food
    

    //MARK: Feeding
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) 

        if let touch = touches.first 

            let location = touch.location(in: self)

            let food = getFood()
            feed(at: location, with: food)
        
    

    //MARK: Eating
    func didBegin(_ contact: SKPhysicsContact) 


        guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else 

            //Silliness like removing a node from a node tree before physics simulation is done will trigger this error
            fatalError("Physics body without its node detected!")
        

        let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

        switch mask 

           //Contact between fish and a food
          case Collider.fish | Collider.food:

            if let food = (contact.bodyA.categoryBitMask == Collider.food ? nodeA : nodeB) as? SKSpriteNode
            
                self.nodesForRemoval.append(food)
            

        default:
            //some unknown contact occurred
            break
        
    

    //MARK: Removing unneeded nodes
    override func didSimulatePhysics() 

        for node in self.nodesForRemoval 
            node.removeFromParent()
        
        self.nodesForRemoval.removeAll()
    

    //MARK: Chasing the food
    override func update(_ currentTime: TimeInterval) 

        self.enumerateChildNodes(withName: "fish") 
            [unowned self] node, stop in

            if let fish = node as? Fish 

                self.enumerateChildNodes(withName: "food") 
                    node, stop in

                    fish.lock(food: node as! SKSpriteNode)
                

                fish.chase(within: self.water.frame)
            
        
    

就是这样。在这里,我们设置我们的节点,解决接触检测并告诉哪些鱼应该追逐哪个食物节点。我离开了 cmets,所以一切都在那里。我希望这些方法几乎是不言自明的,但当然您可以向我询问有关工作原理的详细信息。

这里有一段简短的视频介绍了它的工作原理:

以及更长的版本,因为我只能上传两个 2 兆字节:screen recording

基本上,如果食物节点不在其定义的范围内,鱼就不会追逐它。尽管如此,鱼仍会追逐锁定的节点,直到它吃掉它。但是如果有一些其他的食物更接近,鱼就会追逐那个食物节点。当然这不是必须的,你可以随意调整(检查isChasing:)方法。

【讨论】:

谢谢!很好的答案。 @Grumme 欢迎您!请注意,swimmingSpeedsensorRadius 之类的内容可能会通过 Fish 类的自定义初始化程序传递。这样你就可以让每条鱼都与众不同。 同样在GameViewControllerviewDidLoad() 中,当从sks 文件加载场景时,您应该设置scene.size = view.bounds.size 并设置aspectFill 的缩放模式,以便此代码像视频中一样工作. 我刚刚实施并开始测试您的解决方案@Whirlwind,但我的鱼似乎加速了?当半径内有食物时,鱼会得到“提升”。请看这个视频:giphy.com/gifs/3o7btUkxKM96u9iVk4鱼应该保持匀速:)。 @Grumme 当然,设置正确的速度取决于您。 :) 您可能需要设置其他内容。我只是向你展示了你可以走哪条路,实现随机移动+追逐目标。

以上是关于以一定速度在屏幕上随机移动物体的主要内容,如果未能解决你的问题,请参考以下文章

corona sdk / solar2d 多个移动物体

以一定速度在屏幕上移动对象。(Sprite Kit)

用opengl随机移动小行星

OpenGL - 对象在随机方向移动

Unity中C#如何实现物体在场景中随机移动

随机游走模型(RandomWalk Mobility)