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

Bob's Quest (征服 Android Wear,第二部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (4投票s)

2015年10月14日

CPOL

6分钟阅读

viewsIcon

19870

downloadIcon

92

隆重推出 Bob's Quest,一个(某种程度上的)Flappy Bird 克隆游戏,在这个游戏中,Bob 必须在跳跃穿越太空时避免撞上柱子。

Get it on Google Play

引言

你好,欢迎来到我的最新 CP 文章!

在本系列文章的第一部分中,我发布了 Wearable Chess,这是世界上第一个适用于 Android Wear 的免费开源国际象棋游戏。我还为初学者包含了一个 FAQ 以及一个设置 Android Wear 开发环境的指南。点击此处在 Google Play 上获取 Wearable Chess

在本文(第二部分)中,我将从头到尾向您展示我是如何构建 Bob's Quest 的。(最初我也想为第一部分这样做,但 Wearable Chess 的复杂性意味着完整的、从头到尾的演练并不可行。相反,我更专注于应用程序设计背后的通用原理。)

总之,介绍就到这里。让我们开始编码吧! :bob

如果您在阅读我的代码时有任何评论或建议,请随时在页面底部的评论区发表。 :D

游戏循环

当今几乎所有计算机游戏都基于一个共同的概念——游戏循环。

一个简单的游戏循环,用伪代码表示如下:

while (true) {
    GetUserInput(); //Get input from keyboard, mouse, touch, etc
    DoGameLogic(); //Use the above input to do explosions, physics calculations, etc, for this frame
    RefreshGUI(); //Draw the next frame
}

在 Bob's Quest 中,我们的游戏循环形式如下:

package com.orangutandevelopment.bobsquest;

import ...

public class BobView extends View {
    final int refresh_interval = 40;
    private double delta_time = 0;
    private Date last_updated;
    private Handler h;

    public BobView(Context context) {
        super(context);
        init();
    }

    public void init() {
        setWillNotDraw(false); //Ensures that drawing occurs immediately when requested.

        //User input
        this.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //Handle touch event
				...
                return false;
            }
        });

        //Game loop
        last_updated = new Date();
        h = new Handler();
        h.postDelayed(new Runnable() {
            @Override
            public void run() {
                Update();
                h.postDelayed(this, refresh_interval);
            }
        }, refresh_interval);
    }

    public void Update() {
        //How long since last update?
        delta_time = (new Date()).getTime() - last_updated.getTime();
        
		...
		//Make changes to game data
		...

        //Update GUI!
        this.postInvalidate();
        last_updated = new Date();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        ...
		//Render the GUI
		...
    }
}

创建游戏对象

Bob's Quest 使用以下类来创建视差背景、Bob 需要避开的随机生成的墙壁,甚至 Bob 本身!

public class GameObject {
    public double X = 0;
    public double Y = 0;
    public double Horizontal_Speed = 0;
    public double Vertical_Speed = 0;
    public double scaling = 1;

    public GameObject() {

    }
}

在 BobView.java 中,GameObjects 的声明如下:

GameObject Bob = new GameObject();
ArrayList<GameObject> walls = new ArrayList<>();
ArrayList<GameObject> stars = new ArrayList<>();
ArrayList<GameObject> clouds = new ArrayList<>();

要绘制游戏对象,我们需要声明一些 Bitmap...

public Bitmap Bob_Image;
public Bitmap Background_Image;
public Bitmap Star_Image;
public Bitmap Cloud_Image;

private Paint mPaint; //Used for drawing Bitmaps

public void init() {
    ...

    //Decode resources
    Bob_Image = BitmapFactory.decodeResource(getResources(), R.drawable.bob_w);
    Background_Image = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
    Cloud_Image = BitmapFactory.decodeResource(getResources(), R.drawable.cloud_t);
    Star_Image = BitmapFactory.decodeResource(getResources(), R.drawable.star_t);

    mPaint = new Paint();
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(Color.BLACK);
    mPaint.setTypeface(tf);
	
	...
}

...然后我们使用这些 void 来绘制我们的对象

private void drawGameObject(Canvas canvas, Paint paint, GameObject object, Bitmap bitmap) {
    canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), getGameObjectRect(object, bitmap), paint);
}

private RectF getGameObjectRect(GameObject object, Bitmap bitmap) {
    return new RectF((float) object.X, (float) object.Y, (float) object.X + (float)(object.scaling * bitmap.getWidth()), (float) object.Y + (float)(object.scaling * bitmap.getHeight()));
}

一点可能让 C# 开发者(比如我)感到惊讶的是,Java 似乎通过 (x1, y1, x2. y2) 定义 Rectangle,而不是 (x, y, width, height)。

创建视差背景

