65.9K
CodeProject 正在变化。 阅读更多。
Home

打砖块

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2012 年 7 月 25 日

BSD

10分钟阅读

viewsIcon

43852

downloadIcon

1790

本文介绍如何使用一份代码为 iOS、Android 和 Bada 创建 Breakout 游戏。


目录  

简介 

Breakout 是一款著名的街机游戏,于1976年5月13日由 Atari 公司首次推出。截至今年,这款游戏已有许多不同版本的不同平台。去年 Atari 推出了适用于 Apple 移动设备的 Breakout Boost 游戏。然而,现在我们为您提供如何为目前最常用的平台:iOS、Android 或 Bada 创建此游戏的说明。 

游戏概述  

游戏的主要玩法是用一个球击落屏幕顶部五行中的所有砖块。球开始时会获得一定的速度,然后被屏幕底部的挡板反弹。为了让游戏更有趣,此实现包含四个关卡,随着玩家在游戏中前进,砖块会停留更长时间。当球击中第一关的砖块时,它会消失。当它击中第二关的砖块时,它会变成第一关的砖块,依此类推。

类型 1 - 当砖块被球击中时,它会消失
类型 2 - 当砖块被球击中时,它会变成砖块类型 1
类型 3 - 当砖块被球击中时,它会变成砖块类型 2
类型 4 - 当砖块被球击中时,它会变成砖块类型 3

系统要求

正如我之前提到的,我们的游戏运行在所有常用平台(根据 G1 2012 的统计数据)。要使用一份代码创建运行在三个平台上的游戏,我们使用 Moscrif SDKMoscrif SDK 的硬件要求非常低。即使是仅配备 420MHz 处理器的设备也能运行。这意味着我们的游戏将运行在所有带有 iOS、Android 或 Bada 的常用设备上。

用户界面与图形

游戏发生在太空中,配有合适的动画(闪电效果),从而增强了用户体验。游戏由两部分组成:游戏部分和菜单。两者都被创建为 Moscrif 的独立 Scene 类。

菜单提供了所有必要的操作,如开始新游戏、继续之前开始的游戏和退出游戏。由于 Apple 的应用政策,iOS 版本中没有退出按钮。

游戏场景只有一个返回菜单场景的按钮。游戏的其余组件是砖块、球和挡板。

图片:图形提案 

物理引擎 

游戏中的球遵循物理定律运动。它获得一定的速度,然后从挡板反弹。游戏中也应用了少许重力。为了模拟这种物理行为,我们使用了 Moscrif SDK 支持的box2d 物理引擎。该引擎还可以在其他平台,如Nintendo DS 或 Wii 上看到。

世界

box2d 物理引擎最重要的特点是世界和物理体。世界为所有物理体创建背景。世界可以有重力,重力可以在两个轴(x、y)上指定。世界始终具有宽度,相当于现实世界中的10 米。世界中的所有对象都会被缩放以确保这个宽度。比例因子表示一米有多少像素。box2d 还使用自己的坐标系,该坐标系从左下角开始,以米为单位。


幸运的是,Moscrif 框架通常使用正常的像素坐标,并且转换会自动完成。

物理体 

物理体代表在世界中相互作用的所有实际对象/事物。有三种类型的物理体,它们的行为不同。 

  • #static(静态): 静态物理体在模拟下不移动,并且具有无限质量。静态物理体可以通过 setPosition 方法手动移动。静态物理体速度为零,并且不与其他静态或运动学物理体碰撞
  • #kinematic(运动学): 运动学物理体根据其速度在模拟下移动。运动学物理体不受力影响。它们可以被用户手动移动,但通常运动学物理体由需要设置的速度来移动。运动学物理体具有无限质量,但它不与其他静态或运动学物理体碰撞。
  • #dynamic(动态): 动态物理体被完全模拟。它可以通过 setPosition 手动移动,但通常根据受到的力移动。动态物理体可以与所有类型的物理体碰撞,并且始终具有有限的、非零的质量。如果您尝试将动态物理体的质量设置为零,它将自动获得一公斤的质量。

正如您所见,并非每个物理体都与其他物理体碰撞。下表显示了哪些物理体相互碰撞。 

物理体类型#静态#动态#运动学
#静态
#动态
#运动学


