How can I make background of an SKScene transparent so I can layer in a ZStack? - sprite-kit

I'm trying to make a game using SwiftUI and SpriteKit. And I want to be able to layout the game in a ZStack with the game layered over the background image. In order to do that, I need to make the background of the SKScene transparent. But when I do that, the nodes that I add to the SKScene no longer show up.
Here's the coding that I'm using:
import SwiftUI
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
view.allowsTransparency = true
self.backgroundColor = .clear
view.alpha = 0.0
view.isOpaque = true
view.backgroundColor = SKColor.clear.withAlphaComponent(0.0)
if let particles = SKEmitterNode(fileNamed: "Mud") {
particles.position.x = 512
addChild(particles)
}
}
}
struct ContentView: View {
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: 300, height: 400)
scene.scaleMode = .fill
return scene
}
var body: some View {
ZStack {
Image("road")
.resizable()
.aspectRatio(contentMode: /*#START_MENU_TOKEN#*/.fill/*#END_MENU_TOKEN#*/)
.ignoresSafeArea()
SpriteView(scene: scene)
.ignoresSafeArea()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
}
The background of the SKScene is transparent and I can see the image layered underneath it in the ZStack. But, everything else in the scene is also transparent so that I can't see the emitter effect at all.
How can I make just the background of the SKScene transparent?

Finally solved using custom SpriteView. The code below makes the spritekit view actually have a transparent background. The problem seems to be the swiftUI's declarative nature means you're always using their SpriteView (which has a black background). The SKScene is really the only thing being passed into SwiftUI.
I think this explains why the view settings don't have an effect as with UIKit.
The solution was to create a new SpriteView outside of the body struct, and then use it in the body struct in place of standard SpriteView(scene:scene). This allows setting the SpriteView.Options in the init() to finally set .allowsTransparency in the SwiftUI's some View that is presented.
See the code below:
// Created by Larry Mcdowell on 9/25/20.
//
import SwiftUI
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
// 1
self.backgroundColor = .clear
//2
view.allowsTransparency = true
if let particles = SKEmitterNode(fileNamed: "Mud"){
particles.position.x = 140
particles.position.y = 200
addChild(particles)
}
}
}
struct ContentView: View {
var mySpriteView:SpriteView
init(){
mySpriteView = SpriteView(scene: scene, transition: nil, isPaused: false, preferredFramesPerSecond: 60, options: [.allowsTransparency], shouldRender: {_ in return true})
}
let scene:GameScene = {
let scene = GameScene()
scene.size = CGSize(width: 300, height: 400)
scene.scaleMode = .fill
return scene
}()
var body: some View {
ZStack {
Image("road")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea()
mySpriteView
.ignoresSafeArea()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This was a fun project for a rainy day!!

All you need is to set the scene background to .clear, and to pass options as a parameter to the SpriteView for transparency. So for your case, it would be
SpriteView(scene: scene, options: [.allowsTransparency])
All the SKView modifications can be removed.

See my final answer on this thread for the actual solution. I decided to delete the workaround I originally included once I figured out custom SpriteViews.

Related

Project with SpriteView runs but animation does not show in simulator

Have started exploring SpriteView and I have gotten to the point which the editor recognizes the SpriteView object and runs, but the simulator screen stays black.
The scene runs and everything in the scene editor too.
Am I missing something? Is there some sort of initialization step that I'm missing?
import SwiftUI
import SpriteKit
struct ContentView: View {
var scene: SKScene {
let scene = SKScene(fileNamed: "MyScene")!
scene.scaleMode = .aspectFit
scene.size = CGSize(width: 300, height: 400)
scene.backgroundColor = .white
return scene
}
var body: some View {
SpriteView(scene: scene)
.frame(width: 300, height: 400)
.ignoresSafeArea()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

pass / share class with SpriteView GameScene from a SwiftUi View

I am playing around with SpriteView, which works great. However I cannot figure out how to share a class with Spritekit GameScene,that is instantiated in one my views. Sharing my class with other views works like a charm. But how can I access my gameCenterManager class from spritekit's GameScene, how do I pass the class ? I don't have any clue how to do this. I considered making a global accessible class, but its not what I want.
The goal is to be able to send and receive data from inside GameScene to update players location and etc. But the matchmaking happens inside a SwiftUI View, which is also where the class is created.
The gameCenterManger class is created like so;
struct ContentView: View {
#StateObject var gameCenterManager = GameCenterManager()
var body: some View {
...
etc
the class is set up like so :
class GameCenterManager: NSObject, ObservableObject {
//lots of stuff going on
...
}
And is shared to the GameSceneView view that will create the SpriteView, like so :
Button("Show Sheet") {
self.showingSheet.toggle()
}
.buttonStyle(RoundedRectangleButtonStyle())
.buttonStyle(ShadowButtonStyle())
.fullScreenCover(isPresented: $showingSheet, content: { GameSceneView(gameCenterManager:self.gameCenterManager)})
Finally, inside my GameSceneView, the GameScene for SpriteView is configured.
struct GameSceneView: View {
var gameCenterManager: GameCenterManager
#Environment(\.presentationMode) var presentationMode
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height)
scene.scaleMode = .fill
return scene
}
var body: some View {
Button("Dismiss") {
self.presentationMode.wrappedValue.dismiss()
}
Button("send data") {
gameCenterManager.increment()
}
SpriteView(scene: scene )
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height - 200 )
.ignoresSafeArea()
.overlay(ImageOverlay(), alignment: .bottomTrailing)
}
}
Perhaps others are looking to do the same? Here below is the answer in code snippets
GameCenterManager is the class that I want to access across all views and SKScenes
In mainview:
#main
struct gameTestApp: App {
#StateObject var gameCenterManager = GameCenterManager()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(gameCenterManager)
}
}
}
In Contentview view add :
#EnvironmentObject var gameCenterManager:GameCenterManager
And where I transition to GameSceneView inside Contentview, which will load my SKScene
Button("Show Sheet") {
self.showingSheet.toggle()
}
.buttonStyle(RoundedRectangleButtonStyle())
.buttonStyle(ShadowButtonStyle())
.fullScreenCover(isPresented: $showingSheet, content: { GameSceneView()})
Then in GameSceneView add:
#EnvironmentObject var gameCenterManager:GameCenterManager
and where we load SKScene:
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height)
scene.scaleMode = .fill
scene.gameCenterManager = gameCenterManager
return scene
}
Then finally in GameScene itself add:
var gameCenterManager: GameCenterManager?

