I am creating a game and I want to include a page control (the three dots on the bottom of the screen - I would post a picture but I need more reputation!). This is so the user knows what page they are on while they are flicking through the scenes.
I have created my game using SpriteKit and I was wondering if there is a way to add this programtically (without the use of story boards).
In an iOS Game template Playground I was able to achieve this with the following code (first two lines are from the template, the rest is my addition):
// Load the SKScene from 'GameScene.sks'
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
let pageControl = UIPageControl()
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.numberOfPages = 3
pageControl.currentPage = 1
sceneView.addSubview(pageControl)
// Centre the control left-to-right
pageControl.centerXAnchor.constraint(equalTo: sceneView.centerXAnchor).isActive = true
// Place the control 20 points up from the bottom edge
pageControl.bottomAnchor.constraint(equalTo: sceneView.bottomAnchor, constant: -20).isActive = true
To be notified of interaction with the UIPageControl, register a handler for the event:
pageControl.addTarget(thing, action: #selector(Thing.handlePagingEvent), for: .valueChanged)
(Where thing is of type Thing - your app structure will determine which object that should logically be)
This assumes you have a single SKView in which you present one of three SKScenes. If you actually have a UIScrollView containing three SKViews, then look to UIScrollView.indicatorStyle instead.
Related
I am creating a SpriteKit game and want to be able to take advantage of textAlerts - like all the games I grew up playing. I also want them to be flexible so I can subclass them and add them throughout my game - I am not sure how to do this and so would be very grateful of any advice.
There are a number of ways that I have started to look into:
Use another SKScene
The idea here would be to add another scene immediately on top of our current one. This would have a faded out background and would be configured with the text to be displayed. This seems like the wrong approach
Use an SKNode
I could create a custom node and initialise it with my specific text. I would then disable movement and add the node at the bottom of the screen. I would then customise the actions that occur when it is tapped.
Use an SKLabel
SKLabels are designed to show text so these seem like a promising place to look. The issue is that I want to add an image into the popup view (this a headshot of the person you are talking to) and so it doesn't feel like I should be able to inject an image in.
Use something else
I don't know what this might be. Is what I am trying to do easy or harder than I think?
My problem is that I come from a swift background so am struggling to convert to a SpriteKit mindset. In Swift I would put together a custom UIView with all the UIKit components and add it to the screen. In SpriteKit we don't have the same tools so I don't know the right combinations to put together what I want.
Ideally I want to be able to configure the text popup with an array of text so that a conversation can be had with the user tapping for each new line.
Edit 1: What I have tried so far
Based on #ElTomatos comment I have experimented with adding a SKSpriteNode and an SKLabelNode on top of each other using the following code:
guard let camera = camera else { return }
let dialogueBackground = SKSpriteNode(texture: nil, color: .red, size: CGSize(width: screenSize.width / camera.xScale, height: screenSize.height / 3 / camera.xScale))
dialogueBackground.anchorPoint = CGPoint(x: 0, y: 0)
dialogueBackground.zPosition = 50
dialogueBackground.position = CGPoint(x: -screenSize.width, y: -screenSize.height)
camera.addChild(dialogueBackground)
let label = SKLabelNode(text: "Hello world")
label.zPosition = 100
label.position = CGPoint(x: -screenSize.width, y: 100 - screenSize.height)
camera.addChild(label)
As you can see the view doesn't go to the edge of the screen. Is this something to do with the safe area? It's difficult to get the width and height as the camera doesn't have a CGSize we can use.
I put some objects in AR space using ARKit and SceneKit. That works well. Now I'd like to add an additional camera (SCNCamera) that is placed elsewhere in the scene attached and positioned by a common SCNNode. It is oriented to show me the current scene from an other (fixed) perspective.
Now I'd like to show this additional SCNCamera feed on i.Ex. a SCNPlane (as the diffuse first material) - Like a TV screen. Of course I am aware that it will only display the SceneKit content which stays in the camera focus and not rest of the ARKit image (which is only possible by the main camera of course). A simple colored background then would be fine.
I have seen tutorials that describes, how to play a video file on a virtual display in ARSpace, but I need a realtime camera feed from my own current scene.
I defined this objects:
let camera = SCNCamera()
let cameraNode = SCNNode()
Then in viewDidLoad I do this:
camera.usesOrthographicProjection = true
camera.orthographicScale = 9
camera.zNear = 0
camera.zFar = 100
cameraNode.camera = camera
sceneView.scene.rootNode.addChildNode(cameraNode)
Then I call my setup function to place the virtual Display next to all my AR stuff, position the cameraNode as well (pointing in the direction where objects stay in the scene)
cameraNode.position = SCNVector3(initialStartPosition.x, initialStartPosition.y + 0.5, initialStartPosition.z)
let cameraPlane = SCNNode(geometry: SCNPlane(width: 0.5, height: 0.3))
cameraPlane.geometry?.firstMaterial?.diffuse.contents = cameraNode.camera
cameraPlane.position = SCNVector3(initialStartPosition.x - 1.0, initialStartPosition.y + 0.5, initialStartPosition.z)
sceneView.scene.rootNode.addChildNode(cameraPlane)
Everything compiles and loads... The display shows up at the given position, but it stays entirely gray. Nothing is displayed at all from the SCNCamera I put in the scene. Everything else in the AR scene works well, I just don't get any feed from that camera.
Hay anyone an approach to get this scenario working?
To even better visualize, I add some more print screens.
The following shows the Image trough the SCNCamera according to ARGeo's input. But it takes the whole screen, instead of displaying its contents on a SCNPlane, like I need.
The next Print screen actually shows the current ARView result as I got it using my posted code. As you can see, the gray Display-Plane remains gray - it shows nothing.
The last print screen is a photomontage, showing the expected result, as I'd like to get.
How could this be realized? Am I missing something fundamental here?
After some research and sleep, I came to the following, working solution (including some inexplainable obstacles):
Currently, the additional SCNCamera feed is not linked to a SCNMaterial on a SCNPlane, as it was the initial idea, but I will use an additional SCNView (for the moment)
In the definitions I add an other view like so:
let overlayView = SCNView() // (also tested with ARSCNView(), no difference)
let camera = SCNCamera()
let cameraNode = SCNNode()
then, in viewDidLoad, I setup the stuff like so...
camera.automaticallyAdjustsZRange = true
camera.usesOrthographicProjection = false
cameraNode.camera = camera
cameraNode.camera?.focalLength = 50
sceneView.scene.rootNode.addChildNode(cameraNode) // add the node to the default scene
overlayView.scene = scene // the same scene as sceneView
overlayView.allowsCameraControl = false
overlayView.isUserInteractionEnabled = false
overlayView.pointOfView = cameraNode // this links the new SCNView to the created SCNCamera
self.view.addSubview(overlayView) // don't forget to add as subview
// Size and place the view on the bottom
overlayView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width * 0.8, height: self.view.bounds.height * 0.25)
overlayView.center = CGPoint(x: self.view.bounds.width * 0.5, y: self.view.bounds.height - 175)
then, in some other function, I place the node containing the SCNCamera to my desired position and angle.
// (exemplary)
cameraNode.position = initialStartPosition + SCNVector3(x: -0.5, y: 0.5, z: -(Float(shiftCurrentDistance * 2.0 - 2.0)))
cameraNode.eulerAngles = SCNVector3(-15.0.degreesToRadians, -15.0.degreesToRadians, 0.0)
The result, is a kind of window (the new SCNView) at the bottom of the screen, displaying the same SceneKit content as in the main sceneView, viewed trough the perspective of the SCNCamera plus its node position, and that very nicely.
In a common iOS/Swift/ARKit project, this construct generates some side effects, that one may struggle into.
1) Mainly, the new SCNView shows SceneKit content from the desired perspective, but the background is always the actual physical camera feed. I could not figure out, how to make the background a static color, by still displaying all the SceneKit content. Changing the new scene's background property affects also the whole main scene, what is actually NOT desired.
2) It might sound confusing, but as soon as the following code get's included (which is essential to make it work):
overlayView.scene = scene
the animation speed of the entire scenes (both) DOUBLES! (Why?)
I got this corrected by adding/changing the following property, which restores the animation speed behavour almost like normal (default):
// add or change this in the scene setup
scene.physicsWorld.speed = 0.5
3) If there are actions like SCNAction.playAudio in the project, all the effects will no longer play - as long as I don't do this:
overlayView.scene = nil
Of course, the additional SCNView stops working but everything else gets gets back to its normal.
Use this code (as a starting point) to find out how to setup a virtual camera.
Just create a default ARKit project in Xcode and copy-paste my code:
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.showsStatistics = true
let scene = SCNScene(named: "art.scnassets/ship.scn")!
sceneView.scene = scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 0, 1)
cameraNode.camera?.focalLength = 70
cameraNode.camera?.categoryBitMask = 1
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNode
sceneView.allowsCameraControl = true
sceneView.backgroundColor = UIColor.darkGray
let plane = SCNNode(geometry: SCNPlane(width: 0.8, height: 0.45))
plane.position = SCNVector3(0, 0, -1.5)
// ASSIGN A VIDEO STREAM FROM SCENEKIT-RECORDER TO YOUR MATERIAL
plane.geometry?.materials.first?.diffuse.contents = capturedVideoFromSceneKitRecorder
scene.rootNode.addChildNode(plane)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}
}
UPDATED:
Here's a SceneKit Recorder App that you can tailor to your needs (you don't need to write a video to disk, just use a CVPixelBuffer stream and assign it as a texture for a diffuse material).
Hope this helps.
I'm a little late to the party, but I've had a similar issue recently.
As far as I can tell, you cannot directly connect a camera to a node's material. You can, however, use a scene's layer as a texture for a node.
The code below is not verified, but should be more or less ok:
class MyViewController: UIViewController {
override func loadView() {
let projectedScene = createProjectedScene()
let receivingScene = createReceivingScene()
let projectionPlane = receivingScene.scene?.rootNode.childNode(withName: "ProjectionPlane", recursively: true)!
// Here's the important part:
// You can't directly connect a camera to a material's diffuse texture.
// But you can connect a scene's layer as a texture.
projectionPlane.geometry?.firstMaterial?.diffuse.contents = projectedScene.layer
projectedScene.layer.contentsScale = 1
// Note how we only need to connect the receiving view to the controller.
// The projected view is not directly connected as a subview,
// but updates in projectedScene will still be reflected in receivingScene.
self.view = receivingScene
}
func createProjectedScene() -> SCNView {
let view = SCNView()
// ... set up scene ...
return view
}
func createReceivingScene() -> SCNView {
let view = SCNView()
// ... set up scene ...
let projectionPlane = SCNNode(geometry: SCNPlane(width: 2, height: 2)
projectionPlane.name = "ProjectionPlane"
view.scene.rootNode.addChildNode(projectionPlane)
return view
}
}
I'm building a menu Game Scene where I have a Logo and 4 buttons, all of those have a SKPhysicsBody with the rectangle size of the image.
All of those I only set:
logo.physicsBody = SKPhysicsBody(rectangleOf: logo.size)
logo.physicsBody?.affectedByGravity = false
Full code of creating the objects (all are the same)
logo = SKSpriteNode(imageNamed: "logo")
logo.anchorPoint = CGPoint.zero
logo.name = "logo"
logo.zPosition = 2
logo.size = CGSize(width: 309, height: 272)
logo.setScale(Generic.utilities.getScale())
self.addChild(logo)
logo.position = CGPoint(x: self.frame.width/2 - logo.size.width/2, y: self.frame.height - logo.size.height - 40)
logo.physicsBody = SKPhysicsBody(rectangleOf: logo.size)
logo.physicsBody?.affectedByGravity = false
and during a button press I change all the nodes affectedByGravity to true so they can fall of the screen and new elements can come in.
The problem I'm having is that when I run the app, the elements are all shifted/tilted on the screen and not in their original position
All I wanted is for them to stay in their position until I click one of the buttons and then they can fall off the screen.
Any reason why when I set a physics body and affectedByGravity they all tilt like this?
Well, Thanks to #RonMyschuk and #Knight0fDragon I found out (didn't know before) that I could add to my Scene loading the following:
skView.showsPhysics = true
Which add border lines around the physicsBody of your nodes, that way you can see them interacting. And by doing that I saw that the physicsBody of one of my nodes was completely in a different position then it should
By taking care of this issue, everything went back to normal
Edit 1
Hello, This is my first time using code to add constraints. I normally just use interface builder. I am trying to add a vertical UISlider, before I thought that mixing visually added constraints was interfering with coded constraints. I have now updated my code and therefore this question to create constrains using ONLY code in this particular view container.
What I have done is I created a view directly underneath another view. I created 3 small views inside that that match the width of the textfields in the view above and then spaced them out the same as how the textfields are spaced out.
For testing purposes only I gave these 3 small views a color to see if it worked, and it does.
When I actually finish the app those red, green, and blue views will be clear. The only reason I wanted them was so when I create the sliders I can constrain each one of them to the center of the view...which is how the labels above the textfields are constrained.
Here is the code for this
which works
// Mark: Hidden View
let leftHiddenView = UIView()
let centerHiddenView = UIView()
let rightHiddenView = UIView()
let hiddenViews = [leftHiddenView, centerHiddenView, rightHiddenView]
for views in hiddenViews {
views.translatesAutoresizingMaskIntoConstraints = false
sliderContainer.addSubview(views)
views.backgroundColor = .white
let widthConstraint = views.widthAnchor.constraint(equalToConstant: 35)
let heightConstraint = views.heightAnchor.constraint(equalToConstant: 5)
NSLayoutConstraint.activate([widthConstraint, heightConstraint])
}
let centerViewHorizontalConstraint = centerHiddenView.centerXAnchor.constraint(equalTo: sliderContainer.centerXAnchor)
let centerViewTopConstraint = centerHiddenView.topAnchor.constraint(equalTo: sliderContainer.topAnchor, constant: 50)
NSLayoutConstraint.activate([centerViewHorizontalConstraint, centerViewTopConstraint])
let leftViewVerticalCenterConstraint = leftHiddenView.centerYAnchor.constraint(equalTo: centerHiddenView.centerYAnchor, constant: 0)
let leftViewTrailingConstraint = leftHiddenView.trailingAnchor.constraint(equalTo: centerHiddenView.leadingAnchor, constant: -60)
NSLayoutConstraint.activate([leftViewVerticalCenterConstraint, leftViewTrailingConstraint])
let rightViewVerticalCenterConstraint = rightHiddenView.centerYAnchor.constraint(equalTo: centerHiddenView.centerYAnchor, constant: 0)
let rightViewTrailingConstraint = rightHiddenView.leadingAnchor.constraint(equalTo: centerHiddenView.trailingAnchor, constant: 60)
NSLayoutConstraint.activate([rightViewVerticalCenterConstraint, rightViewTrailingConstraint])
Now, I started to add a UISlider as vertical. And the exact same thing that happened before happened now.
As you can see everything breaks.
Here is the code thus far on that
// Mark: Slider View
let leftSlider = UISlider()
let centerSlider = UISlider()
let rightSlider = UISlider()
let colorSliders = [leftSlider, centerSlider, rightSlider]
for slider in colorSliders {
slider.translatesAutoresizingMaskIntoConstraints = false
sliderContainer.addSubview(slider)
let w = sliderContainer.bounds.width
slider.bounds.size.width = w
slider.center = CGPoint(x: w/2, y: w/2)
slider.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
slider.value = 0
slider.minimumValue = 0
slider.maximumValue = 255
let sliderTopConstraint = slider.topAnchor.constraint(equalTo: centerHiddenView.bottomAnchor, constant: 5)
let sliderBottomConstraint = slider.bottomAnchor.constraint(equalTo: sliderContainer.bottomAnchor, constant: 5)
NSLayoutConstraint.activate([sliderTopConstraint, sliderBottomConstraint])
slider.backgroundColor = .purple
}
let centerSliderHorizontalConstraints = centerSlider.centerXAnchor.constraint(equalTo: sliderContainer.centerXAnchor)
NSLayoutConstraint.activate([centerSliderHorizontalConstraints])
Don't ever misc the Design time constraints with adding Runtime constraints. Either add all constraints at design time or all constraints at runtime only. Else you will be in MESS. Make this as a good practice.
In case you need to change the frame, just change the constant property of the constraints and add all the required constraint at design time.
There will be very rare times when you need to add runtime constraints. (I am saying this because I always design in that way only. And that helps me a lot.) Design your screen in such a ways that even if you need to add 2 controls for dynamic UI changes, then keep 2 controls and do show hide with that controls. In can you need some kind of Animation with your controls, you don't need to change design time constraints.
I know this does not answer your question directly, but hope you will get the understanding of how to use constraints.
From your screenshot, I am not able understand exactly what is your UI looks like. Can you give little bit more idea of how your UI looks like? so that I can suggest some idea of how to give constraints...
Well, it turns out that the problem was actually quite easy to solve from the beginning. I only overlooked it because of being intimidated by the vertical UISlider. Since I did not give the above container a fixed height when I added the container below it and ran the app, the containers equally filled the space and the contents inside messed up accordingly. I simply gave the top container with just the labels and textfield a fixed height of 61 and its now much closer to being done. Sorry
I have a problem with SpriteKit and Swift.
I'm adding SKSpriteNodes at different points of the code to the scene - some of them are clickable, some are not. I use the clickable Nodes as a Menu for the player. So, for example - if he clicks the InventoryButtonNode he jumps into the Inventory. In the Inventory he can touch the PlayButton and jumps back into the game. So, first i add the Nodes:
override func didMoveToView(view: SKView) {
PlayButton = SKSpriteNode(imageNamed: "PlayButton")
PlayButton.size = CGSize(width: 100, height: 100)
PlayButton.position = CGPoint ... // not important
PlayButton.zPosition = 200
PlayButton.name = "PlayButton"
self.addChild(PlayButton)
InventoryButton = SKSpriteNode(imageNamed: "InventoryButton")
InventoryButton.size = CGSize(width: 100, height: 100)
InventoryButton.position = CGPoint ... // different position than PlayButton
InventoryButton.zPosition = 200
InventoryButton.name = "PlayButton"
self.addChild(InventoryButton)
In the override func touchesBegan I use these "menu"-Nodes, here for example the InventoryButton-Node.
if InventoryButton.containsPoint(touch.locationInNode(self)) {
print("Show Inventory")
ShowInventory()
}
Now in the ShowInventory() function i want to remove those "menu"-Buttons from view so that i can add other nodes to show the Inventory of the Player.
func ShowInventory(){
PlayButton.removeFromParent()
InventoryButton.removeFromParent()
}
If I build and run this, the Nodes will be removed - or lets better say - they will get invisible.
Because, if i touch now at the position of InventoryButton I still get the print "Show Inventory" - so the function still reacts to my touch even if the node is not visible.
My problem is, that i have like 4 different functions like ShowInventory() .. I have ShowGame() and so on .. and i want in these functions to completely remove the Nodes and the "Touch"-Ability ..
I need a function which can remove the Nodes completely ..
I have even tried:
func ShowInventory(){
self.removeAllChildren()
}
I get a grey Background without any nodes .. but still - if I touch where the Inventory Buttons position was i get the function called and the print "Show Inventory" ... it's frustrating.
It is because your check for which button is pressed does not take into account whether the button is visible.
Even if a node has been removed from its parent, it still has a position and size. Those two properties are used by containsPoint: to determine if a point is in a node
The easiest way to fix it would be to just check if the button has a parent node before checking to see if the button contains the point.
if InventoryButton.parrent != nil && InventoryButton.containsPoint(touch.locationInNode(self)) {
print("Show Inventory")
ShowInventory()
}