(上面的动画 GIF 可能需要一段时间才能加载)

花了几个小时才正确实现这个动画——换句话说,让它增强视觉体验而不分散用户注意力。最难的部分是创建图像本身以及尝试调整颜色。不幸的是——因为每个游戏的图像和颜色都不同——我在这方面能提供的帮助不多,只能建议您多尝试、注意细节并保持耐心。

一旦图像完成,创建视差就相当简单了。背景、云和星星就是这样绘制的:

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.argb(255, 0, 0, 0));

    //Background first
    canvas.drawBitmap(Background_Image, new Rect(0, 0, Background_Image.getWidth(), Background_Image.getHeight()), new Rect(0, 0, canvas.getWidth(), canvas.getHeight()), mPaint);

    //Clouds and stars next
    for (int j = 0; j < stars.size(); ++j) {
        drawGameObject(canvas, mPaint, stars.get(j), Star_Image);
    }
    for (int j = 0; j < clouds.size(); ++j) {
        drawGameObject(canvas, mPaint, clouds.get(j), Cloud_Image);
    }
    
    ...
}

为了移动云和星星,我做了这个:

for (int j = 0; j < stars.size(); ++j) {
    stars.get(j).X -= 1;
    if (stars.get(j).X < -30)
        stars.remove(j);
}
for (int j = 0; j < clouds.size(); ++j) {
    //Clouds move twice as fast as stars.
    clouds.get(j).X -= 2;

    //Removes them once they pass off the screen
    if (clouds.get(j).X < -1 * clouds.get(j).scaling * Cloud_Image.getWidth())
        clouds.remove(j);
}

新星星和云是这样随机生成的:

if (times == 50) {
    times = 0;
    GameObject c = new GameObject();
    c.Y = r.nextInt(180) + 120;
    c.X = 340;
    c.scaling = r.nextDouble();
    clouds.add(c);
}
if (times == 10 || times == 20 || times == 30 || times == 40 || times == 50 || times == 0) {
    GameObject s = new GameObject();
    s.Y = r.nextInt(320);
    s.X = 340;
    s.scaling = r.nextDouble() * .25;
    stars.add(s);
}

绘制墙壁

这些需要一些花哨的画布操作。这是我的绘制方法,并附有大量注释说明:

for (int j = 0; j < walls.size(); ++j) {
    GameObject w = walls.get(j);

    //Draw the black center top part
    canvas.drawRect((float) w.X + 3, 0, (float) w.X + 14, (float) w.Y - 60, mPaint);
 
    //Change color for the white outline
    mPaint.setColor(Color.argb(255, 230, 250, 252));
    
    //Top left white line
    canvas.drawRect((float) w.X, 0, (float) w.X + 2, (float) w.Y - 60, mPaint);
    
    //Top right white line
    canvas.drawRect((float)w.X + 15, 0, (float)w.X + 17, (float)w.Y - 60, mPaint);
    
    //Top white block
    canvas.drawRect((float) w.X - 4, (float) w.Y - 60, (float) w.X + 21, (float) w.Y - 54, mPaint);
    
    //Change color to draw bottom half
    mPaint.setColor(Color.argb(255, 0, 0, 0));

    //Bottom black region
    canvas.drawRect((float) w.X, (float) w.Y + 60, (float) w.X + 17, 320, mPaint);
    
    //Outline color
    mPaint.setColor(Color.argb(255, 230, 250, 252));
    
    //Bottom left white line
    canvas.drawRect((float) w.X, (float) w.Y + 60, (float) w.X + 2, 320, mPaint);
    
    //Bottom right white line
    canvas.drawRect((float) w.X + 15, (float)w.Y + 60, (float) w.X + 17, 320, mPaint);
    
    //Bottom white block
    canvas.drawRect((float) w.X - 4, (float) w.Y + 60, (float) w.X + 21, (float) w.Y + 66, mPaint);
    
    //Change color for repeat
    mPaint.setColor(Color.argb(255, 0, 0, 0));
}

墙壁的动画方式与云和星星一样,只是它们移动的速度是云的两倍,是星星的四倍。当 Bob 经过时,还会实现得分。

for (int j = 0; j < walls.size(); ++j) {
    walls.get(j).X -= 4;
    if (walls.get(j).X < -30)
        walls.remove(j);
        
    //Did we score?
    if (Math.abs(walls.get(j).X - Bob.X) < 2)
        ++score;
	
	...
}

动画 Bob

为了让 Bob 像重力作用下的真实物体一样上下“摆动”,我们需要定义一个较低的重力常数,并根据该常数改变 Bob 的下落速度。

我们还需要阻止他在游戏开始前掉落,而是让他轻轻地漂浮在屏幕上,等待用户轻点开始。

