Introduction to Apple's 2D engine SpriteKit in ios development, iosspritekit
In ios development, Apple's 2D engine SpriteKit is introduced. Recently, Apple's own 2D engine SpriteKit and 3D engine SceneKit have been studied. Before the start, we need to explain objectively, if you are engaged in a team or company project or directly engage in unity, this involves the development and maintenance costs. After all, SpriteKit currently cannot provide cross-platform support. However, if you are an independent developer interested in Apple's native framework, or you only pay attention to Apple's App Store, I think SpriteKit and SceneKit are also a good choice.
Sprite is translated as an genie. In the SpriteKit world, monsters in the game are an genie, and shells fired by the protagonist and the protagonist are also an genie, or a non-dynamic background image in the game can also be an genie. The following uses the genie as the starting point to explain a ambitious 2D engine developed by Apple.
I. Genie and scenarios
1. Create an Xcode project. You can see that, whether it is iOS, macOS, or even tvOS, there is a project creation method called Game. We chose iOS Game to create a new Game project. What is the difference between Game and common engineering projects? In fact, the default GameViewController view of the Project is loaded in the form of skView. Next we will create a new Game scenario SKScene, used to load the new genie.
2. Create a scenario.
Note: The starting coordinate of the Scene scenario is (0, 0) at the lower left corner, rather than the upper left corner of the view.
-(MenuScene *) menuScene {
if (! _menuScene) {
_menuScene = [[MenuScene alloc] initWithSize: self.view.bounds.size];
// @ interface MenuScene: SKScene
// @ end
}
return _menuScene;
}
-(void) viewDidLoad {
[super viewDidLoad];
[(SKView *) self.view presentScene: self.menuScene];
// Do any additional setup after loading the view, typically from a nib.
}
@implementation MenuScene
-(instancetype) initWithSize: (CGSize) size {
if (self = [super initWithSize: size]) {
#if TARGET_OS_IPHONE
#define SKColor UIColor
#else
#define SKColor NSColor
#endif
// SKColor is mainly compatible with NSColor of mac and UIColor of iOS
self.backgroundColor = [SKColor whiteColor];
[self addChild: self.titleNode];
[self addChild: self.pathLabelNode];
[self addChild: self.collLabelNode];
[self addChild: self.physLabelNode];
[self addChild: self.physCollNode];
}
return self;
}
3. Switching of game scenes. Consistent with the initial page, the game's transition is to use presentScene to jump to the newly created Scene.
-(SKLabelNode *) titleNode {
if (! _titleNode) {
_titleNode = ({
SKLabelNode * labelNode = [SKLabelNode labelNodeWithFontNamed: @ "Chalkduster"];
labelNode.text = @ "SpriteKit Test";
labelNode.fontSize = 30;
labelNode.fontColor = [SKColor blueColor];
labelNode.position = CGPointMake (CGRectGetMidX (self.frame), self.frame.size.height * 0.75);
labelNode.name = labelNode.text;
labelNode;
});
}
return _titleNode;
}
-(void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event {
for (UITouch * touch in touches) {
CGPoint location = [touch locationInNode: self];
SKNode * node = [self nodeAtPoint: location];
[self changeToGameSceneWithNodeName: node.name];
}
}
-(void) changeToGameSceneWithNodeName: (NSString *) nodeName {
NSLog (@ "nodeName =% @", nodeName);
if ([nodeName isEqualToString: self.pathLabelNode.name]) {
PathScene * pathScene = [PathScene sceneWithSize: self.size];
// Custom transition type
SKTransition * reveal = [SKTransition revealWithDirection: SKTransitionDirectionUp duration: 0.5];
[self.scene.view presentScene: pathScene transition: reveal];
}
else if ...
4. New and added wizard. There are two ways to create a sprite, one is to create it directly in the form of a picture, its size is the size of the picture by default, and the other is to create it in the form of texture.
// New from picture
-(SKSpriteNode *) player {
if (! _player) {
_player = [SKSpriteNode spriteNodeWithImageNamed: @ "player"];
_player.name = @ "player";
_player.position = CGPointMake (self.size.width / 2, self.size.height / 2);
}
return _player;
}
// New with texture
-(SKSpriteNode *) walkMan {
if (! _walkMan) {
_walkMan = [SKSpriteNode spriteNodeWithImageNamed: @ "walkR01"];
_walkMan.name = @ "walkMan";
_walkMan.position = CGPointMake (self.player.position.x, CGRectGetMaxY (self.player.frame) + 30);
SKTexture * texture1 = [SKTexture textureWithImageNamed: @ "walkR01"];
SKTexture * texture2 = [SKTexture textureWithImageNamed: @ "walkR02"];
SKTexture * texture3 = [SKTexture textureWithImageNamed: @ "walkR03"];
SKTexture * texture4 = [SKTexture textureWithImageNamed: @ "walkR04"];
SKTexture * texture5 = [SKTexture textureWithImageNamed: @ "walkR05"];
SKAction * animation = [SKAction animateWithTextures: @ [texture1, texture2, texture3, texture4, texture5] timePerFrame: 1];
SKAction * action = [SKAction repeatActionForever: animation];
[_walkMan runAction: action];
}
return _walkMan;
}
5. Add movement events of sprites
-(void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event {
for (UITouch * touch in touches) {
// Add weapons
SKSpriteNode * arms = [SKSpriteNode spriteNodeWithImageNamed: @ "projectile"];
arms.position = self.player.position;
[self addChild: arms];
CGPoint location = [touch locationInNode: self];
// straight track
// SKAction * moveToAction = [SKAction moveTo: location duration: 0.5] ;;
// Continued to increase
// SKAction * moveByAction = [SKAction moveByX: 100 y: 100 duration: 0.3];
// change the size
// SKAction * sizeAction = [SKAction resizeByWidth: arms.size.width * 1.5 height: arms.size.height * 1.5 duration: 0];
// rotate
// SKAction * radiansAction = [SKAction rotateByAngle: M_PI * 4 duration: moveToAction.duration];
// sound effects
// SKAction * armsSound = [SKAction playSoundFileNamed: @ "pew-pew-lei.caf" waitForCompletion: NO];
SKAction * armsAction = [SKAction group: @ [pathAction, sizeAction, armsSound]];
[arms runAction: armsAction completion: ^ {
// remove
[arms removeFromParent];
}];
}
}
Second, the contact detection of elves
SKScene has a scene method, which will be triggered once every frame for simple event analysis and monitoring, such as sprite destruction and destruction, sprite contact monitoring, and storyboard score update.
/ **
Override this to perform per-frame game logic. Called exactly once per frame before any actions are evaluated and any physics are simulated.
@param currentTime the current time in the app. This must be monotonically increasing.
* /
-(void) update: (NSTimeInterval) currentTime;
-(void) update: (CFTimeInterval) currentTime {
// Remove monsters and weapons out of bounds
// Update the digital version
// Detect sprite events (for example, click on the sprite, set a logo for it, and do event processing in the next frame)
}
Note: The physical collision of sprites is described later, which can more accurately detect the collision of sprites
Third, the elf physics engine
// square
-(SKSpriteNode *) square {
if (! _square) {
_square = [SKSpriteNode spriteNodeWithImageNamed: @ "square"];
_square.position = CGPointMake (self.size.width * 0.8, CGRectGetMidX (self.frame));
_square.name = @ "square_prey";
_square.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize: _square.size];
}
return _square;
}
// round
-(SKSpriteNode *) circle {
if (! _circle) {
_circle = [SKSpriteNode spriteNodeWithImageNamed: @ "circle"];
_circle.position = CGPointMake (self.size.width * 0.65, CGRectGetMidX (self.frame));
_circle.name = @ "circle_prey";
_circle.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: _circle.size.width / 2];
}
return _circle;
}
// triangle
-(SKSpriteNode *) triangle {
if (! _triangle) {
_triangle = [SKSpriteNode spriteNodeWithImageNamed: @ "triangle"];
_triangle.position = CGPointMake (self.size.width * 0.5, CGRectGetMidX (self.frame));
_triangle.name = @ "triangle_prey";
CGMutablePathRef trianglePath = CGPathCreateMutable ();
// center
CGPathMoveToPoint (trianglePath, nil, -_triangle.size.width / 2, -_triangle.size.height / 2);
//
CGPathAddLineToPoint (trianglePath, nil, _triangle.size.width / 2, -_triangle.size.height / 2);
CGPathAddLineToPoint (trianglePath, nil, 0, _triangle.size.height / 2);
CGPathAddLineToPoint (trianglePath, nil, -_triangle.size.width / 2, -_triangle.size.height / 2);
_triangle.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath: trianglePath];
CGPathRelease (trianglePath);
}
return _triangle;
}
4. Physical collision detection of elves
-(instancetype) initWithSize: (CGSize) size {
if (self = [super initWithSize: size]) {
self.backgroundColor = [UIColor whiteColor];
[self addChild: self.back];
[self addChild: self.square];
[self addChild: self.circle];
[self addChild: self.triangle];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect: self.frame];
self.scene.name = @ "self";
self.physicsBody.categoryBitMask = 0x00000001;
self.physicsBody.collisionBitMask = 0x00000001;
self.physicsBody.contactTestBitMask = 0x00000001;
self.physicsWorld.contactDelegate = (id) self;
}
return self;
}
-(void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event {
for (UITouch * touch in touches) {
CGPoint location = [touch locationInNode: self];
// Get the clicked SKNode
SKNode * node = [self nodeAtPoint: location];
// Create a new black ball
Node * ball = [SKSpriteNode spriteNodeWithImageNamed: @ "blackBall"];
ball.position = CGPointMake (0, 0);
ball.name = @ "ball";
ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius: ball.size.width / 2];
CGPoint offset = CGPointMake (location.x-ball.position.x, location.y-ball.position.y);
// slope
float ratio = (float) offset.y / (float) offset.x;
// speed
ball.physicsBody.velocity = CGVectorMake (1000, 1000 * ratio);
// angular velocity radians / second
ball.physicsBody.angularVelocity = M_PI * 4;
// density
ball.physicsBody.density = 100;
// Elasticity
ball.physicsBody.restitution = 1;
// Momentum / kg
ball.physicsBody.mass = 100;
// Smoothness 0 ~ 1
ball.physicsBody.friction = 0.5;
// Whether affected by gravity default value is YES
ball.physicsBody.affectedByGravity = NO;
// Whether affected by acceleration
ball.physicsBody.allowsRotation = NO;
// Linear damping (0: the speed never decreases; 1: the speed decreases immediately)
ball.physicsBody.linearDamping = 0.5;
// Angular velocity damping (0: the velocity never decreases; 1: the velocity decreases immediately) default 0.1
ball.physicsBody.angularDamping = 0;
// Type of object (a hexadecimal number)
ball.physicsBody.categoryBitMask = 0x00000001;
// Set which object can't collide with it (i.e. impenetrable)
ball.physicsBody.collisionBitMask = 0x00000001;
// contact (trigger detection function)
ball.physicsBody.contactTestBitMask = 0x00000001;
[self addChild: ball];
}
}
-(void) didBeginContact: (SKPhysicsContact *) contact {
NSLog (@ "The first object in contact:% @", contact.bodyA.node.name);
NSLog (@ "The second object in contact:% @", contact.bodyB.node.name);
NSLog (@ "Contact point coordinates:% @", NSStringFromCGPoint (contact.contactPoint));
NSLog (@ "normal vector of collision direction:% @", NSStringFromCGVector (contact.contactNormal));
NSLog (@ "Collision intensity of two objects (Newton per second):% f", contact.collisionImpulse);
if ([contact.bodyA.node.name containsString: @ "prey"] && [contact.bodyB.node.name isEqualToString: @ "ball"]) {
[contact.bodyA.node removeFromParent];
}
if ([contact.bodyB.node.name containsString: @ "prey"] && [contact.bodyA.node.name isEqualToString: @ "ball"]) {
[contact.bodyB.node removeFromParent];
}
}
@end