HarmonyOS实战:基于鸿蒙服务卡片的分布式游戏

Posted 蒙娜丽宁

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HarmonyOS实战:基于鸿蒙服务卡片的分布式游戏相关的知识,希望对你有一定的参考价值。

       2021年7月31日,我在杭州HarmonyOS开发者日做了一个分享,主题是关于鸿蒙服务卡片的奇妙用法。通过让多张服务卡片之间相互交互来实现一个类似“连连看”的游戏(项目名称是“找我”),而且还支持分布式,可以让多部鸿蒙设备参与进来。在过去的一段时间,经常有小伙伴私信我,问能否讲解一下这款游戏的实现原理。现在我就借这篇文章的机会,来谈一谈这款基于鸿蒙服务卡片的分布式游戏的实现原理。

1. 项目概述

游戏演示请看下面的视频

杭州鸿蒙开发者日“找我”游戏演示视频

        “找我”游戏包含两个服务卡片,尺寸分别是2×4和1×2。其中2 ×4的服务卡片用于控制游戏(相当于游戏面板)、1×2的服务卡片用于玩游戏。游戏面板卡片只能在桌面上放一个(就算放置多个,也只有第一个起作用),1×2的服务卡片用于玩游戏,可以在桌面上放置1个或多个。每一个1×2的服务卡片被分成左右两部分,分别用来显示两个随机字符,而且随机字符的颜色和背景色也是随机的,如下图所示。

         在游戏控制面板的左侧也显示一个随机字符。用户可以单击1×2的服务卡片左侧或右侧。如果被单击的随机字符与游戏控制面板上的随机字符是否相同,在游戏控制卡片上的得分就会加5分(可以设置积分增量)。当游戏控制面板右侧的倒计时为零时游戏结束,并将游戏最终的积分和相关的数据保存到数据库中,可以查看不同用户的游戏积分,如下图所示。

         如果点击游戏控制面板右侧的扩展按钮,会弹出设备列表,点击某一个设备,将该窗口流转到另外一部鸿蒙的设备,同时,两部鸿蒙设备已经连接,如下图所示。

 这时两部鸿蒙设备可以同时玩游戏,如下图所示。

 

         加入的鸿蒙设备越多,难度越大。而且需要脑袋来回转动寻找相同的字符,所以这款游戏对颈椎相当有好处。

2. 服务卡片的布局

        在游戏中有两个服务卡片,他们的布局都需要使用CSS和HML来实现,例如,游戏控制卡片的布局代码如下:

<div >

    <div class="normal_container">

        <div class="pic_title_container" onclick="settings">
            <div style="flex-direction : row;">
                <text style="text-align : center; width : 30%; font-size : 60px; color : brown;">{{ randomChar}}</text>
                <div style="flex-direction : column; width : 40%; margin-top: 20px;">
                    <text style="text-align : center; width : 100%; font-size : 25px;">
                        得分
                    </text>
                    <text style="text-align : center; width : 100%; font-size : 25px;color: blue;">
                        {{ score }}

                    </text>
                </div>
                <text style="text-align : center; font-size:60px; width : 30%;color: darkmagenta;">{{countDown}}</text>
            </div>
            <div style="margin-right : 10px;">
                <button onclick="start" type="capsule" style="opacity: 0.7; margin-right : 10px; text-align : center; width : 33%;">开始</button>
                <button onclick="stop" type="capsule" style="opacity: 0.7; margin-right : 10px; text-align : center; width : 33%;">停止</button>
                <button onclick="extend" type="capsule" style="opacity: 0.7;text-align : center; width : 33%;">扩展</button>
            </div>
        </div>


    </div>
</div>

 

        这段布局代码与html非常类似。整段代码分成两部分,上半部分是游戏信息显示界面,下半部分是3个按钮。而且在这段布局代码中包含了大量的变量,如 {{ score }}、{{ randomChar}}等。这些变量都需要用Java代码进行设置。

3. 如何高频刷新服务卡片

        在默认的情况下,服务卡片的定时刷新时间最短是30分钟(需要是30分钟的整数倍)。但这个游戏要求以秒为单位刷新,所以我们需要使用其他的方式定时刷新服务卡片。可以使用线程或者是定时器进行刷新,这款游戏使用了线程来刷新服务卡片。

        例如下面的代码创建了一个线程对象gameThread。在线程对象的run方法中通过休眠的方式定时刷新服务卡片。在本例中,每2秒刷新一次(使用updateForm方法刷新服务卡片)。

Thread gameThread = new Thread(new Runnable() {
    // 延迟放到最后
    @Override
    public void run() {
        // 刷新服务卡片
        while (true) {
            try {
                Thread.sleep(50);
                if (startFlag) {
                    ... ... 
                    // 刷新服务卡片,产生随机字符
                    updateForm(gameWidgetFormId, formBindingData);

                    }
                    Thread.sleep(2000);  // 没2秒刷新一次
                }
            } catch (Exception e) {
           
            }
        }
    }
});
 gameThread.start();

