·您现在的位置: 云翼网络 >> 文章中心 >> 网站建设 >> app软件开发 >> IOS开发 >> SpriteBuilder学习笔记二

SpriteBuilder学习笔记二

作者:佚名      IOS开发编辑:admin      更新时间:2022-07-23

Chapter 3  Controlling and Scrolling

@implementation GameScene {
    __weak CCNode *_levelNode;
    __weak CCPhysicsNode *_physicalNode;
    __weak CCNode *_playerNode;
    __weak CCNode *_backgroundNode;
}

注意__weak关键字。总的来说,声明一个obejct pointer 变量而不是由类created 或者说owned的时候,最好都使用__weak,尤其是在cocos2d中,应该总是声明一个引用,当这个引用不是parent或者node的“兄弟”(sibling)时。如果没有__weak关键字,默认生成一个strong引用。

通过名字找到Player Node

在GameScene中添加代码:

- (void)didLoadFromCCB {
    NSLog(@"GameScene created!");
    // 使得可以接受输入的事件 (enable receiving input events)
    // 这句话允许GameScene类去接受触摸事件
    self.userInteractionEnabled = YES;
    // load the current level 载入当前level
    [self loadLevelNamed:nil];
}
- (void)loadLevelNamed:(NSString*)levelCCB {
    // 在scene中获取当前level的player,递归寻找
    _playerNode = [self getChildByName:@"player" recursively:YES];
    // 如果没有找到,NSAssert会抛出一个异常
    NSAssert1(_playerNode, @"player node not found in level:%@", levelCCB);
}

下面的代码用于实现通过触摸移动物体到触摸的位置

- (void)touchBegan:(CCTouch*)touch withEvent:(UIEvent*)event {
    _playerNode.position = [touch locationInNode:self];
}

NOTE:书中第一个参数类型为UITouch* 报错,改为CCTouch后即可实现功能。

查阅API,摘抄如下:

touchBegan:withEvent:

Called when a touch began. Behavior notes:

