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,摘抄如下:
Called when a touch began. Behavior notes:
- (void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event
Contains the touch.
Current event information.
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
新闻热点
疑难解答