I am working with iOS 11 (for ARKit) and while many point to a sample app for SceneKit from Apple with a Fox, I am having problem with the extension it uses in that sample project (file) to add animations:
extension CAAnimation {
class func animationWithSceneNamed(_ name: String) -> CAAnimation? {
var animation: CAAnimation?
if let scene = SCNScene(named: name) {
scene.rootNode.enumerateChildNodes({ (child, stop) in
if child.animationKeys.count > 0 {
animation = child.animation(forKey: child.animationKeys.first!)
stop.initialize(to: true)
}
})
}
return animation
}
}
It seems that this extension is very handy but I am not sure how to migrate this now that it is deprecated? Is it built into SceneKit by default now?
The documentation didn't really show much info on why it was deprecated or where to go from here.
Thanks
TL;DR: examples of how to use new APIs can be found in Apple's sample game (search for SCNAnimationPlayer)
Even though animation(forKey:) and its sister methods that work with CAAnimation have been deprecated in iOS11, you can continue using them – everything will work.
But if you want to use new APIs and don't care about backwards compatibility (which you wouldn't need in the case of ARKit anyway, because it's only available since iOS11), read on.
The newly introduced SCNAnimationPlayer provides a more convenient API compared to its predecessors. It is now easier to work with animations in real time.
This video from WWDC2017 would be a good starting point to learn about it.
As a quick summary: SCNAnimationPlayer allows you to change animation's speed on the fly. It provides a more intuitive interface for animation playback using methods such as play() and stop() compared to adding and removing CAAnimations.
You also can blend two animations together which, for example, can be used to make smooth transitions between them.
You can find examples of how to use all of this in the Fox 2 game by Apple.
Here's the extension you've posted adapted to use SCNAnimationPlayer (which you can find in the Character class in the Fox 2 sample project):
extension SCNAnimationPlayer {
class func loadAnimation(fromSceneNamed sceneName: String) -> SCNAnimationPlayer {
let scene = SCNScene( named: sceneName )!
// find top level animation
var animationPlayer: SCNAnimationPlayer! = nil
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
return animationPlayer
}
}
You can use it as follows:
Load the animation and add it to the corresponding node
let jumpAnimation = SCNAnimationPlayer.loadAnimation(fromSceneNamed: "jump.scn")
jumpAnimation.stop() // stop it for now so that we can use it later when it's appropriate
model.addAnimationPlayer(jumpAnimation, forKey: "jump")
Use it!
model.animationPlayer(forKey: "jump")?.play()
Lësha Turkowski's answer without force unwraps.
extension SCNAnimationPlayer {
class func loadAnimationPlayer(from sceneName: String) -> SCNAnimationPlayer? {
var animationPlayer: SCNAnimationPlayer?
if let scene = SCNScene(named: sceneName) {
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
}
return animationPlayer
}
}
Here's an example of SwiftUI and SceneKit
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
#Binding var isPlayingAnimation: Bool
let scene = SCNScene(named: "art.scnassets/TestScene.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
controlAnimation(isAnimating: isPlayingAnimation, nodeName: "TestNode", animationName: "TestAnimationName")
}
func controlAnimation(isAnimating: Bool, nodeName: String, animationName: String) {
guard let node = scene.rootNode.childNode(withName: nodeName, recursively: true) else { return }
guard let animationPlayer: SCNAnimationPlayer = node.animationPlayer(forKey: animationName) else { return }
if isAnimating {
print("Play Animation")
animationPlayer.play()
} else {
print("Stop Animation")
animationPlayer.stop()
}
}
}
struct DogAnimation_Previews: PreviewProvider {
static var previews: some View {
ScenekitView(isPlayingAnimation: .constant(true))
}
}
A 2023 example.
I load typical animations like this:
func simpleLoadAnim(filename: String) -> SCNAnimationPlayer {
let s = SCNScene(named: filename)!
let n = s.rootNode.childNodes.filter({!$0.animationKeys.isEmpty}).first!
return n.animationPlayer(forKey: n.animationKeys.first!)!
}
So,
laugh = simpleLoadAnim(filename: "animeLaugh") // animeLaugh.dae
giggle = simpleLoadAnim(filename: "animeGiggle")
You then, step one, have to add them to the character:
sally.addAnimationPlayer(laugh, forKey: "laugh")
sally.addAnimationPlayer(giggle, forKey: "giggle")
very typically you would have only one going at a time. So set the weights, step two.
laugh.blendFactor = 1
giggle.blendFactor = 0
to play or stop an SCNAnimationPlayer it's just step three
laugh.play()
giggle.stop()
Almost certainly, you will have (100s of lines) of your own code to blend between animations (which might take only a short time, 0.1 secs, or may take a second or so). To do so, you would use SCNAction.customAction.
If you prefer you can access the animation, on, the character (sally) using the keys. But really the "whole point" is you can just start, stop, etc etc the SCNAnimationPlayer.
You will also have lots of code to set up the SCNAnimationPlayer how you like (speed, looping, mirrored, etc etc etc etc etc etc etc etc)
You will need THIS very critical answer to get collada files (must be separate character / anim files) working properly https://stackoverflow.com/a/75093081/294884
Once you have the various SCNAnimationPlayer animations working properly, it is quite easy to use, run, blend etc animes.
The essential sequence is
each anime must be in its own .dae file
load each anime files in to a SCNAnimationPlayer
"add" all the animes to the character in question
program the blends
then simply play() or stop() the actual SCNAnimationPlayer items (don't bother using the keys on the character, it's a bit pointless)
Related
I’ve been using the agora SDK (audio only) for a while now, and I’m very pleased! Users can enter rooms of max 8 people and talk to each other. Now, I’m supposed to add a video feature, so they can enable their video stream at any time they want.
I’ve added the AgoraRtcEngine_iOS 3.7.0 pod and this is how I initialize the agora engine:
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: AppKeys.agoraAppId, delegate: self)
agoraKit?.setChannelProfile(.liveBroadcasting)
agoraKit?.setClientRole(.broadcaster)
agoraKit?.enableVideo()
agoraKit?.muteLocalVideoStream(true)
agoraKit?.muteLocalAudioStream(true)
agoraKit?.enableAudioVolumeIndication(1000, smooth: 3, report_vad: true)
This way, when a user joins a room, they have muted their audio and video streams, so they can enable it whenever they’re ready. The thing is, I’m using a UICollectionView to present the broadcasters. (everyone's user cell is visible at all times, so no reuse takes place, but collection view reloads happen constantly)
This is part of the cell setup (cellForItemAt), that handles the video view using a delegate:
private func setupVideoView(uid: UInt, isOfCurrentUser: Bool) {
videoView.frame.size = avatarBackgroundView.frame.size
videoView.layer.cornerRadius = avatarBackgroundView.layer.cornerRadius
if isOfCurrentUser {
delegate?.localVideoSetupWasRequested(videoView: videoView)
} else {
delegate?.remoteVideoSetupWasRequested(uid: uid, videoView: videoView)
}
}
And this is the conformance to the protocol:
extension RealTimeAudioAgoraService: NewVoiceRoomCellsDelegate {
func localVideoSetupWasRequested(videoView: UIView) {
guard !enabledVideoUIds.contains(currentUserRTCServiceId) else { return }
let videoCanvas = setUpVideoView(uid: currentUserRTCServiceId, videoView: videoView)
agoraKit?.setupLocalVideo(videoCanvas)
}
func remoteVideoSetupWasRequested(uid: UInt, videoView: UIView) {
guard !enabledVideoUIds.contains(uid) else { return }
let videoCanvas = setUpVideoView(uid: uid, videoView: videoView)
agoraKit?.setupRemoteVideo(videoCanvas)
}
private func setUpVideoView(uid: UInt, videoView: UIView) -> AgoraRtcVideoCanvas {
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = uid
videoCanvas.view = videoView
videoCanvas.renderMode = .hidden
// ------------------------------------
enabledVideoUIds.append(uid)
// ------------------------------------
return videoCanvas
}
}
As you can see, I keep track of all the uids I have enabled the video canvas for, so I do it only once for each user. The thing is something messes up the UI (see attached video) and even users with muted videos show the video canvas of others. It’s like the video canvas of each user is being cycled over every other user.
Any help will be much appreciated!
I am making a custom animation handler class with swift, and GameKit. In summary, I want to initialize an Animator (a custom class), on any SKNode that I want to animate. In principle, this animator has a list of animations (SKActions) that all have various "trigger States", which correspond with the various game states the game can be in. The Animator class then observers the global state variable, and if it changes, and matches the animations' trigger states, then it runs that animation on the target node.
I got this to work, however, I want to add just a bit more functionality to this, in the form of changing timings for the animation. An example of this would be something like a dog running, perhaps over a set period of time (where the state does not change), I want the dog to start running fast, and slow over time. The way I'm thinking of implementing something like this is to have a variable, speed, for example, that represents the speed of the animation, and have it slow over time. Then I would pass in a computed property (return the current value of the speed var)to the animator class, such that every time it calls the function for this specific animation, instead of having a fixed time interval, it would get the speed var and pass it in.
I'm having trouble because when I pass in a computed property, swift just takes its value when it's getting passed and holds onto that, instead of taking the entire getter with it. I'm wondering if there is a way I can pass a computed property with a getter, into a function/class. Here is the animator classes that I'm referring to, if you would like to see some code :)
class Animation {
let waitForCompletion: Bool
let repeating: Bool
let animation: SKAction
var running: Bool = false
let triggerState: State
var currentState: State {
return States.currentState
}
let target: SKNode
init(_ triggerState: State, animates animation: SKAction, for target: SKNode, waitForCompletion: Bool = true, repeating: Bool = true) {
self.triggerState = triggerState
self.target = target
//here I simply just pass in an animation, which allows for grouping animations if I want to do more than jsut SKAction.Animat(with:,timeInterval:)
self.waitForCompletion = waitForCompletion
self.repeating = repeating
self.animation = animation
}
func update(_ first: Bool) {
if (currentState == triggerState || first) && !running {
self.running = true
if !self.waitForCompletion { target.removeAllActions() }
if !target.hasActions() {
target.run(animation) {
self.running = false
self.target.removeAllActions()
if self.repeating { self.update(false) }
}
}
}
}
}
heres the animator class:
class Animator {
var itemObserver: AnyCancellable?
var animations: [Animation] = []
var state: State {
didSet {
update()
}
}
init(_ animations: [Animation]) {
self.animations = animations
self.state = States.currentState
//initialize a temporary value, such that the class has initialized self
itemObserver = States.$currentState.sink() { let _ = ($0) }
itemObserver = States.$currentState
.sink() { self.state = $0 }
}
func update() {
if let animation = animations.first(where: {$0.triggerState == state}) {
animation.update(true)
}
}
}
and a sample initialization in an SKNode class:
//this initializes the animtor with a series of animations. The animation being passed here is a group animation(an important capabality of this system, and in the scaleY, I pass a stretchVar, which is a computed property, that only passes its value in this example)
animator = Animator([
Animation(State.throwing, animates: SKAction.group([
SKAction.animate(with: ballAtlas, timePerFrame: 0.1),
SKAction.scaleY(to: stretchVar, duration: 0)
]), for: self)
])
I'd like to be able to loop a live photo, for continuous playback.
So far, I'm trying to use the PHLivePhotoViewDelegate to accomplish this.
import Foundation
import SwiftUI
import PhotosUI
import iOSShared
struct LiveImageView: UIViewRepresentable {
let view: PHLivePhotoView
let model:LiveImageViewModel?
let delegate = LiveImageLargeMediaDelegate()
init(fileGroupUUID: UUID) {
let view = PHLivePhotoView()
// Without this, in landscape mode, I don't get proper scaling of the image.
view.contentMode = .scaleAspectFit
self.view = view
// Using this to replay live image repeatedly.
view.delegate = delegate
model = LiveImageViewModel(fileGroupUUID: fileGroupUUID)
guard let model = model else {
return
}
model.getLivePhoto(previewImage: nil) { livePhoto in
view.livePhoto = livePhoto
}
}
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let model = model else {
return
}
guard !model.started else {
return
}
model.started = true
view.startPlayback(with: .full)
}
}
class LiveImageLargeMediaDelegate: NSObject, PHLivePhotoViewDelegate {
func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
livePhotoView.stopPlayback()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
livePhotoView.startPlayback(with: .full)
}
}
}
But without full success. It seems the audio does play again, but not the video. The livePhotoView.stopPlayback and the async aspect are just additional changes I was trying. I've tried it without those too.
Note that I don't want the user to have to manually change the live photo (e.g., see NSPredicate to not include Loop and Bounce Live Photos).
Thoughts?
ChrisPrince I tried your code and it works fine for me, I just add delegate and start playback inside of it and everything runs well and smoothly. I thought that there is no point in using stop playback because the function itself says that the playback ended.
func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
livePhotoView.startPlayback(with: .full)
}
Let me explain a little better what I mean since it's kinda tricky to understand.
I'm creating a prototype for a videogame. Every level inherits the main rules from a SKScene called SceneLogic:
class SceneLogic: SKScene, SKPhysicsContactDelegate {
// Set up the physics, the contacts, touches and so on...
}
class Level1: SceneLogic { }
class Level2: SceneLogic { }
Every level has its own .sks file which specifies the different icon to show in the HUD. In order to create a kind of "game engine" I thought to init every kind of graphics inside the SceneLogic class by lazy var and ignore them if the current level doesn't need it.
Let me explain with an example
class SceneLogic: SKScene, SKPhysicsContactDelegate {
// Text, available only for level 1
private lazy var textTopHUD = childNode(withName: "textTop") as! SKLabelNode
// Icon, available only for levels 3,4,5
private lazy var iconBottomHUD = childNode(withName: "iconBottom") as! SKSpriteNode
// Icon, available only for level 2
private lazy var iconLeftHUD = childNode(withName: "iconLeft") as! SKSpriteNode
func initGame(level: Int) {
switch mode {
case 1: // Level 1
textTopHUD.text = "Level 1"
case 2: // Level 2
iconLeftHUD.position = ....
}
}
}
The fact is: for level 1, iconBottomHUD is nil, for level 2 textTopHUD is nil... but the app doesn't crash since the var is lazy and it won't be called for some levels.
My question is: is it a good programming style? Is it safe to use lazy in this way?
The thing about lazy properties is that it defers the execution of the initialization code. So in your case it doesn't really matter since they are actually nil. So you defer the init of nothing basically. If i were you i would basically either make it as a computed propert as such:
private lazy var iconBottomHUD: SKSpriteNode = {
guard let node = childNode(withName: "iconBottom") as? SKSpriteNode else {
fatalError()
}
return node
}
Or make it as #JavierRivarola mentioned, make it protocol based.
In a swift game using UIKit I am writing, a human player will interact with UIKit UIButtons, GUI elements to take actions.
In the game, the player will play against AI players.
But here's the thing; the human player presses buttons and interacts and the AI player does not.
Given a simple UIViewController;
class SampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buyBtnPressed(_ sender: UIButton) {
print ("pressed")
}
}
So what I'm trying to ascertain is, how does the AI player itself take actions and handling turns within the context of the current view controller?
I believe the best way to do this is that there should be a loop that will wait until all players have completed their respective turns.
But where does this loop go? In the view did load?
If so, won't it eat up memory, or potentially lead (if not careful) to an endless loop?
I'm finding it hard to ascertain how an AI player can take actions within the given context of a UIViewController considering GUI elements are for human interaction.
I don't mean the AI should be animating pressing buttons or interacting with the screen, I mean; I have a UIViewController, it has a view did load; what is the strategy of implementing AI turns and whether or not this should be be achieved in a "game loop" in the View did load or can this be achieved in another way?
My question is; given the context of a UIViewController; how can I code the handling of an AI player taking turns and can this be achieved with a loop or another strategy?
Many thanks
edit: Code is now added
I have written out a turn base manager using Swift playgrounds, and 2 examples one using a UIViewController and another is a loop.
code now follows;
import Foundation
import GameplayKit
class Player {
var name: String
public private(set) var isAI: Bool = false
public private(set) var turnOrder: Int = 0
init(name: String, isAI: Bool?) {
self.name = name
if let hasAI = isAI {
self.isAI = hasAI
}
}
func setTurnOrderIndex(number: Int) {
self.turnOrder = number
}
}
let p1 = Player.init(name: "Bob", isAI: false)
let p2 = Player.init(name: "Alex", isAI: true)
protocol TurnOrderManagerDelegate: NSObjectProtocol {
func turnOrderWasSet()
}
protocol TurnDelegate: class {
func turnIsCompleted()
}
class Turn: NSObject {
weak var player: Player?
weak var delegate: TurnDelegate?
public private(set) var completed: Bool = false {
didSet {
delegate?.turnIsCompleted()
}
}
init(player:Player, delegate: TurnDelegate) {
self.player = player
self.delegate = delegate
}
func setAsComplete() {
self.completed = true
}
}
class TurnOrderManager: NSObject, TurnOrderManagerDelegate, TurnDelegate {
static var instance = TurnOrderManager()
public private(set) var turnOrderIndex: Int = 0
public private(set) var turnOrder: [Turn] = [Turn]() {
didSet {
self.turnOrderWasSet()
}
}
var playerOnTurn: Player? {
let turnObj = self.turnOrder[turnOrderIndex]
return (turnObj.player)
}
var allTurnsCompleted: Bool {
let filtered = turnOrder.filter { (turnObj:Turn) -> Bool in
return (turnObj.completed)
}.count
return (filtered == turnOrder.count)
}
func setTurnOrder(players:[Player]) {
if (self.turnOrder.count == 0) {
for playerObj in players {
let turnObj = Turn.init(player: playerObj, delegate: self)
self.turnOrder.append(turnObj)
}
}
}
func turnOrderWasSet() {
for (index, turnObj) in self.turnOrder.enumerated() {
turnObj.player?.setTurnOrderIndex(number: index)
}
}
func next() {
if (turnOrderIndex < (self.turnOrder.count - 1)) {
turnOrderIndex += 1
}
else {
turnOrderIndex = 0
}
}
internal func turnIsCompleted() {
print (" - turnIsCompleted")
TurnOrderManager.instance.next()
}
}
class GameModel {
var turnOrderManager: TurnOrderManager
init() {
self.turnOrderManager = TurnOrderManager.instance
self.turnOrderManager.setTurnOrder(players:[p1,p2])
}
// other game model stuff [...]
}
class Phase1State : GKState {
var gameModel: GameModel!
init(gameModel:GameModel) {
super.init()
self.gameModel = gameModel
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool
{
return false
}
override func didEnter(from previousState: GKState?) {
}
override func willExit(to nextState: GKState) {
}
// MARK: - Action
func buy() {
let index = self.gameModel.turnOrderManager.turnOrderIndex
let turn = self.gameModel.turnOrderManager.turnOrder[index]
turn.setAsComplete()
}
}
class SomeViewController: UIViewController
{
var gameModel: GameModel?
weak var gamePhase: Phase1State?
var isPhaseComplete: Bool {
return self.gameModel?.turnOrderManager.allTurnsCompleted ?? false
}
override func viewDidLoad() {
super.viewDidLoad()
self.gameModel = GameModel.init()
self.gamePhase = Phase1State.init(gameModel: self.gameModel!)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func buyButtonPressed() {
self.gamePhase?.buy()
self.finishTurn()
}
func finishTurn() {
guard let turnIndex = self.gameModel?.turnOrderManager.turnOrderIndex else {
return
}
guard let turn = self.gameModel?.turnOrderManager.turnOrder[turnIndex] else {
return
}
turn.setAsComplete()
if (self.isPhaseComplete)
{
print ("All turns are completed")
}
else {
//self.gameModel?.turnOrderManager.next()
self.gamePhase?.buy()
guard let playerOnTurn = self.gameModel?.turnOrderManager.playerOnTurn else {
print ("No player is on turn")
return
}
print ("\(playerOnTurn.name) is on turn")
if (playerOnTurn.isAI)
{
self.gamePhase?.buy()
self.finishTurn()
}
}
}
}
// EXAMPLE 1 -- first attempt ...
let vc = SomeViewController()
vc.viewDidLoad()
vc.buyButtonPressed()
// EXAMPLE 2 -- another attempt ....
let gameModel: GameModel = GameModel.init()
let gamePhase = Phase1State.init(gameModel: gameModel)
// player then takes an action
while (gameModel.turnOrderManager.allTurnsCompleted == false)
{
let turnIndex = gameModel.turnOrderManager.turnOrderIndex
let turnObj = gameModel.turnOrderManager.turnOrder[turnIndex]
guard let playerOnTurn = turnObj.player else {
break
}
print ("Player \(playerOnTurn.name) is on turn")
gamePhase.buy()
}
print ("All turns are completed, advance to next phase")
The issue is;
On the finishTurn, it only seems to work if it relies on the first player in the index is a human player. If its not, I have no idea how to make it fire the buy action.
On the second example, I use a loop; but I'm concerned using a loop could end up just looping forever.
My query is therefore clarifyed, how can I ensure my view controller will fire actions for AI players when they don't press buttons and loop through each player and execute their respective turn.
Many thanks
Further edit:
I do not know if I should have the while (gameModel.turnOrderManager.allTurnsCompleted == false) loop inside my viewDidLoad() to act like a game loop.
There is no need to specifically use Sprite Kit for this. SpriteKit would be more to do with how the UI is made rather than how the logic of the game works.
However, I would recommend looking at GameplayKit. It's a framework that contains lots of built in game logic tools. Specifically you want something like the GKDecisionTree. There are a few WWDC videos about it too. GameplayKit can be used with SpriteKit, UIKit, SSceneKit or any other game engine that you decide to use (or not).
Also, the question you're asking is a very general question about game development. Having the computer "decide" to do something is quite a complex subject.
I'd also suggest having a quick watch of this video from AI & Games and other videos from that channel.
It'll give you an idea of how to approach your problem.
Session 609 and 608 from WWDC 2015 and 2016 are prob good :D
Regarding updating the AI.
Your AI should be event driven. You have the concept of "turns" and "players". There is a point in the game at which it becomes a "player's" "turn". (Even at the very beginning of the game it is either Player 1 or Player 2's turn.
At this time there are two possibilities. Either the player is an AI, or the player is a person.
As soon as this happens there should be some sort of trigger (like a function call or something) that tells the player its turn has started.
If that player is the AI then you need to start some sort of calculation (maybe with a built in delay to make it realistic) so that it decides what to do.
Look, I'm not sure on what kind of game you're making, buy you should probably learn SpriteKit, specially SKActions. With that, you can easily control the flow of events from your game.
With that said, how is your AI implementation? Based on your code, I would begin with something like this:
class AI {
enum Decision {
case doSomething
case doAnotherThing
case dontDoAnything
}
public func decide() -> Decision {
// Decide which action the AI will take...
return .doSomething // This return is just a example!
}
public func act(on : Decision) {
// Do whatever the AI needs based on a decision...
}
}
Then, in your ViewController:
class SampleViewController: UIViewController {
var ai = AI()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buyBtnPressed(_ sender: UIButton) {
print ("pressed")
ai.act(on: ai.decide())
}
}
I hope that helps!