How to go full screen with VideoPlayer() in SwiftUI?

I'm using the following code in my project and it works great.
But I need to know if its possible to play the video in full screen when the user taps on a button or any other action.
This is my code:
VideoPlayer(player: AVPlayer(url: URL(string: "https://xxx.mp4")!)) {
VStack {
Text("Watermark")
.foregroundColor(.black)
.background(Color.white.opacity(0.7))
Spacer()
}
.frame(width: 400, height: 300)
}
.frame(width: UIScreen.main.bounds.width, height: 300)
So basically, the idea is to make the VideoPlayer to go full screen similar to youtube's video player.
Is this possible using the VideoPlayer() in swiftUI?
They should include an option or have that button by default in VideoPlayer I don't know why apple did this if someone knows add a comment about the same, anyways
There is a simpler method though
struct CustomVideoPlayer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let url: String = "https://xxx.mp4"
let player1 = AVPlayer(url: URL(string: url)!)
controller.player = player1
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}
Try this code and call CustomVideoPlayer() and give it a frame with any height and you will have a fullscreen option on the top left corner just like youtube
EDIT: I tried the accepted solution but I don't think it is going fullscreen
I took #Viraj D's answer and created a Swift package called AZVideoPlayer. Add the package with SPM or feel free to just copy the source code into your project.
The package also adds the following:
Addresses the issue where onDisappear is called when full screen presentation begins by allowing a way to ignore the call in that particular case.
Allows you to have the video continue playing when ending full screen presentation (it pauses by default).
Here's an example:
import SwiftUI
import AVKit
import AZVideoPlayer
struct ContentView: View {
var player: AVPlayer?
#State var willBeginFullScreenPresentation: Bool = false
init(url: URL) {
self.player = AVPlayer(url: url)
}
var body: some View {
AZVideoPlayer(player: player,
willBeginFullScreenPresentationWithAnimationCoordinator: willBeginFullScreen,
willEndFullScreenPresentationWithAnimationCoordinator: willEndFullScreen)
.onDisappear {
// onDisappear is called when full screen presentation begins, but the view is
// not actually disappearing in this case so we don't want to reset the player
guard !willBeginFullScreenPresentation else {
willBeginFullScreenPresentation = false
return
}
player?.pause()
player?.seek(to: .zero)
}
}
func willBeginFullScreen(_ playerViewController: AVPlayerViewController,
_ coordinator: UIViewControllerTransitionCoordinator) {
willBeginFullScreenPresentation = true
}
func willEndFullScreen(_ playerViewController: AVPlayerViewController,
_ coordinator: UIViewControllerTransitionCoordinator) {
// This is a static helper method provided by AZVideoPlayer to keep
// the video playing if it was playing when full screen presentation ended
AZVideoPlayer.continuePlayingIfPlaying(player, coordinator)
}
}
Here's how I managed to do it. I hope this helps someone else in the future.
This is a rough idea but it works as it was intended to.
Basically, I've put the VideoPlayer in a VStack and gave it a full width and height.
I gave the VStack a set height and full width.
I gave the VStack a tapGesture and changed its width and height on double tap and this way, all its content (VideoPlayer) will resize accordingly.
This is the code:
struct vidPlayerView: View {
#State var animate = false
var body: some View {
VStack{
VideoPlayer(player: AVPlayer(url: URL(string: "https://xxxx.mp4")!)) {
VStack {
Image(systemName: "info.circle")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
Spacer()
}
.frame(width: 400, height: 300, alignment: .topLeading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Spacer()
}.onTapGesture(count: 2) {
withAnimation {
self.animate.toggle()
}
}
.frame(maxWidth: self.animate ? .infinity : UIScreen.main.bounds.width, maxHeight: self.animate ? .infinity : 300, alignment: .topLeading)
}
}
The above code can be executed by taping on a button or any other actions.

How to change the background color of a SceneView 3D Object in SwiftUI

Does anybody knows how to change the color of the background of a SceneView object?
I'm trying by just placing the background attribute to the SceneView, but it doesn't change it, still with a default background.In this case I wanted to the background be green, but it's keeps gray/white
import SwiftUI
import SceneKit
struct ContentView: View {
var body: some View {
ZStack{
Color.red
SceneView(
scene: SCNScene(named: "Earth.scn"),
options: [.autoenablesDefaultLighting,.allowsCameraControl]
)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height/2, alignment: .center)
.background(Color.green)
.border(Color.blue, width: 3)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The gray that you are seeing is from SCNScene's background property. You need to set this to UIColor.green.
struct ContentView: View {
var body: some View {
ZStack{
Color.red
SceneView(
scene: {
let scene = SCNScene(named: "Earth.scn")!
scene.background.contents = UIColor.green /// here!
return scene
}(),
options: [.autoenablesDefaultLighting,.allowsCameraControl]
)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height/2, alignment: .center)
.border(Color.blue, width: 3)
}
}
}
Result:

SwiftUI with SceneKit: How to use button action from view to manipulate underlying scene

I’m trying to implement a very basic SwiftUI app with an underlying SceneKit scene/view. The buttons in the SwiftUI view should manipulate the content of the scene and vice versa the content of the scene should determine if the buttons are active or inactive.
Yes, I’ve read and watched the Apple developer sessions 226: Data Flow through SwiftUI and 231: Integrating SwiftUI. And I worked my way through the Apple tutorials. But I’m little lost here. Any hints and directions are appreciated.
Here is the code:
I have a MainView, which uses a SceneKit SCNView with a HUDView on top:
import SwiftUI
struct MainView: View {
var body: some View {
ZStack {
SceneView()
HUDView()
}
}
}
The SceneView integrates a SCNView via the UIViewRepresentable protocol. The scene has two functions to add and remove the box from the scene:
import SwiftUI
import SceneKit
struct SceneView: UIViewRepresentable {
let scene = SCNScene()
func makeUIView(context: Context) -> SCNView {
// create a box
scene.rootNode.addChildNode(createBox())
// code for creating the camera and setting up lights is omitted for simplicity
// …
// retrieve the SCNView
let scnView = SCNView()
scnView.scene = scene
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// configure the view
scnView.backgroundColor = UIColor.gray
// show statistics such as fps and timing information
scnView.showsStatistics = true
scnView.debugOptions = .showWireframe
}
func createBox() -> SCNNode {
let boxGeometry = SCNBox(width: 20, height: 24, length: 40, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
return box
}
func removeBox() {
// check if box exists and remove it from the scene
guard let box = scene.rootNode.childNode(withName: "box", recursively: true) else { return }
box.removeFromParentNode()
}
func addBox() {
// check if box is already present, no need to add one
if scene.rootNode.childNode(withName: "box", recursively: true) != nil {
return
}
scene.rootNode.addChildNode(createBox())
}
}
The HUDView bundles the buttons with actions to add and remove the box from the underlying scene. If the box object is in the scene, the add button should be inactive, only the remove button should be active:
struct HUDView: View {
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: 2) {
Spacer()
ButtonView(action: {}, icon: "plus.square.fill", isActive: false)
ButtonView(action: {}, icon: "minus.square.fill")
ButtonView(action: {}) // just a dummy button
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
The buttons are fairly simple as well, they take an action and optionally an icon as well as their initial active/inactive state:
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
#State var isActive: Bool = true
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
}
}
The resulting app is pretty simple:
The SceneKit scene is rendered correctly and I am able to manipulate the camera on screen.
But how do I connect the button actions to the corresponding scene functions? How do I let the scene inform the HUDView about it’s content (box is present or not), so it can set the active/inactive state of the buttons?
What is the best approach to this task? Should I implement a Coordinator in SceneView? Should I use a separate SceneViewController implemented via the UIViewControllerRepresentable protocol?
I found a solution using #EnvironmentalObject but I am not completely sure, if this is the right approach. So comments on this are appreciated.
First, I moved the SCNScene into it’s own class and made it an OberservableObject:
class Scene: ObservableObject {
#Published var scene: SCNScene
init(_ scene: SCNScene = SCNScene()) {
self.scene = scene
self.scene = setup(scene: self.scene)
}
// code omitted which deals with setting the scene up and adding/removing the box
// method used to determine if the box node is present in the scene -> used later on
func boxIsPresent() -> Bool {
return scene.rootNode.childNode(withName: "box", recursively: true) != nil
}
}
I inject this Scene into the app as an .environmentalObject(), so it is available to all views:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let sceneKit = Scene()
let mainView = MainView().environmentObject(sceneKit)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: mainView)
self.window = window
window.makeKeyAndVisible()
}
}
}
MainView is slightly altered to call SceneView (a UIViewRepresentable) with the separate Scene for the environment:
struct MainView: View {
#EnvironmentObject var scene: Scene
var body: some View {
ZStack {
SceneView(scene: self.scene.scene)
HUDView()
}
}
}
Then I make the Scene available to the HUDView as an #EnvironmentalObject, so I can reference the scene and its methods and call them from the Button action. Another effect is, I can query the Scene helper method to determine, if a Button should be active or not:
struct HUDView: View {
#EnvironmentObject var scene: Scene
#State private var canAddBox: Bool = false
#State private var canRemoveBox: Bool = true
var body: some View {
VStack {
HStack(alignment: .center, spacing: 0) {
Spacer ()
ButtonView(
action: {
self.scene.addBox()
if self.scene.boxIsPresent() {
self.canAddBox = false
self.canRemoveBox = true
}
},
icon: "plus.square.fill",
isActive: $canAddBox
)
ButtonView(
action: {
self.scene.removeBox()
if !self.scene.boxIsPresent() {
self.canRemoveBox = false
self.canAddBox = true
}
},
icon: "minus.square.fill",
isActive: $canRemoveBox
)
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
Here is the ButtonView code, which used a #Bindingto set its active state (not sure about the correct order for this with the#State property inHUDView`):
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
#Binding var isActive: Bool
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(self.isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
.disabled(self.isActive ? false: true)
}
}
Anyway, the code works now. Any thoughts on this?
I answered a very similar question here. The idea is that you create a PassthroughSubject that your parent view (the one that owns the buttons) will send events down and your UIViewRespresentable will subscribe to those events.
There are many ways to do this stuff and you've obviously solved it - congrats!
You might check out this post 58103566 on game design layout. I'm sure there will be experts that disagree which is fine, and I don't want to cause a debate, just thought it might save you a few steps in the future.
From multiple VC's, I manipulate game objects and buttons often, usually without muddying up the water too much.
Hope it helps.