- (void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event

Parameters

touch

Contains the touch.

event

Current event information.

Discussion

  • If a touch is dragged inside a node which does not claim user interaction, a touchBegan event will be generated.
  • If node has exclusive touch, all other ongoing touches will be cancelled.
  • If a node wants to handle any touch event, the touchBegan method must be overridden in the node subclass. Overriding just touchMoved or touchEnded does not suffice.
  • To pass the touch further down the Cocos2D responder chain, call the super implementation, ie [super touchBegan:withEvent:].

See Also

  • CCTouch, CCTouchEvent

Declared In

CCResponder.h

 

分配Level-Node变量

在SPRiteBuilder中分配变量和通过名字获取一个node是一样的,仅仅是个人习惯问题。但是不推荐频繁使用getChildByName:方法在schedule methond中(不太懂这是什么方法)和updata:方法中,特别是递归查找和a deep-node hierarch。

Caution:在SpriteBuilder中分配一个变量仅仅适用于CCB文件的直接descendants(后代--不知如何翻译),不可以对通过Sub File(CCBFile)导入的另一个CCB指定node为变量或者properties。这也是为何player node通过名字获取。

打开GameScene.ccb,

 

note:A doc root var assigns a node to a correspondingly named ivar or property declared in the CCB root node's custom class

Doc root var:分配一个node,为在CCB根node的自定义类中声明的相对应名字的变量或者属性。

做完这步后,_levelNode变量会在它发送didLoadFromCCB消息之前被CCBReader分配,这是创建一个在CCB中包含的node的最简单,最有效的方法。

 

用CCActionMoveTo移动Player

为了平滑的移动player到指定位置,可以修改如下代码:

- (void)touchBegan:(CCTouch*)touch withEvent:(UIEvent*)event {
   // _playerNode.position = [touch locationInNode:self];
    CGPoint pos = [touch locationInNode:_levelNode];
    CCAction *move = [CCActionMoveTo actionWithDuration:0.2 position:pos];
    [_playerNode runAction:move];
}

触摸点根据_levelNode转化。这一点很重要,保证了player可以在整个_levelNode上移动,而不是被禁锢在屏幕空间中。但是这一点目前还看不出来,因为还没有添加滚动(scrolling)。

但是此时,如果增加duration(持续时间),会发现移动的动作并没有叠加,player也不会停在你最后一次点击的地方。所以必须添加一个tag,有了这个tag,可以在执行新的动作之前,停止当前动作,代码更改如下:

- (void)touchBegan:(CCTouch*)touch withEvent:(UIEvent*)event {
   // _playerNode.position = [touch locationInNode:self];
    [_playerNode stopActionByTag:1];
    CGPoint pos = [touch locationInNode:_levelNode];
    CCAction *move = [CCActionMoveTo actionWithDuration:20.2 position:pos];
    move.tag = 1;
    [_playerNode runAction:move];
}

 

滚动Level(Scrolling the Level)

在2D游戏中,更普遍的做法是相反方向移动content layer,已达到滚动效果。

在Cocos2D和OpenGl中,没有camera的概念,只有device screen(设备屏幕).

 

 Scheduling Updates(调度更新)

如果player移动到右边和上边,那我们要做的事情实际上是移动_levelNode向左边和下边方向移动。player的位置限定在level node中,左下角左边为(0,0),在这个程序中,范围是4000*500 points。

在GameScene中添加如下代码:

// the updata:method is automatically called once per frame
// update方法在每一帧都被自动调用
- (void)update:(CCTime)delta {
    // update scroll node position to player node, with offset to center player in the view
    [self scrollToTarget:_playerNode];
}

update:方法自动被Cocos2d调用,在底层,每一帧,node出现在屏幕之前,都回被调用。

不像之前的Cocos2d版本,你不再需要去明确调度更新(you no longer have to explicitly schedule the update:method.)

你可以使用node schedule和unschedule方法调度其他的方法或blocks.(you can still schedule other methods or blocks using the node schedule and unschedule methods)

例如:延迟运行一个selector,可以写为:

[self scheduleOnce:@selector(theDelayedMethod:)delay:2.5]:

然后再相同的类中实现对应的selector。这个selector必须使用一个CCTime参数:

-(void)theDelayedMethod:(CCTime)delta {

//your code

}

Caution:永远不要使用NSTimer等。这些时间方法在node或者Cocos2d暂停时候不会自动暂停。

delta参数是delta time,或者difference in time。

在60帧每秒时,delta时间经常取大约0.0167,单位是秒。

delta time通常用作以相同的速度移动nodes,而忽略帧速率。我们在这本书中不使用delta time,因为我们使用Cocos2d的物理引擎。

 

Moving the level Node in the Opposite Derection

向相反方向移动Level Node

在GameScene.m中添加scrollToTarget方法以完成滚动:

- (void)scrollToTarget:(CCNode*)target {
    CGSize viewSize = [CCDirector sharedDirector].viewSize;
    CGPoint viewCenter = CGPointMake(viewSize.width / 2.0,viewSize.height / 2.0);
    CGPoint viewPos = ccpSub(target.positionInPoints, viewCenter);
    CGSize levelSize = _levelNode.contentSizeInPoints;
    viewPos.x = MAX(0.0, MIN(viewPos.x, levelSize.width - viewSize.width));
    viewPos.y = MAX(0.0,MIN(viewPos.y, levelSize.height - viewSize.height));
    _levelNode.positionInPoints = ccpNeg(viewPos);
}

前两行的作用是指定view的尺寸到viewSize,值为屏幕以points为单位的值。

然后计算view的中心点。

viewPos变量被初始化为目标的positionInPoints减去中心点viewCenter。

这个使用ccpSub做的减法是为了保持目标node保持中心位置,如果不做这一步,目标node会消失在屏幕的左下角。

levelSize变量被定义为_lovelNode.contentSizeInPoints,在下面两行中,它用于夹住viewPos。

因为屏幕永远不应该比viewCenter滚动的更接近于level的边界,所以使用减法。每个边界的距离相加等于viewSize。或者换句话说,可以滚动的区域是viewCenter的两倍或者一个viewSize   ???

level区域和可滚动区域的关系图:箭头表示可滚动区域。注意player在接近level边界的时候已经不在中心位置了。

 

Parallax Scrolling 视差滚动

有很多种实现视差滚动的方法,最简单的方法是给每个layer不同的速度,并移动layers。但是这种方法有一个缺点,就是你永远也不可能知道每个layer到底需要多大,而且很难判断当player到达一个level中的点时,背景的哪一个部分会是可见的。

Working with Images

 

如果你只有2x规模的图片版本,可以适配retina iphone和non-retina ipad,你可以改变“Scale from”设置,从Default改变为2x。这不能起到节省内存的作用,SpriteBuilder会创建一个低规模的1x和一个高规模的4x版本。这意味着4x版本的图片和原始的2x版本的图片有一样程度的细节。你也可以为各种规模的图片采用不同的图片。强烈建议创建所有images。

SpriteBuilder给你两个选择:要么创建所有图片,4x和568x384,以便在ipads上运行,要么创建2x,568x320,然后只为了在iphone设备上运行。

举例来说,填满ipads Retina屏幕需要最少2048x1536个像素(defult 4x),如果是2272x1536更好,可以更好的覆盖4-inch的iphon屏幕。如果你的app只为了在iphone上运行,(对所有图片使用2x规模),那么覆盖整个retina屏幕的图片需要1136x640像素点(568X320 points)。但是如果你希望稍后添加ipad版本,那么就需要你为ipad retina屏幕设计你所有的图片了。

Project Setting

如果你开发一个仅在Iphone上运行的app,SpriteBuilder给了你改变默认4x规模的选择。File-Project-Setting dialog

 

如果你想你的iPad版本显示对应的更大的游戏世界,你可以改变“Default scaling from”,设定为2x(phonehd),注意设置不会应用在已经存在的图片上,只有伺候新的添加进SpriteBuilder的images会改变。

注意:Apple要求app开发者支持Retina。强烈建议使用设置:Scale from setting 1x for any images,因为在iPad Retina屏幕上的显示质量会很低。

设置:phone:仅在iPhone3GS上适用

     phonehd:在iPhone4和更新的设备上适用

   tablet:对非Retina ipads:Ipad1 和2,iPad mini1上适用

    tablethd:对iPad3和iPadmini2和更新的设备上适用

其他的选项:

    Audio quality:影响发布的音频文件的大小和质量。level 1创建最小但是质量最差的文件。

    Screen mode:主要的应用情况是当你的游戏仅仅是iPhone上运行时,并且你希望把3.5和4英寸iphone作为同一个设备,这种情况下可以考虑使用fixed mode,但是通常不建议使用因为这会使得屏幕的布局很困难。

   Orientation:横屏或者竖屏设置。

 

Adding Addition Background Layers

很明显,需要更多的layers去达到景深效果。距离观察者更近的layer,它的size就需要更大。

 

Prepare to Parallax in 3 2 1...

使得背景layers视差滚动需要一些初始化步骤。

首先,引进physics node,并使得player node变成physics node的子node。现在,physics node是level的内容容器,而不是level.ccb本身。

让player作为另一个node的子node的主要原因是为了能够在视差背景中独立移动level内容。如果继续使用Level.ccb做为player的父node,the changes made to the player's parent position would offset all of the level1.ccb child nodes,including the background .这会使得背景滚动的代码复杂得多,并且很难添加一个静止不动的node,比如暂停按钮。

NOTE:不能把player node拖拽到background node下。因为background node 是Sub File node,不可以接受子nodes。还有其他几类无法拥有子nodes的:Particle System,Label TTF, Label BM-Font,Button,Text Field,Slider,Scroll View。

现在需要分配CCPhysicsNode引用_physicsNode变量。因为getChildByName:返回一个CCNode类的引用,所以必须强转返回的node。

_physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO];

 