相互碰撞
不相互碰撞

物理体有许多影响其物理行为的属性。直接影响物理体行为的三个属性是弹跳、密度和摩擦

密度是一个影响物理体质量的属性。

摩擦是抵抗相对运动的元素相互滑动的作用力。

弹跳属性影响弹跳的大小。值为 0.0 意味着物理体不会从其他物理体上弹起。值为 1.0 意味着物理体将以与落下的相同速度弹起;然而,有可能增加弹跳率,球会以更大的速度弹起。

开发过程  

游戏位于游戏场景中。游戏场景继承自PhysicsScene,它创建了 box2d 世界。如果通过 create 函数创建了 PhysicsScene 的实例,则世界会自动创建,否则必须在 init 函数中手动创建。在 Moscrif 中,box2d 世界类的所有成员都映射到 PhysicsScene 类。这意味着 Scene 表现得像 box2d 世界。

砖块 

砖块由 Brick 类表示。它继承自 PhysicsSprite 类,这意味着它们表现得像 box2d 物理体。砖块的物理体类型设置为静态。这意味着它与其他动态物理体(球)交互,但不会根据重力移动。砖块有四种状态(等级)。当球击中砖块时,等级会降低。所有物理属性都在 init 函数中设置。物理体摩擦和密度为零。静态物理体的密度始终为零(无论如何设置)。弹跳属性影响从砖块上的弹跳,仅设置为 0.1,因为球的弹跳已经足够大(1.0 - 完全弹跳)。

当砖块被击中时,砖块会降级。如果砖块的等级小于 1,砖块就会消失。 

示例 hit 函数。降低状态或禁用砖块

function hit()
{
    if (this.disabled == false)
        // disable brick if tis state (level) is less then one
        if (this.state < 1) {
            this.disabled = true;
            this.scene.brickCount--;
            this.hide(this);
        // decrease the brick's state (level)
        } else {
            this.state -= 1;
        }
}

砖块通过平滑动画显示和隐藏。它只改变 alpha 值。动画由 Animator 类创建。Animator 类实现了动画的所有数学背景。使用此类,动画不必具有线性行为。根据所需行为,Animator 对象调用 addSubject 函数指定的调用函数。调用函数只有一个参数:state,它指定动画中的当前位置。

Moscrif 在 transition 属性中提供了许多各种行为。在 show 函数中,创建了一个持续 500ms 的 animator,并使用由 easyIn 标识的 transition。它开始时缓慢,然后在动画结束时快速加速。

示例: 以平滑动画显示砖块

// animate brick wehn it is shown
function show(obj)
{
    // create animator
    var animator = new Animator({
        transition: Animator.Transition.easeIn,     // start up slowly and then quickly speed up at the end of the animation
        duration: 500,                              // length of animation in miliseconds
    });
    animator.addSubject(function(state) {           // state starts from 1.0 to 0.0
        obj._paint.alpha = 255-(state*255).toInteger();
    });
   animator.reverse();
}

然而,easyIn 并不是唯一的 transition 类型。Moscrif 提供了许多其他类型。它们的行为显示在接下来的三张图中。

图片: Animator 转换类型




挡板 

挡板用作反弹球以击中砖块的板。它也作为从 PhysicsSprite 继承的独立类创建。它的类型是静态的,因为它也不会根据世界的重力和其他力移动。当用户在屏幕上拖动手指时,它通过 setPosition 函数手动移动。为了让游戏更有趣,挡板与闪电动画连接。它只有三个帧,但 fps 设置为 10,因为闪电效果通常很短。真实的闪电不是平滑动画。

图片: 动画帧

帧在定时器类使用的时间间隔内定期更改。帧在 _electricShock 函数中更改,该函数会在定时器滴答事件上循环调用。在第一帧和最后一帧之间的效果间隔为 100ms。第一帧和最后一帧之间有一个更长的时间延迟。 

示例: 创建闪电动画

// create electric animation
function _eletricShock(frame = 0)
{
    // change current frame
    this.frame = frame;
    frame++;
 
    // if animation is on the last frame move to the first
    if (frame == 3)
        frame = 0;
 
    this._timer = new Timer(100, false); // interval make no sense here
    if (frame != 1)
        this._timer.start(100);
    else
        this._timer.start(res.values.electricShockMin + rand(res.values.electricShockGap));
    this._timer.onTick = function(sender) {this super._eletricShock(frame)};
}

