本文只是对那篇老外博客进行了翻译,翻译的不好,自己刚过六级.肯定有很多问题,欢迎同学指出.
原文链接:http://www.paradeofrain.com/2010/07/lessons-learned-in-tilt-controls/
回顾一下
我想简单粗糙的讲讲TtL(游戏名)实现的过程。这不是后期分析,但是读者可以因此了解Ttl的实现和代码是如何慢慢改进的。Ttl是我做的移动平台的第一个app程序。第一次使用mac,第一次用XCode, 第一次写Objective-C.有太多的第一次。通常,当我学东西的时候,我不会因为风格,最优化,或者平台的特殊规定等而烦恼。但当我开始学这个的时候,我就把这些都给学了。Ttl从第一次的改进到现在,已经花了很长时间。
这是以前的版本
在那时的Ttl中,我们把绿色的点叫做核弹,
现在
游戏很美观,要多谢谢Adam强大的美工(3A质量)。代码却有时感觉像大丑兽。通常,我会这样来写程序,先写一个原型,然后重新开始,对代码进行重构,重新编写,这样使得代码容易维护。然而Ttl不是这样的,这个游戏的改进是直接从原型改成了产品级代码。原始代码经历了大量的重写,重构,和一些补丁,使得代码开始变得健壮起来。好的是,我现在对可维护性代码不应该是什么样的有了更清楚的认识,同时在编码过程中,懂得更多设计架构,可以避免代码以后出现混乱。
要明确指出的是,因为要把它做成一个产品,所以最后,只要游戏有趣,没有人会骂它(除了作者)
Tile 控制中的设计问题
在操控设计中,我们当前关注的主要重点是如何为用户实现校准功能。整体上,我们对增加游戏用户的体验有个非常简单的方法。我们需要让用户进入游戏花上最少的点击次数,一旦进入游戏,你所做的就是倾斜设备开始游戏。这样看来,让校准自动进行看起来会是最好的用户体验。用户甚至不知道自己做了什么,只知道这只是个游戏。那么如何初始化校准工作呢?当游戏计数”3,2,1”,“1”消失时,游戏就在这个时间点即时在平衡位置进行了校准。同样,当玩家取消暂停游戏,校准也会执行。听起来不错,玩起来更好…如果你知道它是如何工作的。从游戏测试中我们发现:
用户可能会迷惑,当他们改变他们玩的姿势时没有暂停游戏,校准没有生效。这是一个使他们困惑的情况。
有很多的聪明玩家会马上找到校准的选项,因为他们在以前的iphone游戏中用到过这种功能。如果没有找到校准选项,他们会因为不能按照他们喜欢的方式去打游戏而感到不爽。
在更多地社会环景中,而不是单独的游戏测试,那里游戏可以玩的很随意。这可能会破坏我们的自动校准功能。玩家可能把iphone朝下,与同事交谈时又把iphone摆到自己旁边,当游戏计数开始时,又马上把iphone向后转动后再玩,这个时候校准就乱了。这会对用户产生不好的印象。
那么我们是否要在玩家玩之前告诉他通过暂停和开始游戏就可以对游戏重新校准?我们快速模拟了客户的反应.
对那些知道校准功能的用户,他们可以立即的理解.
对其他人,他们很可能会说:"校准是什么意思?".是的,全都带着不好的口音.
好吧,因为我们要对那些最幼稚的iphone玩家有吸引力,做了以下测试:
1. 针对自动校准:没有合适的反馈,用户可能会错误的设置校准,使他们认为这个游戏出问题了.
2. 针对用户自定义的校准:有些用户可以很好的使用,但是对用户的无数种情况,很验难预料到不怎么玩游戏的玩家会知道校准功能.
3. 一般的情况:有经验的用户会比较喜欢校准,甚至要求更多的功能.用户要玩你的游戏,他要做的决定越少,就越喜欢玩.这是在移动领域中很重要的概念,特别是像加速度计这些不精确的物理输入.
4. 尤其在Ttl的设计方面:TtL的游戏设置需要精确的tilt控制,否则,游戏就会失败.其它的不怎么需要精确控制的游戏,则可以使用自动校准.
因此,通过集思广益,想到了很多种校准界面.相比其它基于tilt操控的游戏,Adam在那时想到了一个独一无二的好主意.
我们认为这是一个好主意,同时测试也很好,我们没有想到目前的这个选择跟用户会产生更多的共鸣.
“这是我发现的自定义tilt 控制最好的选项.”
“那些害怕手动校准的人,现在不用怕了”
“游戏的不同之处在于他的校准选项,提供了三个默认操控,这样可以满足大多数人的游戏姿势.”
玩家和评论者对我们的方法都给予了积极的反馈.regular的设置功能可以满足大多数的游戏情况,top_down也更能满足游戏爱好者.sleepy是有很多种可能的姿势.比如我吧,如果你让一组用户用sleepy的姿势来玩游戏,你可能会得到一个比regular, top_down姿势更大的方向方差.我不认为现在的tilt control比下一代游戏的要好,但是我感觉,让用户更少的去选择就能得到更好的精确值,那么他们就会更加积极.
Tilt控制的技术问题
除了设计上的问题,在实现tilt控制的技术上,我有很多问题要克服.第一个也是最大的问题是如何计算tile的偏移限制.当iphone的校准出现完全竖直的情况时,那么所能检测到的水平移动几乎没有.为了解决这个问题,我加了一个控制方案,当设备在这个位置时玩家通过steering来控制游戏中的飞船,而不是倾斜设备来控制.这不是好的情况,但是随着iphone4陀螺仪的出现,这也许可以缓解.已经有很好的方法来实现3G/3GS上的tilt控制,所以这不是一个问题,我没有太多时间来做这个实验.当我做这些实验的时候,我会发布更新的.
第二个问题是加速度计数据的噪音.我按照Apploc文档的说明,对原始输入值进行了过滤.但是最后,我还是让输入值没有进过过滤,取而代之的是,只改变了飞船的显示.如果飞船在高速运行,通过原始输入值来改变飞船的方向显示,会引起不可控的抖动.这就不可能使用那些要瞄准的武器.要解决这个基本问题,我加了一个插值函数,可以使飞船从当前方向到目标方向有一个几帧的缓冲,同时,加了一个盲区,可以让玩家坐着玩的很好,而不会产生游戏内飞船时时刻刻旋转的问题.我已经玩了很多基于titl操控的游戏,这些游戏都没有实现盲区的功能.对玩家更好的是,至少加个该死的盲区吧.
第三个但是比较小的问题就是采样率.Sss我设置的加速度计的采样率是 30hz,那么Ttl运行也是30FPS.但FPS与加速度计的数据有点不同步,你会看到加速度数据会从几帧中获得,因而有点细小的变化.Ttl没有做太多管理不同帧时间的工作(不好意思),但是这个问题也没有带来太多的不好,因此我没有觉得这个问题值得修.
是如何真正工作的?
对一直读下去的读者们,都会很想知道这是怎么实现的,我添加了一个XCode工程来实现.我写了一个GLSprite demo程序,把其中的功能进行了分解,使它满足了一个简单tile控制 的样例.我的这个方法就是写了一个函数,用来实现 反转操作杆 的功能.我不会试着去解释WTF的意思,通过下面简单的绘画来描述.
我通过对加速度计数据进行插值,使它转向的向量可以一直指向远离设备屏幕的方向.我通过这样,找到这个向量和’neutral’位置(blue)的差,最后投影在屏幕的2D平面上,返回一个速度值.绿色的箭头就是玩家速度的方向.那么,当neutral位置和反转的加速度数据很接近时,玩家的速度值会趋近于0(盲区).
你可以从这个案例程序中发现,所有的加速度计算的逻辑代码都在TiltPlayer类的'accelerometer:didAccelerate"方法里实现.在TiltPlayer的update方法,做了对图标方向的平滑处理,减少了抖动.其它的都是渲染程序或其它引用程序.因此这里贴出了与本文相关的代码,不用打开XCode工程去查看了.
样例程序下载链接:http://www.paradeofrain.com/downloads/Tilt_Controls
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { /* while not ideal, most of the relevant code is stuffed in this method for clarity. A lot can be computed once and saved in instance variables, preferences, etc instead of re-calculating each frame */ // hard coding our 'neutral' orientation. By default this is the orientation of // having the device tilted slightly towards your face. // if you wanted strictly a 'top down' orientation then a (0,0,-1) vector would be put here. // to create a new 'neutral' position you can sample the UIAcceleration parameter for a single // frame and set that to be the new nx,ny,nz for the remainder of the app (aka calibrating). const float nx = -0.63f; const float ny = 0; const float nz = -0.92f; // this quaternion represents an orientation (I like to think of it as a 3D vector with 0 rotation in this case) // that points straight out from the device's back away from the user's face. Quaternion neutral(0,nx, ny, nz); // take a straight up vector and rotate it by our 'neutral' orientation // to give us a vector that points straight out from the device's screen (at the user's face) Quaternion neutralPosition = neutral * Quaternion(0,0,0,1); /* now with our 'neutral' quaternion setup we: 1. take the incoming accelerometer data 2. convert it to a 2D velocity projected onto the plane of the device's screen 3. and rotate it by 90 degrees (since we are landscape oriented) and feed it to our player's velocity directly. */ // convert our accel data to a Quaternion Quaternion accelQuat(0, acceleration.x, acceleration.y, acceleration.z); // now rotate our accel data BY the neutral orientation. effectively transforming it // into our local space. Vec3 accelVector = (accelQuat * neutralPosition).v; // we only want the 3D vector at this point // now with our accel vector we wish to transform it into our standard (1,1,1) coordinate space Vec3 planeXAxis(1,0,0); Vec3 planeYAxis(0,1,0); Vec3 normal(0,0,1); // the normal of the plane we wish to transform our data into. //project this movement onto our X/Y plane by removing // the accel part that is along our normal // note: Vec3 * Vec3 = dot product of the 2 vectors. Vec3 projection = accelVector - normal *(accelVector * normal); // now decompose that projection along our X and Y axis that represents our 2D plane Vec2 accel2D(0,0); accel2D.x = planeXAxis * projection; accel2D.y = planeYAxis * projection; const float xSensitivity = 2.8f; const float ySensitivity = 2.8f; // yay magic numbers! const float tiltAmplifier = 8; // w0ot more magic numbers // now apply it to our player's velocity data. // we also rotate the 2D vector by 90 degrees by switching the components and negating one // since we are in a landscape orientation. vx += (-accel2D.y) * tiltAmplifier * xSensitivity; vy -= accel2D.x * tiltAmplifier * ySensitivity; // we do a (-) here because the accel y axis is inverted. }
多谢Mikko Mononen,使代码更加简洁,下面是Mikko mononen改进后的代码.
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { // A much more concise version courtesy of Mikko Mononen http://digestingduck.blogspot.com/ Vec2 accel2D(0,0); Vec3 ax(1, 0, 0); Vec3 ay(-.63f, 0,-.92f); Vec3 az(Vec3::Cross(ay,ax).normalize()); ax = Vec3::Cross(az,ay).normalize(); accel2D.x = -Vec3::Dot(Vec3(acceleration.x, acceleration.y, acceleration.z), ax); accel2D.y = -Vec3::Dot(Vec3(acceleration.x, acceleration.y, acceleration.z), az); const float xSensitivity = 2.8f; const float ySensitivity = 2.8f; // yay magic numbers! const float tiltAmplifier = 8; // w0ot more magic numbers // since we are in a landscape orientation. // now apply it to our player's velocity data. // we also rotate the 2D vector by 90 degrees by switching the components and negating one vx += -(accel2D.y) * tiltAmplifier * xSensitivity; vy += accel2D.x * tiltAmplifier * ySensitivity; }
秀才坤坤出品