- (void)loadLevelNamed:(CCNode*) levelCCB {
    _physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO];
    _background = [_levelNode getChildByName:@"background" recursively:NO];
    _playerNode = [_physicsNode getChildByName:@"player" recursively:NO];
    NSAssert1(_playerNode, @"not found %@", levelCCB);
    NSAssert1(_physicsNode, @"not found %@", levelCCB);
    NSAssert1(_background, @"not found %@", levelCCB);
}

现在,需要对scrollToTarget方法进行修改:

_levelNode.positionInPoints = ccpNeg(viewPos);

改为

_physicsNode.positionInPoints = ccpNeg(viewPos);

这样,_backgroundNode的位置就会被解放,可以根据_physicsNode的位置独立的更新。

现在,scrollToTarget方法的代码为:

- (void)scrollToTarget:(CCNode*)target {
    // 屏幕大小 480 * 320
    CGSize viewSize = [CCDirector sharedDirector].viewSize;
    // player的中心位置 (240,160)
    CGPoint viewCenter = CGPointMake(viewSize.width / 2.0, viewSize.height / 2.0);
    // levelNode的size 4000 * 500;
    CGSize levelSize = _levelNode.contentSizeInPoints;
    //
    CGPoint viewPos = ccpSub(target.positionInPoints, viewCenter);
    viewPos.x = MAX(0.0, MIN(viewPos.x, levelSize.width - viewSize.width));
    viewPos.y = MAX(0.0, MIN(viewPos.y, levelSize.height - viewSize.height));
    _physicsNode.positionInPoints = ccpNeg(viewPos);
}