4. 多张服务卡片如何交互

        在本例中需要多张服务卡片进行交互。也就是通过控制游戏的服务卡片来更新用于玩游戏的服务卡片。一个服务卡片要想控制其他的服务卡片,首先需要获得这些服务卡片的FormId。每一个服务卡片都拥有唯一的FormID。

        以首先需要在onCreateForm方法中保存这些服务卡片的FormID,代码如下:

// 用于保持服务卡片的相关信息
public static class GameWidgetData {
    public String leftValue = "4";
    public String rightValue = "6";

    public String leftBackgroundColor = "#FF0000";
    public String rightBackgroundColor = "#FF00FF";

    public String leftColor = "#FF00FF";
    public String rightColor = "#FF0000";

}

public static Map<Long, GameWidgetData> gameWidgetFormIds = new HashMap<>();
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
    ... ...
    if (formName.equals("GameWidget")) {
        GameWidgetData gameWidgetData = new GameWidgetData();
        gameWidgetFormIds.put(formId, gameWidgetData);
    } 
    ... ...
    return formController.bindFormData();
}

        在这段代码中,使用了gameWidgetFormIds来保存所有1×2服务卡片的FormId,其中GameWidgetFormIds类用于保持与1*2服务卡片相关的数据,如字符颜色、背景色等。通过服务卡片的FormId,可以获取与该服务卡片相关的信息。

        不过光在onCreateForm里保存FormID还不行。因为,onCreateForm方法并不是将服务卡片放到桌面上时调用的,而是在显示服务卡片列表时调用的,看下面的图。在这张图中展示了日历应用中所有的服务卡片。其实在这时onCreateForm方法已经被调用了,而且是被调用了多次。

        实际上,日历应用里有4个服务卡片,分别是4个尺寸(1×2、2×2、2×4和4×4)。所以onCreateForm方法被调用了4次。也就是说,App中有n张服务卡片,那么onCreateForm方法就会被调用n次。

         不过不管App中有多少张服务卡片,一次只能将1张服务卡片放到桌面上,所以要获得放在桌面上的服务卡片的FormId,还需要刨除其他n-1张服务卡片的ID。因此,需要在onDeleteForm方法中删除其他n-1张服务卡片的FormID,代码如下:

// formId是被删除的服务卡片的id
@Override
protected void onDeleteForm(long formId) {
  
    if (gamePanelFormId == formId) {
        gamePanelFormId = 0;
    } else {  // 移除多余的服务卡片
        gameWidgetFormIds.remove(formId);
    }
}

也就是说,如果App中有n张服务卡片,将某一张服务卡片放到桌面上,那么会调用2n - 1次事件方法。其中n次是onCreateForm,另外n - 1次是onDeleteForm。

这里还要提一下onDeleteForm方法。该方法有如下两种情况会被调用:

(1)将服务卡片放到桌面之前(前面介绍的场景)

(2)从桌面上删除服务卡片

5. 实现分布式服务卡片

实现分布式服务卡片需要如下3步:

(1)发现其他鸿蒙设备

(2)连接鸿蒙设备

(3)鸿蒙设备之间交互数据

(1)发现其他鸿蒙设备

        发现鸿蒙设备有多种方式,本例使用了鸿蒙特有的分布式技术,就是发现其他设备的DeviceID,每一个鸿蒙设备都有唯一的DeviceID。获取其他设备的DeviceID以及相关信息,使用下面一行代码即可。getDeviceList方法会返回List类型的值,保存发现的所有鸿蒙设备的信息。

DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ALL_DEVICE);

(2)连接鸿蒙设备

        这里的连接是指望网络连接,本例使用socket连接两部鸿蒙设备。因为Socket处理高频数据传输比较有优势。在第一步发现鸿蒙设备后,通过FA流转,将发起流转的鸿蒙设备的IP通过onSaveData方法传给另外一部鸿蒙设备,代码如下:

@Override
public boolean onSaveData(IntentParams intentParams) {
    intentParams.setParam("ip", Tools.getLocalIP(this));
    return true;
}

getLocalIP方法用于获取本地IP,代码如下:

// 获取本地IP
public  static String getLocalIP(Context context) {
    try {
        int ip  = WifiDevice.getInstance(context).getIpInfo().get().getIpAddress() ;
        String ipStr =
                String.format("%d.%d.%d.%d",
                        (ip & 0xff),
                        (ip >> 8 & 0xff),
                        (ip >> 16 & 0xff),
                        (ip >> 24 & 0xff));
        return ipStr;
    } catch (Exception e) {
        System.out.println("socket error:" +e.getMessage());
    }
    return "";
}

        假设发起FA流转的鸿蒙设备为A,FA流转的目标鸿蒙设备为B。这时B已经获取了A的IP。A端需要启动Socket服务,等待B端的连接,代码如下:

