I am building a ship game using swift. The objective is to avoid the incoming stones and score as many points as you can as the level increases. The stones come in opposite direction to hit the ship.But I am unable to detect collisions between ship and stone, stone passes through the ship.The ship can move to the left or to the right.
I used rect1.interects(rect2) for intersection.
Thank You.
here is ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var moveWater: MovingWater!
var boat:UIImageView!
var stone:UIImageView!
var boatLeftRight : UILongPressGestureRecognizer!
var tapTimer:Timer!
var leftM:UInt32 = 55
var rightM:UInt32 = 250
var leftS:UInt32 = 35
var rightS:UInt32 = 220
func startGame() {
boat = UIImageView(image: UIImage(named: "boat"))
boat.frame = CGRect(x: 0, y: 0, width: 60, height: 90)
boat.frame.origin.y = self.view.bounds.height - boat.frame.size.height - 10
boat.center.x = self.view.bounds.midX
self.view.insertSubview(boat, aboveSubview: moveWater)
boatLeftRight = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.leftRight(tap:)))
boatLeftRight.minimumPressDuration = 0.001
moveWater.addGestureRecognizer(boatLeftRight)
}
func leftRight(tap:UILongPressGestureRecognizer) {
if tap.state == UIGestureRecognizerState.ended {
if (tapTimer != nil) {
self.tapTimer.invalidate()
}
} else if tap.state == UIGestureRecognizerState.began {
let touch = tap.location(in: moveWater)
if touch.x > moveWater.frame.midX {
tapTimer = Timer.scheduledTimer(timeInterval: TimeInterval(0.005), target: self, selector: #selector(ViewController.moveBoat(time:)), userInfo: "right", repeats: true)
} else {
tapTimer = Timer.scheduledTimer(timeInterval: TimeInterval(0.005), target: self, selector: #selector(ViewController.moveBoat(time:)), userInfo: "left", repeats: true)
}
}
}
func moveBoat(time:Timer) {
if let d = time.userInfo as? String! {
var bot2 = boat.frame
if d == "right" {
if bot2.origin.x < CGFloat(rightM) {
bot2.origin.x += 2
}
} else {
if bot2.origin.x > CGFloat(leftM) {
bot2.origin.x -= 2
}
}
boat.frame = bot2
}
}
func movingStone() {
stone = UIImageView(image: UIImage(named: "stones.png"))
var stone2 = leftS + arc4random() % rightS
stone.bounds = CGRect(x:10, y:10, width:81.0, height:124.0)
stone.contentMode = .center;
stone.layer.position = CGPoint(x: Int(stone2), y: 10)
stone.transform = CGAffineTransform(rotationAngle: 3.142)
self.view.insertSubview(stone, aboveSubview: moveWater)
UIView.animate(withDuration: 5, delay: 0, options: UIViewAnimationOptions.curveLinear, animations: { () -> Void in
self.stone.frame.origin.y = self.view.bounds.height + self.stone.frame.height + 10
}) { (success:Bool) -> Void in
self.stone.removeFromSuperview()
self.movingStone()
}
}
func update() {
if(boat.bounds.intersects(stone.bounds)) {
boat.image = //set new image
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
moveWater.backgroundStart()
startGame()
movingStone()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I saw your own answer. This is basic collision detection indeed. You should still have a look at SpriteKit. I see that you are using a Timer, which is not the best way to go since Timers will give you performance issues in the long run. It's a common mistake when you start game developement.
Maybe you think that you can ensure a frame rate by setting a very fast timer. The thing is that timers are not consistent and they have low priority too. This means that your timer call will be delayed if something more important happens in the background. If your movement is based on that forced refresh rate, it will become choppy very quickly. Also the code you run in the timer might get faster or slower depending on the logic you are using.
SpriteKit provides you with an update function that is run every frame and that let's you know about the current system time at every frame. By keeping track of that value, you can then calculate how much time went between two frames and you can then scale your movement accordingly to compensate for the time difference between two frames.
On top of that SpriteKit offers you a bunch of options for collision detection and movement. It integrates a very well made Physics Engine and collision detection system. It will also do collision detection on complex shapes, apply forces to the bodies, etc.
I strongly suggest you follow the link to Ray Wenderlich's website given to you in the answer above. If you have the budget, you might also want to buy their book on how to make 2D games for Apple devices. I've read that cover to cover and I can say I love it. I can now do my own stuff with SpriteKit and it's also a very good starter for newcomers to swift.
Any reason you choose UIKit to make your game?
If you make a game you should really be using SpriteKit instead of UIKit.
Check google and youtube for SpriteKit tutorials, there is loads.
A really good start is this one that teaches you the basics of the SpriteKit Scene editor and how to do collisions etc.
https://www.raywenderlich.com/118225/introduction-sprite-kit-scene-editor
I recommend that you do not continue like this.
Hope this helps
I fixed this myself. It was easy and without any SpriteKit, I have detected collisions.
func intersectsAt(tap2 : Timer) {
var f1 : CGRect!
var f2 : CGRect!
f1 = boat.layer.presentation()?.frame
f2 = stone.layer.presentation()?.frame
if(f1.intersects(f2)) {
stopGame()
}
}
Related
I have the following demo app, In which I set an ARWorldTrackingConfiguration over my RealityKit.
I also use plane detection.
When a plane is detected, I add the ability to "Fire" a rectangle on to the plane with a simple square collision box.
After about 100 squares, the app thermalStatus changes to serious and my frame rate goes down to 30fps.
For the life of me, I can't understand why 100 simple shapes in an RealityKit world, with no special textures or even collision events will cause this.
Does anyone have any idea?
PS1: Running this on an iPhone XS, which should be able to perform better according to HW specifications.
PS2: Adding the code below
import UIKit
import RealityKit
import ARKit
let material = SimpleMaterial(color: .systemPink, isMetallic: false)
var sphere: MeshResource = MeshResource.generatePlane(width: 0.1, depth: 0.1)
var box = ShapeResource.generateBox(width: 0.1, height: 0.03, depth: 0.1)
var ballEntity = ModelEntity(mesh: sphere, materials: [material])
let collider = CollisionComponent(
shapes: [box],
mode: .trigger
)
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.vertical]
configuration.worldAlignment = .camera
// Add the box anchor to the scene
configuration.frameSemantics.remove(.bodyDetection)
configuration.frameSemantics.remove(.personSegmentation)
configuration.frameSemantics.remove(.personSegmentationWithDepth)
arView.renderOptions.insert(.disableCameraGrain)
arView.renderOptions.insert(.disableGroundingShadows)
arView.renderOptions.insert(.disableHDR)
arView.renderOptions.insert(.disableMotionBlur)
arView.renderOptions.insert(.disableFaceOcclusions)
arView.renderOptions.insert(.disableDepthOfField)
arView.renderOptions.insert(.disablePersonOcclusion)
configuration.planeDetection = [.vertical, .horizontal]
arView.debugOptions = [.showAnchorGeometry, .showStatistics]
let gesture = UITapGestureRecognizer(target: self,
action: #selector(self.tap(_:)))
arView.addGestureRecognizer(gesture)
arView.session.run(configuration, options: [ .resetSceneReconstruction ])
}
#objc func tap(_ sender: UITapGestureRecognizer) {
let point: CGPoint = sender.location(in: arView)
guard let query = arView.makeRaycastQuery(from: point,
allowing: .existingPlaneGeometry,
alignment: .vertical) else {
return
}
let result = arView.session.raycast(query)
guard let raycastResult = result.first else { return }
let anchor = AnchorEntity(raycastResult: raycastResult)
var ballEntity = ModelEntity(mesh: sphere, materials: [material])
ballEntity.collision = collider
anchor.addChild(ballEntity)
arView.scene.anchors.append(anchor)
}
#IBAction func removePlaneDebugging(_ sender: Any) {
if arView.debugOptions.contains(.showAnchorGeometry) {
arView.debugOptions.remove(.showAnchorGeometry)
button.setTitle("Display planes", for: .normal)
return
}
button.setTitle("Remove planes", for: .normal)
arView.debugOptions.insert(.showAnchorGeometry)
}
}
Can anyone please assist?
When you use ARKit or RealityKit, a thermal condition of your iPhone doesn't entirely depend on a realtime rendering of 100 primitives. The point is a 6-DOF world tracking running at 60 fps. Plane Detection is another hard core feature for your device. So, World Tracking and Plane Detection ops are extremely CPU/GPU intensive (as well as Image/Object Detection or People Occlusion). They also quickly drain your battery.
Another heavy burden for your CPU/GPU are shadows and reflective metallic shaders. Realtime 60 fps soft shadows are additional task for any device (even with A12 processor) and metallic textures that use environment reflectons are calculated on neural engines.
PLEASE HELP!!! I have been trying to figure this out for along time. I have searched the internet and i cannot find anything that will help me.
I am currently making a game in which you are a space ship in the middle and enemy ships are moving towards you and you have to shoot them. some enemies have different lives. for example: a red ship takes one shot to explode, the blue ship takes 3, etc. I have everything to work only the lives. for example: whenever a blue ship is called on to the screen i shoot it once so its life goes down to 2. but whenever a another blue ship is called the first blue ship has its life reset back to 3 again. Is there anyway I can make it so that whenever a ship looses lives it remains that way even if other ships are called ?
this is my ship function that gets called and adds enemy space ships onto the screen:
func VillainRight(){
let TooMuch = self.size.width
let point = UInt32(TooMuch)
life = 3
let VillainR = SKSpriteNode(imageNamed: "BlueVillain")
VillainR.zPosition = 2
VillainR.position = CGPoint(x: self.frame.minX,y: CGFloat(arc4random_uniform(point)))
//This code makes the villain's Zposition point towards the SpaceShip
let angle = atan2(SpaceShip.position.y - VillainR.position.y, SpaceShip.position.x - VillainR.position.x)
VillainR.zRotation = angle - CGFloat(M_PI_2)
let MoveToCenter = SKAction.move(to: CGPoint(x: self.frame.midX, y: self.frame.midY), duration: 15)
//Physics World
VillainR.physicsBody = SKPhysicsBody(rectangleOf: VillainR.size)
VillainR.physicsBody?.categoryBitMask = NumberingPhysics.RightV
VillainR.physicsBody?.contactTestBitMask = NumberingPhysics.Laser | NumberingPhysics.SpaceShip
VillainR.physicsBody?.affectedByGravity = false
VillainR.physicsBody?.isDynamic = true
VillainR.run(MoveToCenter)
addChild(VillainR)
}
This is the code that calls this function:
_ = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(Level1.VillainRight), userInfo: nil, repeats: true)
I am using the spritekit in Swift.
Thank You Very Much in advance!
That is happening because life variable is declared as a property of a scene and it is not local to a specific node (enemy ship). You can solve this in a few ways... First way would be using node's userData property:
import SpriteKit
let kEnergyKey = "kEnergyKey"
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
let blueShip = getShip(energy: 3)
let greenShip = getShip(energy: 2)
let redShip = getShip(energy: 1)
if let blueShipEnergy = blueShip.userData?.value(forKey: kEnergyKey) as? Int {
print("Blue ship has \(blueShipEnergy) lives left")
//hit the ship
blueShip.userData?.setValue(blueShipEnergy-1, forKey: kEnergyKey)
if let energyAfterBeingHit = blueShip.userData?.value(forKey: kEnergyKey) as? Int {
print("Blue ship has \(energyAfterBeingHit) lives left")
}
}
}
func getShip(energy:Int)->SKSpriteNode{
//determine which texture to load here based on energy value
let ship = SKSpriteNode(color: .purple, size: CGSize(width: 50, height: 50))
ship.userData = [kEnergyKey:energy]
return ship
}
}
This is what the docs say about userData property:
You use this property to store your own data in a node. For example,
you might store game-specific data about each node to use inside your
game logic. This can be a useful alternative to creating your own node
subclasses to hold game data.
As you can see, an alternative to this is subclassing of a node (SKSpriteNode):
class Enemy:SKSpriteNode {
private var energy:Int
//do initialization here
}
I'm building my first game in Swift and I wanted to know how to go about handling multiple on screen sprites at once. My game pushes sprites on to screen with addChild continuously, so there are many active at once. I realized that I didn't have a proper way of simultaneously affecting all of them- like if I wanted to affect a physics property of all enemy sprites at once. So far I created an empty array var enemySprites = [enemyType1]() at the begining of GameScene and have been appending the sprite instances to it instead of using addChild to draw them directly to the scene. However, I'm not able to simply loop through and draw them to screen with:
for enemy in enemySprites{
addChild(enemy)
}
this bit of code is in the override func update(currentTime: CFTimeInterval) function, so maybe I'm just misplacing it? Any help on how to go about this would be great!
Sam,
Here's some sample code to update enemies when your lives reach 0:
First, we set a property observer on the lives property so we can call a function when you lose all lives:
var lives = 3 {
didSet {
if lives == 0 {
updateEnemies()
}
}
And then a function to enumerate over all the enemies and change each one's velocity to (0, 0):
func update enemies() {
enumerateChildNodesWithName("type1") {
node, stop in
let enemy = node as! SKSpriteNode
enemy.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
}
}
Instead of use update method, you could use a timer. From sources:
public class func scheduledTimerWithTimeInterval(ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
So if you follow Apple guide, it will be for example:
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("spawnAlien:"), userInfo: myParameter, repeats: true)
func spawnAlien(timer : NSTimer) {
if let myUserInfo = timer.userInfo {
print(myUserInfo) // a parameters passed to help you to the alien creation
}
timer.invalidate()
}
BUT according to Whirlwind I agree with him and with LearnCocos2d work, sprite-kit don't work well with timers (as explained in the link by LearnCocos2d) and the better way, especially as you say you develop your first game, it's to use SKAction, a combination of actions to achieve the similar behavior obtained by NSTimer.
I've think about a function or an extension, let me know if it's work as expected:
extension SKAction {
class func scheduledTimerWithTimeInterval(time:NSTimeInterval, selector: Selector, repeats:Bool)->SKAction {
let call = SKAction.customActionWithDuration(0.0) { node, _ in
node.performSelector(selector)
}
let wait = SKAction.waitForDuration(time)
let seq = SKAction.sequence([wait,call])
let callSelector = repeats ? SKAction.repeatActionForever(seq) : seq
return callSelector
}
}
Usage:
let spawn = SKAction.scheduledTimerWithTimeInterval(time, selector: #selector(GenericArea.spawnAlien), repeats: true)
self.runAction(spawn,withKey: "spawnAlien")
In my game, there's a class for a "wall" that's moving to the left. I want to change the speed of it based on count i that I added to touchesBegan method:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
}
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 1 )
let move = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 0.5)
if(count <= 10)
{
runAction(SKAction.repeatActionForever(moveLeft))
}
else
{
runAction(SKAction.repeatActionForever(move))
}
}
but it's not working. Can you help?
As I said there are a lot of changes which have to be done:
First let's change MLWall class and add a duration property which will be used in startMoving method:
var duration:NSTimeInterval = 0.5
Then still inside MLWall class change the init method:
init(duration: NSTimeInterval) {
self.duration = duration
//...
}
And change startMoving method to use this passed parameter:
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: self.duration)
runAction(SKAction.repeatActionForever(moveLeft))
}
Those are changes inside Wall class. Now let's make some changes in WallGenerator class:
First WallGenerator class should be aware of how fast walls should go. So we are adding property to store that info:
var currentDuration: NSTimeInterval = 1 // I named it duration, because SKAction takes duration as a parameter, but this actually affects on speed of a wall.
After that the first method which has to be changed is startGeneratingWallsEvery(second:) into startGeneratingWallsEvery(second: duration:
//duration parameter added
func startGeneratingWallsEvery(seconds: NSTimeInterval, duration : NSTimeInterval) {
self.currentDuration = duration
generationTimer = NSTimer.scheduledTimerWithTimeInterval(seconds, target: self, selector: "generateWall", userInfo: nil, repeats: true)
}
Here, we are making a WallGenerator aware of desired wall's speed.
And the next method which has to be changed in order to use that speed is:
//duration parameter added
func generateWall() {
//...
//Duration parameter added
let wall = MLWall(duration: self.currentDuration)
//...
}
And there is a GameScene left. There, I've added a tapCounter property:
let debugLabel = SKLabelNode(fontNamed: "Arial") //I've also added a debug label to track taps count visually
var tapCounter = 0
Here is how you can initialize label if you want to see number of tap counts:
//Setup debug label
debugLabel.text = "Tap counter : \(tapCounter)"
debugLabel.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMaxY(frame)-50.0)
debugLabel.fontColor = SKColor.purpleColor()
self.addChild(debugLabel)
First I've changed the start method:
func start() {
//...
// adding duration parameter in method call
wallGenerator.startGeneratingWallsEvery(1,duration: 1)
}
The important part is : wallGenerator.startGeneratingWallsEvery(1,duration: 1) which says start generating walls every second with one second duration(which affects on node's speed).
Next, I've modified touchesBegan of the scene into this:
if isGameOver {
restart()
} else if !isStarted {
start()
} else {
tapCounter++
debugLabel.text = "Tap counter : \(tapCounter)"
if(tapCounter > 10){
wallGenerator.stopGenerating()
wallGenerator.startGeneratingWallsEvery(0.5, duration:0.5)
}
hero.flip()
}
Then, changed restart() method in order to restart the counter when game ends:
func restart() {
tapCounter = 0
//...
}
And that's pretty much it. I guess I haven't forgot something, but at my side it works as it should. Also, note that using NSTimer like from this GitHub project is not what you want in SpriteKit. That is because NSTimer don't respect scene's , view's or node's paused state. That means it will continue with spawning walls even if you think that game is paused. SKAction would be a preferred replacement for this situation.
Hope this helps, and if you have further questions, feel free to ask, but I guess that you can understand what's happening from the code above. Basically what is done is that WallGenerator has become aware of how fast their wall nodes should go, and Wall node has become aware of how fast it should go...
EDIT:
There is another way of changing walls speed by running an moving action with key. Then, at the moment of spawning, based on tapCounter value, you can access an moving action by the key, and directly change actions's speed property...This is probably a shorter way, but still requires some changes (passing a duration parameter inside Wall class and implementing tapCounter inside scene).
Try doing it like so :
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
startMoving()
}
func startMoving() {
removeAllActions()
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: count <= 10 ? 1.0 : 0.5)
runAction(SKAction.repeatActionForever(moveLeft))
}
As of I read comments below your question, you made a really unclear question, but nevertheless you have idealogical problems in your code.
Your touchesBegan method is implemented in the wall if I understood everything right, so it has no effect on newly generated walls. You have to move that logic to the scene and then spawn new walls with speed as a parameter, or at least make count a class var, so every wall can access that, but you still has to handle your touches in the scene, because now touch is handled when user taps directly in the wall.
So I declared a UILabel! named "textLabel" which has a gravity effect which goes up. Now, When it reachs -30, I want the label to reappear at (200, 444). I used a NSTimer to check if (labelText > -30) but when it reaches/passes that point, it just flashes where it's suppose to appear (around the middle) but it does not start from the middle. here is most of my code.
How do I make the Label appear at it's new position? so it can just cycle again once it reaches that -30 on the y-axis?? I've searched and searched. HELPPPPPPP
#IBOutlet weak var textLabel: UILabel!
var gravity: UIGravityBehavior!
var animator: UIDynamicAnimator!
var itemBehavior: UIDynamicItemBehavior!
var boundaryTimer = NSTimer()
Override func viewDidLoad() {
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [textLabel])
animator.addBehavior(gravity)
boundaryTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "leftBoundary", userInfo: nil, repeats: true)
itemBehavior = UIDynamicItemBehavior(items: [textLabel])
itemBehavior.elasticity = 1.2
gravity.gravityDirection = CGVectorMake(0.0 , -0.01)
animator.addBehavior(itemBehavior)
}
func leftBoundary() {
if textLabel.center.y < 40 {
self.textLabel.center = CGPointMake(200, 444)
}
}
Rather than setting the center, you can add and then remove an attachment behavior, e.g.:
func leftBoundary() {
if textLabel.center.y < 40 {
let attachment = UIAttachmentBehavior(item: textLabel, attachedToAnchor: textLabel.center)
animator.addBehavior(attachment)
attachment.anchorPoint = view.center
attachment.action = {[unowned self, attachment] in
if self.textLabel.center.y > 100 {
self.animator.removeBehavior(attachment)
}
}
}
}
Note, you might want to change the itemBehavior so that allowRotation is false (depending upon your desired UI).
Also note, this action that I use here is something you could use with your gravity behavior, too. So, rather than a timer (which might not always catch the label at the same time), use an action, which is called upon every frame of the animation.
You should first remove all label behaviors from the animator.
This has to do with how UIKit Dynamics works. It really only manipulates the layer / presentation layer of your view, so you need to reset everything if you want to set the position yourself (via the view, which also affects the layer).
Also, I think the setup with the timer is not very efficient. Rather, you should give KVO (key-value observing) a try. You can observe the key path "center.y" and react if it passes your limit.