现在,必须获取每个背景layers在_physisNode的位置更新时视差滚动的位置。

因为你想要每个layers的位置对应_physicsNode的位置,(在这个方法中是viewPos),那么考虑到viewPos(view的中心)应该与level的边界保持一定的距离就很重要了。这个最小的距离必须至少在水平方向是viewCenter.width,在垂直方向是viewCenter.height.这样才可以阻止可视区域出现在level边界的外面.

这幅图可以帮助理解,想象viewPos是每个Viewable Area的中心,那么实际的Scrollable Area矩形(比如,viewPos合法位置)必须比Level Area的左下角大,必须比右上角小。

这样,每个背景layer相对应的位置不能和整个尺寸的level相比,也就是4000x500个points。

例子:在Iphone5上,viewCenter是284x160.这样的话,scrollable area就是:

284x160 到( 4000 - 284) x (500 - 160) = 3716 x 340 points.

换句话说,这个Scrollable Area是level的尺寸减去view的尺寸。这样,通过scrollable area(levelSize - viewSize)分割viewPos给了你_physicsNode当前位置在scrollable area上的百分比:

CGPoint viewPosPercent = CGPointMake(viewPos.x / (levelSize.width - viewSize.width),viewPos.y / (levelSize.height - viewSize.height));

现在,得到了_physicsNode的位置的范围(0.0到1.0之间),0.0指的是scrollalbe area的左下角的位置,284x160,1.0指的是右上角的位置 3716x340.下一步必须运用这个百分比在每一个layer上,把每个layer自己的尺寸算进去。

试着计算: 使用568x384作为layerSize的宽和高,并且用568x320作为viewSize的宽和高,计算当viewPosPercent是0.5,0.5的时候,layerPos是多少。

- (void)scrollToTarget:(CCNode*)target {
    // 屏幕大小 480 * 320
    CGSize viewSize = [CCDirector sharedDirector].viewSize;
    // player的中心位置 (240,160)
    CGPoint viewCenter = CGPointMake(viewSize.width / 2.0, viewSize.height / 2.0);
    // levelNode的size 4000 * 500;
    CGSize levelSize = _levelNode.contentSizeInPoints;
    //
    CGPoint viewPos = ccpSub(target.positionInPoints, viewCenter);
    viewPos.x = MAX(0.0, MIN(viewPos.x, levelSize.width - viewSize.width));
    viewPos.y = MAX(0.0, MIN(viewPos.y, levelSize.height - viewSize.height));
    _physicsNode.positionInPoints = ccpNeg(viewPos);
    CGPoint viewPosPercent = CGPointMake(viewPos.x / (levelSize.width - viewSize.width),viewPos.y / (levelSize.height - viewSize.height));
    for (CCNode *layer in _backgroundNode.children) {
        CGSize layerSize = layer.contentSizeInPoints;
        CGPoint layerPos = CGPointMake(viewPosPercent.x * (layerSize.width - viewSize.width), viewPosPercent.y * (layerSize.height - viewSize.height));
        layer.positionInPoints = ccpNeg(layerPos);
    }
}
- (void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event {
    //_playerNode.position = [touch locationInNode:self];
    [_playerNode stopActionByTag:1];
    CGPoint pos = [touch locationInNode:_physicsNode];
    CCAction *move = [CCActionMoveTo actionWithDuration:0.2 position:pos];
    move.tag = 1;
    [_playerNode runAction:move];
}

 

试着计算

 

未完待续d