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開發實戰》一書,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/292805.html
標籤:其他