if (game_started) {
    Bob.Y -= Bob.Vertical_Speed * delta_time;
    Bob.Vertical_Speed -= .00075 * delta_time; //.00075 is the gravitational constant here

    //Don't let him go through the roof or fall through the floor!
    if (Bob.Y > 320 - Bob_Space.height())
        Bob.Y = 320 - Bob_Space.height();
    if (Bob.Y < 0)
        EndGame();
} else {
    Bob.Y += bobbing ? .5 : -.5;
}

bobbing 布尔值每隔几个周期就会改变,使用的是与控制新云和星星创建时相同的 times 整数。

为了在屏幕被轻点时让 Bob 跳起来,我们需要在 init() 函数中实现一个 OnTouchListener:

this.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!game_over) {
            game_started = true;
            Bob.Vertical_Speed = .3;
        }
        return false;
    }
});

将 Bob 的垂直速度设置为 .3 意味着他以每帧 .3 像素的速度向上移动。在接下来的几帧中,重力常数会将 Bob 的速度拉低至 0 以下,并使其开始再次下落。

绘制 Bob 非常简单,我们只需使用前面提到的 drawGameObject() void:

drawGameObject(canvas, mPaint, Bob, Bob_Image);

检测碰撞

检测碰撞非常简单。我们只需要创建一个 void 来查找给定墙壁占据的 Rectangles,如下所示:

private RectF[] getWallSpaceRect(GameObject wall) {
    RectF top = new RectF((float) wall.X, 0, (float) wall.X + 17, (float) wall.Y - 54);
    RectF bottom = new RectF((float) wall.X, (float)wall.Y + 60, (float) wall.X + 17, 320);
    return new RectF[] {top, bottom};
}

然后,我们将此代码添加到移动墙壁的 for 循环末尾——Java 很方便地提供了一个预置的 intersects() 方法!

RectF Bob_Space = getGameObjectRect(Bob, Bob_Image);

for (int j = 0; j < walls.size(); ++j) {
    ...
    
    RectF[] wall_space = getWallSpaceRect(walls.get(j));
    for (RectF r : wall_space) {
        if (RectF.intersects(r, Bob_Space)) {
            EndGame(); //We have a hit!
            break;
        }
    }
}

使用自定义字体

要为 Android Wear 项目添加自定义字体,请在与 "java" 和 "res" 同一级别创建一个名为 "assets" 的新文件夹。

接下来,将您的字体文件放在该文件夹中,如上所示。在此游戏中,我使用了一种名为 Munro 的炫酷字体。为了能够使用此字体在画布上绘制,我在 BobView.java 中使用了以下代码:

public void init() {
    ...
    Typeface tf = Typeface.createFromAsset(getContext().getAssets(), "Munro.ttf");
    mPaint.setTypeface(tf);
    ...
}

@Override
protected void onDraw(Canvas canvas) {
    //Just an example
    canvas.drawText("Cool Font", 10, 10, mPaint);
}

此外,由于我需要在基于 XML 的 GUI 中使用此字体,因此我创建了一个名为 CoolFontTextView 的新类,它继承了默认的 TextView 类:

public class CoolFontTextView extends TextView {
    public CoolFontTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.setTypeface(Typeface.createFromAsset(context.getAssets(), "Munro.ttf"));
    }
}

在 XML 中创建 GUI

让我们从 XML 文档的根元素开始:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WearableFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:id="@+id/container" tools:context=".MainActivity"
    tools:deviceIds="wear">

    <com.orangutandevelopment.bobsquest.BobView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/bob_view" />

    ...

</android.support.wearable.view.WearableFrameLayout>

BobView 是最重要的 GUI 元素。在 BobView 元素之上是一个 FrameLayout,它提供了 "Game Over" 标志的黑色半透明覆盖层:

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/overlay_view"
    android:background="#aa000000"
    android:visibility="gone">

    ...

</FrameLayout>

在该 FrameLayout 内是一个垂直对齐的 LinearLayout,其中包含两个 CoolFontTextView 和一个 ImageButton:

<com.orangutandevelopment.bobsquest.CoolFontTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/view"
    android:layout_gravity="center_horizontal"
    android:text="Game Over"
    android:textColor="#ffffff"
    android:textSize="32sp" />

<com.orangutandevelopment.bobsquest.CoolFontTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/tx_score"
    android:layout_gravity="center_horizontal"
    android:text="Score: 2 | Top: 13"
    android:textColor="#e6fafc"
    android:textSize="22sp"
    android:layout_marginTop="3dp"
    android:layout_marginBottom="7dp" />

<ImageButton
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:id="@+id/btn_new"
    android:layout_gravity="center_horizontal"
    android:src="@drawable/ic_undo_white_24dp"
    android:layout_marginLeft="15dp"
    android:layout_marginRight="15dp"
    android:background="#0B81FF"
    android:padding="7dp" />

