Chapter 3 Controlling and Scrolling
@implementationGameScene {
__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
ParameterstouchContains the touch.
eventCurrent event information.
DiscussionIf 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 AlsoCCTouch, CCTouchEvent
Declared InCCResponder.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.2position: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.2position: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规模的图片版本,可以适配retinaiphone和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 * 320CGSize 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 * 320CGSize 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 *layerin_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.2position:pos];
move.tag=1;
[_playerNode runAction:move];
}
试着计算
未完待续d