GameScene

所有游戏元素都组合在 GameScene 中,包括球、砖块和挡板边界。它们在 GameScene 中创建。边界可防止球离开屏幕。

示例: 在屏幕周围创建边界 

function _createMantinels()
{
    // Ground
    var (width, height) = (System.width, 1);
    this._ground = this.addPolygonBody(null, #static, 0.0, 0.0, 1.0, width, height); // density, friction, bounce
    this._ground.setPosition(System.width/2, System.height - (this._ground.height/2));
    
    // Left mantinel
    var (widthML, heightML) = (1, System.height);
    this._leftMantinel = this.addPolygonBody(null, #static, 0.0, 0.0, 1.0, widthML, heightML);
    this._leftMantinel.setPosition(this._leftMantinel.width/2, System.height/2);
    
    // Righ mantinel
    var (widthMR, heightMR) = (1, System.height);
    this._rightMantinel = this.addPolygonBody(null, #static, 0.0, 0.0, 1.0, widthMR, heightMR);
    this._rightMantinel.setPosition(System.width - (this._rightMantinel.width/2), System.height/2);
    
    // Top mantinel
    var (widthMT, heightMT) = (System.width, 1);
    this._topMantinel = this.addPolygonBody(null, #static, 0.0, 0.0, 1.0, widthMT, heightMT);
    this._topMantinel.setPosition(System.width/2, (this._topMantinel.height/2));
}

当两个物理体碰撞时,会发生两个事件。第一个事件是开始接触时的 onBeginContact,第二个是接触结束时的 onEndContact。这些事件被映射到 _onBeginContactHandler 和 _onEndContactHandler 函数。

在 _onBeginContactHandler 函数中,检查是否有东西撞到了地面。如果是,那只能是球,然后销毁它。由于 onBeginContact 事件在每个时间步最多调用一次,即使出现更多接触,也需要在同一个函数中检查所有接触。两个接触事件的第二个对象是 b2Contact 对象,它携带有关所有接触的信息。相互作用的物理体可以通过 getBodyA 和 getBodyB 函数找到。b2Contact 对象是所有接触的列表。使用 getNext 函数移动到下一个接触。检查所有接触的最佳方法是进行循环。

示例: onBeginContact 事件处理程序

function _onBeginContactHandler(sender, contact)
{
    // if the ball do not exists the contact is irrelevant - do nothing
    if (!this._ball) return;
    // get the first contact
    var current = contact;
    while (current) {
        // get the bodies in the contact
        var bodyA = current.getBodyA();
        var bodyB = current.getBodyB();
        // check if something hit the ground (it can be only ball)
        if (bodyA == this._ground || bodyB == this._ground)
            // destroy the ball
            this._bodiesToDestory.push(this._ball);
        // get the next contact (they can be more contacts during the one step)
        current = current.getNext();
    }
}

_onEndContactHandler 的工作原理类似,但它会检查是否有东西撞到了砖块。如果是,它会降低砖块的等级并播放声音。 

示例: onEndContact 事件处理程序 

function _onEndContactHandler(sender, contact)
{
    // if the ball do not exists the contact is irrelevant - do nothing
    if (!this._ball) return;
    var current = contact;
    // get the first contact
    while (current) {
        // get the bodies in the contact
        var bodyA = current.getBodyA();
        var bodyB = current.getBodyB();
        var existing = this._bricks.filter(:x { return x == bodyA; }); // lamba function, the same as ".filter(function(x) { return x == bodyA; })"
        if (existing.length != 0) {
            bodyA.hit();
            if (this._enableSounds) this._wavPaddle.play();
            return;
        }
        existing = this._bricks.filter(:x { return x == bodyB; });
        if (existing.length != 0) {
            bodyB.hit();
            if (this._enableSounds) this._wavPaddle.play();
            return;
        }
        if (this._enableSounds && (bodyA == this._paddle || bodyB == this._paddle))
            this._wavBall.play();
        // get next contact
        current = current.getNext();
    }
}

物理体不会直接在回调函数中移除。在 box2d 中,不允许在 box2d 回调函数中移除或禁用物理体。尽管此功能在 box2d 官方文档中也有直接提及,但此问题在论坛和其他非官方教程中也经常被讨论。为了确保安全地移除物理体,它们只会被添加到数组中。在 process 函数中(大约每 25ms 运行一次),会搜索数组并从世界中移除所有物理体。

示例: 如果需要移除某些物理体,则从 process 事件调用 destroy body 函数

function process()
{
    var timeStep = 1.0 / 40.0;
    // recalculate physics world. All objects are moved about timeStep
    if (!this.paused)
        this.step(timeStep, 4, 8);
 
    // remove bricks from the world
    if (this._bodiesToDestory.length != 0)
        this._removeBricks();
 
    // inactive bricks in the world
    if (this._bodiesToInactive.length != 0)
        this._inactiveBricks();
 
    // if user finished the level move to the next level
    if (this.brickCount == 0 && this.paused == false){
        this._nextLevel();
        this.paused = true;
    }
}

搜索数组和移除砖块是在单独的函数中完成的。

示例: 从场景中移除砖块

function _removeBricks()
{
    // Remove touched bricks
    for(var body in this._bodiesToDestory) {
        var existing = this._bricks.filter(:x { return x == body; });
        if (existing.length != 0)
            this._bricks.removeByValue(body);
            // GAME OVER
             if (this._ball == body) {
                this._gameStarted = false;
                this._createBall();
                this._paddle.setPosition( this._paddleX, this._paddleY);
            }
            this.destroyBody(body);
    }
    // zero the array
    this._bodiesToDestory = [];
}

菜单 

游戏还有一个简单的菜单,它被创建为一个单独的场景。它只包含三个按钮:play、continue 和 quit。iOS 版本没有 quit 按钮,因为 Apple 的应用程序政策不允许。所有按钮都由单独的函数创建。

示例: 创建 play 按钮

function _createPlayButton(top)
{
    // create new instanco of GameButton with images from the resources
    var button = new ImageButton({image: res.img.playButton, x:System.width/2, y: top, frameWidth: res.img.playButton.width, frameHeight: res.img.playButton.height / 2});
    // set onClick event - start new game
    button.onClick = function(s)
    {
        // create new game scene
        game.game = GameScene.create(0.0, -0.5, { calledClass: GameScene } );
        // initialize new game
        game.game.removeAllBricks();
        game.game.brickCount = 0;
        game.game.paused = true;
        game.game.visible = true;
        game.game.level = 0;
        game.game.start();
        game.push(game.game, new SlideToBottom());
    }
    return button;
}

当应用程序启动时,按钮会以惊人的推入动画显示。要达到所需的效果,需要组合多个 animator。需要更改 alpha 值和缩放。使用 AnimatorChain 来组合更多 animator。

示例: 创建脉冲动画

function _pulseAnimation(obj)           // fade in then pulse
{
    obj.alpha = 1;                      // start form invisible state
    var fade = Transition.to(obj, {
        transition  : Animator.Transition.easeIn,
        duration    : 400,
        alpha       : 255,               // transparency - custom attribute
    }, false);                           // false = don't play autimatically
    var scale1 = Transition.to(obj, {
        transition  : Animator.Transition.easeInOut,
        duration    : 150,
        scale       : 0.8,              // smothly resize the object
    }, false);
    var scale2 = Transition.to(obj, {
        transition  : Animator.Transition.easeInOut,
        duration    : 200,
        scale       : 1.3,              // smothly resize the object
    }, false);
    var scale3 = Transition.to(obj, {
        transition  : Animator.Transition.easeInOut,
        duration    : 100,
        scale       : 1.0              // smothly resize the object
    }, false);
    // play all animations gradually
    new AnimatorChain([fade, scale1, scale2, scale3]).play();
}

总结 

现在是时候将游戏移植到移动平台了。本文介绍了如何在 Moscrif SDK 中创建它。使用 Moscrif SDK可以节省大量时间,因为它只需要为所有平台编写一份代码。此游戏适用于大量设备,因为它支持 iOS、Android 和 Bada 智能手机以及平板电脑。

更多资源 

更多关于 Moscrif 的示例、演示和信息可以在 www.moscrif.com 上找到。

© . All rights reserved.