添加事件处理程序

我们快完成了!现在我们需要实现一种方式来响应 Bob 撞墙的情况。我是通过创建以下接口来实现的:

public interface OnGameFinishedListener {
    void onEvent(int score);
}

然后,在 BobView.java 中,我写了以下代码,它允许将多个同类型的监听器附加到同一个事件。Bob's Quest 只需一个,但支持多个是良好的实践,因为您将来可能需要添加更多。

ArrayList<OnGameFinishedListener> mListeners = new ArrayList<>();
public void addListener(OnGameFinishedListener listener) {
    mListeners.add(listener);
}

每当我在 BobView.java 中需要触发此事件时,我都会使用以下代码:

for (OnGameFinishedListener hl : mListeners)
    hl.onEvent(score);

MainActivity 在接收端响应,并在 MainActivity.java 中调用 addListener() 方法。此代码在 Bob 碰撞时显示 "Game Over" 标志。

mBobView.addListener(new OnGameFinishedListener() {
    @Override
    public void onEvent(int score) {
        if (score > TopScore)
            TopScore = score;

        mTxScore.setText("Score: " + score + " | Top: " + TopScore);
        mGameOver.setVisibility(View.VISIBLE);
    }
});

同样,我们也在 MainActivity.java 中处理 New Game 按钮,如下所示:

mNewGame.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mGameOver.setVisibility(View.GONE);
        mBobView.NewGame();
    }
});

记住最高分

记住最高分很容易。我们只需要在 MainActivity.java 中进行以下实现:

private int TopScore = 0;
public static final String PREFS_NAME = "BobsQuestPrefs";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    TopScore = this.getSharedPreferences(PREFS_NAME, 0).getInt("Top_Score", 0);
    ...
}

@Override
protected void onStop() {
    super.onStop();
    SharedPreferences settings = this.getSharedPreferences(PREFS_NAME, 0);
    SharedPreferences.Editor editor = settings.edit();
    editor.putInt("Top_Score", TopScore);
    editor.commit();
}

就这些! :cool

实现长按退出

我在上一篇文章(第一部分)中已经讲过——但为了完整起见,我在这里再次介绍。

第一步是创建一个名为 hold_to_exit.xml 的自定义资源:

<resources>
    <style name="HoldToExit" parent="@android:style/Theme.DeviceDefault.Light">
        <item name="android:windowSwipeToDismiss">false</item>
    </style>
</resources>

接下来,在 AndroidManifest.xml 中,将相关 Activity 的样式更改为 HoldToExit

...
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/HoldToExit" >
...

然后,将此元素添加到您的 XML 布局文件中(最好作为根元素的最后一个子项):

<android.support.wearable.view.DismissOverlayView
    android:id="@+id/dismiss_overlay"
    android:layout_height="match_parent"
    android:layout_width="match_parent"/>

最后,在 MainActivity.java 中,实现以下代码:

private DismissOverlayView mDismissOverlay;
private GestureDetector mDetector;
...

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    mDismissOverlay = (DismissOverlayView) findViewById(R.id.dismiss_overlay);
    mDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
        public void onLongPress(MotionEvent ev) {
            mDismissOverlay.show();
        }
    });
    ...
}
    
@Override
public boolean dispatchTouchEvent (MotionEvent e) {
    return mDetector.onTouchEvent(e) || super.dispatchTouchEvent(e);
}

支持圆形屏幕

在为 Android Wear 开发时,必须注意支持圆形屏幕。

Bob's Quest 通过以下方式支持圆形屏幕:

  • 将得分计数器放置在屏幕的中心顶部,以免被裁剪。
  • 默认情况下将 Bob 偏移到屏幕中心,以免其移动被裁剪。
  • 将墙壁中的孔限制在圆形屏幕的视野范围内。
  • 将按钮保持在 290px 垂直标记之上(适用于 Moto 360)。

总结

感谢您阅读到最后! :D

我非常喜欢写这篇文章,希望它能激励其他开发者看看他们能用 Android Wear 做些什么。可穿戴技术仍然有很大的潜力尚未挖掘。

一如既往——如果您有任何评论、问题或建议,请在下方发表。如果您喜欢这篇文章,请不要忘记给它打 5 星! :cool

如果您有 Android Wear 智能手表,但又不想下载源代码自己编译 Bob's Quest,那么您可以在 Google Play 上以少量费用获取预编译的应用程序。

Get it on Google Play

历史

  • 15/10/14 发布第一个版本
  • 15/10/15 添加了 Google Play 链接,并重写/重新排列了一些部分
© . All rights reserved.