// 在A端调用startServer方法启动Socket服务
public void startServer() {
    ServerSocket serverSocket = new ServerSocket(8888);
    if (thread == null) {

        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                       
                        final Socket socket = serverSocket.accept();
                        // 等待B的连接 
                        }catch (Exception e) {
                        }
                    } catch (Exception e) {

                    }
                }
            }
        });
        thread.start();
    }

}

        B端在接收到A的IP后,会在onRestoreData方法中获取A的IP,并通过Socket连接到A,代码如下:

public boolean onRestoreData(IntentParams intentParams) {
    // 获取A的IP
    final String ip = intentParams.getParam("ip").toString();
    Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                     // 连接A
                    clientSocket = new Socket(ip, 8888);

                    //获取输入输出流,与A交互
                    InputStream is = clientSocket.getInputStream();
                    OutputStream os = clientSocket.getOutputStream();

                } catch (Exception e) {
                    
                }
            }
        });
    thread.start();
    return true;
}

(3)鸿蒙设备之间交互数据

        经过前两步后,A和B已经建立了Socket数据线路,剩下的事情就简单得多了。首先A会同时为A和B产生随字符,然后从A端将随机字符传送到B端,这时B会将传输过来的随机字符显示在1×2的服务卡片上,就会看到本文一开始的效果。然后B端点击某一个卡片,会自己判断点击结果,如果点击正确,会通知A端加分。

6. 保存游戏记录

        如果想要游戏有更好的可玩性,可以将游戏中所产生的数据保存起来。本例将游戏所产生的积分保存在SQLite数据库中,以便可以查询游戏积分。保存积分数据的核心代码如下:

package com.unitymarvel.harmonyos.projects.findme.common;

import ohos.app.Context;
import ohos.data.DatabaseHelper;
import ohos.data.rdb.RdbOpenCallback;
import ohos.data.rdb.RdbStore;
import ohos.data.rdb.StoreConfig;
import ohos.data.resultset.ResultSet;
import java.util.ArrayList;
import java.util.List;



public class DataService {
    private Context context;
    private RdbStore store;
    public DataService(Context context) {
        this.context = context;

        StoreConfig config = StoreConfig.newDefaultConfig("game.sqlite");

        RdbOpenCallback callback = new RdbOpenCallback() {
            // 创建表时调用
            @Override
            public void onCreate(RdbStore store) {
                // 创建t_users表
                store.executeSql("CREATE TABLE IF NOT EXISTS t_records (id INTEGER PRIMARY KEY autoincrement, user VARCHAR(30), score int, time datetime default (datetime('now', 'localtime')))");
                Tools.print("成功创建t_records表");
            }
            // 升级表时调用
            @Override
            public void onUpgrade(RdbStore store, int oldVersion, int newVersion) {
            }
        };
        DatabaseHelper helper = new DatabaseHelper(context);

        store = helper.getRdbStore(config,
                1,
                callback,
                null);


    }
    // 保存积分数据
    public void writeGameRecord(String user, int score) {
        String insertSQL = "insert into t_records(user, score) values(?,?);";
        // 向t_users表中插入3条记录
        store.executeSql(insertSQL, new Object[]{user, score});
    }
    // 获取积分数据
    public List<GameRecord> getGameRecords() {
        ArrayList<GameRecord> result = new ArrayList<>();
        String selectSQL = "select user, score, time from t_records order by score desc, time, user";

        ResultSet resultSet = store.querySql(selectSQL, null);

        while(resultSet.goToNextRow()) {

            GameRecord record = new GameRecord();
            record.user = resultSet.getString(0);
            record.score = resultSet.getInt(1);
            record.time = resultSet.getString(2);

            result.add(record);
        }

        return result;
    }
}

        上面的代码将建立一个名为game.sqlite的SQLite数据库文件,并创建一个t_records表,每次游戏结束(倒计时为0),会将游戏积分和用户名保存在t_records表中。并通过getGameRecords方法获取所有用户的游戏积分数据,并可以通过这些数据显示本文一开始展示的游戏积分列表。

        到现在为止,已经深度剖析了“找我”的核心实现原理,其中涉及到了大量鸿蒙的技术,如服务卡片、FA流转、数据库等。在开发类似应用之前,需要先掌握这些技术。如果对这些技术还不熟悉,或想完整掌握本例的实现过程,可以参考如下的视频课程:

《鸿蒙(HarmonyOS)编程思想(Java版)》

《【鸿蒙项目实战】基于鸿蒙服务卡片的分布式游戏:找我》

也可以参考我新出的《鸿蒙征途:App开发实战》一书。

以上是关于HarmonyOS实战:基于鸿蒙服务卡片的分布式游戏的主要内容,如果未能解决你的问题,请参考以下文章

HarmonyOS实战—将CSDN博文搬上鸿蒙卡片

HarmonyOS实战—将CSDN博文搬上鸿蒙卡片

HarmonyOS实战—卡片的样式设计

HarmonyOS实战—可编辑的卡片交互

HarmonyOS实战—欧洲杯还可以这么玩?

HarmonyOS 实战——认识服务卡片及运行第一个服务卡片