【木棉花】原子化服务卡片还原经典小游戏——数字华容道 原创 精华
前言
服务卡片也能玩游戏了,今天就来还原经典小游戏——数字华容道。详细讲述了数字华容道在服务卡片上的开发思路,内含详细注释。赶紧动手来开发一张服务卡片,休闲时刻来一局!
概述
老规矩,先上效果图。
分别调出两种类型的服务卡片,效果如下:
点击开始按钮,游戏页面服务卡片的文本会随机打乱,开始进行计时。点击上下左右四个按钮,文本会向对应的方向移动一格。
当游戏结束时或者游戏页面服务卡片被删除时,控制页面服务卡片会停止计时。
正文
一、创建一个空白的工程
1. 安装和配置DevEco Studio 2.1 Release
DevEco Studio 2.1 Release下载、DevEco Studio 2.1 Release安装
2. 创建一个Empty JS Phone应用
DevEco Studio下载安装成功后,打开DevEco Studio,点击左上角的File,点击New,再选择New Project,选择Empty Ability(JS)选项,点击Next按钮。
将文件命名为MyCardGame(文件名不能出现中文或者特殊字符,否则将无法成功创建项目文件),选择保存路径,选择API5,设备勾选Phone,最后点击Finish按钮。
3. 创建两个空白的服务卡片
在MyCarGame->entry->src->main->js中点击右键,在弹出的菜单项中选择New,再在弹出的子菜单项中选择Service Widget。
选择Image With Information。
1)在配置列表中,Service Widget Name中命名为ControlPage,Description中命名为This is a control widget,Type选择JS,JS Component Name中命名为ControlPage,Support Dimensions勾选2 * 4。
2)再重复上述操作创建第二个新的服务卡片,在配置列表中,Service Widget Name中命名为GamePage,Description中命名为This is a game widget,Type选择JS,JS Component Name中命名为GamePage,Support Dimensions勾选2 * 4。
4. 设置首卡片尺寸
在MyCarGame->entry->src->main->config.json中,将服务卡片ControlPage中defaultDimension的属性值修改为2 * 4,将服务卡片GamePage中defaultDimension的属性值修改为2 * 4。
"forms": [
{
"jsComponentName": "ControlPage",
"isDefault": true,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*4",
......
},
{
"jsComponentName": "GamePage",
"isDefault": false,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*4",
......
}
]
二、完善两个服务卡片页面布局
1. 完善控制服务卡片页面布局
在MyCarGame->entry->src->main->js->ControlPage->pages->index->index.hml中将原有的容器代码全部删除。
添加一个基础容器div,增加类选择器为class。添加一个基础容器div,类选择器为class_text,内部添加两个text组件,类选择器均为text,第一个text组件的文本为“时间:”,第二个text组件的文本为{{time}},即动态绑定变量time,以显示游戏的时间。
最后依次添加五个按钮,按钮上的文本分别为“上”、“左”、“右”、“开始”和“下”,类选择器分别为btn_up、btn_left、btn_right、btn_start和btn_down,分别添加一个点击事件click_up、click_left、click_right、start和click_down。其中“左”和“右”两个按钮置于类选择器为class_button的基础容器div中,“开始”和“下”两个按钮置于类选择器为class_button的基础容器div中。
<div class="class">
<div class="class_text">
<text class="text">时间:</text>
<text class="text">{{time}}</text>
</div>
<button class="btn_up" onclick="click_up">上</button>
<div class="class_button">
<button class="btn_left" onclick="click_left">左</button>
<button class="btn_right" onclick="click_right">右</button>
</div>
<div class="class_button">
<button class="btn_start" onclick="btn_start">开始</button>
<button class="btn_down" onclick="click_down">下</button>
</div>
</div>
在MyCarGame->entry->src->main->js->ControlPage->pages->index->index.css中将原有的类选择器全部删除。
各个类选择器配置如下:
.class{
width: 100%;
height: 100%;
flex-direction: column;
align-items: flex-start;
background-color: antiquewhite;
}
.class_text{
flex-direction: row;
}
.text{
font-size: 30px;
margin-top:3%;
text-align: center;
}
.btn_up{
width: 20%;
height: 20%;
margin-top: 3%;
margin-left: 40%;
align-items: center;
}
.class_button{
flex-direction: row;
}
.btn_left{
width: 20%;
height: 20%;
margin-top: 3%;
margin-left: 20%;
align-items: center;
}
.btn_right{
width: 20%;
height: 20%;
margin-top: 3%;
margin-left: 20%;
align-items: center;
}
.btn_start{
width: 20%;
height: 20%;
margin-top: 3%;
margin-left: 10%;
align-items: center;
}
.btn_down{
width: 20%;
height: 20%;
margin-top: 3%;
margin-left: 10%;
align-items: center;
}
在MyCarGame->entry->src->main->js->ControlPage->pages->index->index.json中将data原有的数据全部删除,在data中将time初始化为“00:00:00”。
添加actions数组,actions数组是所有事件的集合,数组的每个属性包括每个事件的名称,名称里面又含有事件的类型“action”和携带的参数“params”。现在为上述五个点击事件分别配置属性:
{
"data": {
"time": "00:00:00"
},
"actions": {
"click_up": {
"action": "message",
"params": {
"message": "click_up"
}
},
"click_left": {
"action": "message",
"params": {
"message": "click_left"
}
},
"click_right": {
"action": "message",
"params": {
"message": "click_right"
}
},
"click_down": {
"action": "message",
"params": {
"message": "click_down"
}
},
"click_start": {
"action": "message",
"params": {
"message": "click_start"
}
}
}
}
2. 完善控制服务卡片页面布局
在MyCarGame->entry->src->main->js->GamePage->pages->index->index.hml中将原有的容器代码全部删除。
添加一个基础容器div,增加类选择器为class。依次添加16个text组件,类选择器均为text,文本分别为{{text1}}、{{text2}}、……、{{text16}},即分别动态绑定一个变量。其中,每四个text组件置于类选择器为class_text的基础容器div中。
<div class="class">
<div class="class_text">
<text class="text">{{text1}}</text>
<text class="text">{{text2}}</text>
<text class="text">{{text3}}</text>
<text class="text">{{text4}}</text>
</div>
<div class="class_text">
<text class="text">{{text5}}</text>
<text class="text">{{text6}}</text>
<text class="text">{{text7}}</text>
<text class="text">{{text8}}</text>
</div>
<div class="class_text">
<text class="text">{{text9}}</text>
<text class="text">{{text10}}</text>
<text class="text">{{text11}}</text>
<text class="text">{{text12}}</text>
</div>
<div class="class_text">
<text class="text">{{text13}}</text>
<text class="text">{{text14}}</text>
<text class="text">{{text15}}</text>
<text class="text">{{text16}}</text>
</div>
</div>
在MyCarGame->entry->src->main->js->GamePage->pages->index->index.css中将原有的类选择器全部删除。
各个类选择器配置如下:
.class{
width: 100%;
height: 100%;
flex-direction: column;
align-items: flex-start;
}
.class_text{
flex-direction: row;
}
.text{
width: 25%;
height: 25%;
text-color: black;
font-size: 30px;
background-color: aquamarine;
text-align: center;
}
在MyCarGame->entry->src->main->js->GamePage->pages->index->index.json中将data原有的数据全部删除,在data中将text1、text2、……、text16初始化为“1”、“2”、……、“15”、“”。
{
"data": {
"text1": "1",
"text2": "2",
"text3": "3",
"text4": "4",
"text5": "5",
"text6": "6",
"text7": "7",
"text8": "8",
"text9": "9",
"text10": "10",
"text11": "11",
"text12": "12",
"text13": "13",
"text14": "14",
"text15": "15",
"text16": ""
}
}
三、响应点击事件
在MyCarGame->entry->src->main->java->com->test->mycargame->MainAbility.java中修改以下代码。
1. 修改onCreateForm()方法
onCreateForm()方法在两种情况被调用。第一种情况是上滑呼出卡片的时候,这时候上滑卡片的动作就会调用一次onCreateForm()方法生成一张卡片;
第二种情形是长按应用,然后点击"服务卡片",此时会显示该应用的所有卡片,每一张卡片都会回调一次onCreateForm()方法并生成一张对应的卡片。当选择了其中一张卡片添加到桌面后,其他卡片回调onDeleteForm()方法来删除该卡片。
为了保证控制页面服务卡片和游戏页面服务卡片只对对应类型卡片的第一张起作用,分别定义两个long类型的全局变量ControlPanelFormId和GamePanelFormId,并初始化为0。在onCreateForm()方法中,根据服务卡片的名字formName来判断是哪种类型的服务卡片,如果对应的变量为0,即表示该种类型的卡片还没有生成,则使该卡片的id赋值给对应的变量。
public static long ControlPanelFormId = 0;
public static long GamePanelFormId = 0;
protected ProviderFormInfo onCreateForm(Intent intent) {
......
if(formName.equals("ControlPage")) {
if(ControlPanelFormId == 0) {
ControlPanelFormId = formId;
}
}
if(formName.equals("GamePage")) {
if(GamePanelFormId == 0) {
GamePanelFormId = formId;
}
}
return formController.bindFormData();
}
2. 修改onDeleteForm()方法
为了使卡片删除后再生成卡片时,游戏仍然能够继续进行,我们需要对onDeleteForm()方法进行修改。删除卡片时,判断卡片的id和ControlPanelFormId、GamePanelFormId是否相同。如果相同,则说明删除的是响应游戏功能的卡片,需要将对应的变量赋值为0,等待下一次生成该种类型的卡片。
protected void onDeleteForm(long formId) {
.....
if(ControlPanelFormId == formId){
ControlPanelFormId = 0;
}
if(GamePanelFormId == formId){
GamePanelFormId = 0;
}
}
3. 添加文本更新方法
为了响应控制页面服务卡片的上、下、左、右的点击事件,从而使游戏页面服务卡片的文本进行更新操作,因此在响应点击事件前,先增加文本更新方法。
添加两个int类型的全局变量row_0和column_0,并初始化为4,以记录空文本的位置。添加一个int[][]类型的变量grids,并初始化为{{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}},以记录游戏页面服务卡片各位置上的文本。
添加一个名为upText的函数,形参为(String direction)。根据传入的参数的值(click_up、click_left、click_right和click_down),通过zsonObject.put(key, value)和updateForm(formId, formBindingData)对游戏页面服务卡片的文本进行更新。并且对变量grids也进行同步更新。
private static int row_0 = 4;
private static int column_0 = 4;
private static int[][] grids = {{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}};
public void upText(String direction){
if(direction == "click_up"){
if(row_0 != 4){
try{
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("text" + ((row_0 - 1) * 4 + column_0), Integer.toString(grids[row_0][column_0 - 1]));
zsonObject.put("text" + ((row_0) * 4 + column_0), "");
FormBindingData formBindingData = new FormBindingData(zsonObject);
updateForm(GamePanelFormId, formBindingData);
int temp = grids[row_0][column_0 - 1];
grids[row_0][column_0 - 1] = 0;
grids[row_0 - 1][column_0 - 1] = temp;
row_0++;
} catch (Exception e){
}
}
}else if(direction == "click_left"){
if(column_0 != 4){
try{
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("text" + ((row_0 - 1) * 4 + column_0), Integer.toString(grids[row_0 - 1][column_0]));
zsonObject.put("text" + ((row_0 - 1) * 4 + column_0 + 1), "");
FormBindingData formBindingData = new FormBindingData(zsonObject);
updateForm(GamePanelFormId, formBindingData);
int temp = grids[row_0 - 1][column_0];
grids[row_0 - 1][column_0] = 0;
grids[row_0 - 1][column_0 - 1] = temp;
column_0++;
}catch (Exception e){
}
}
}else if(direction == "click_right"){
if(column_0 != 1){
try{
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("text" + ((row_0 - 1) * 4 + column_0), Integer.toString(grids[row_0 - 1][column_0 - 2]));
zsonObject.put("text" + ((row_0 - 1) * 4 + column_0 - 1), "");
FormBindingData formBindingData = new FormBindingData(zsonObject);
updateForm(GamePanelFormId, formBindingData);
int temp = grids[row_0 - 1][column_0 - 2];
grids[row_0 - 1][column_0 - 2] = 0;
grids[row_0 - 1][column_0 - 1] = temp;
column_0--;
}catch (Exception e){
}
}
}else if(direction == "click_down"){
if(row_0 != 1){
try{
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("text" + ((row_0 - 1) * 4 + column_0), Integer.toString(grids[row_0 - 2][column_0 - 1]));
zsonObject.put("text" + ((row_0 - 2) * 4 + column_0), "");
FormBindingData formBindingData = new FormBindingData(zsonObject);
updateForm(GamePanelFormId, formBindingData);
int temp = grids[row_0 - 2][column_0 - 1];
grids[row_0 - 2][column_0 - 1] = 0;
grids[row_0 - 1][column_0 - 1] = temp;
row_0--;
}catch (Exception e){
}
}
}
}
4. 响应上下左右四个按钮的点击事件
点击事件的回调方法为onTriggerFormEvent()。在onTriggerFormEvent()方法中,通过zsonObject.getString接受事件的参数,先判断GamePanelFormId不等于0,即已经生成游戏页面服务卡片,再根据参数的不同来调用函数upText。
protected void onTriggerFormEvent(long formId, String message) {
......
ZSONObject zsonObject = ZSONObject.stringToZSON(message);
String direction = zsonObject.getString("message");
if(GamePanelFormId != 0){
if (direction.equals("click_up")) {
upText("click_up");
}else if(direction.equals("click_left")){
upText("click_left");
}else if(direction.equals("click_right")){
upText("click_right");
}else if(direction.equals("click_down")){
upText("click_down");
}
}
}
5. 添加时间更新方法和文本打乱方法
为了响应控制页面服务卡片的开始的点击事件,从而使游戏页面服务卡片的文本进行打乱操作和控制页面服务卡片的时间进行更新操作,因此在响应点击事件前,先增加时间更新方法和文本打乱方法。
添加一个Timer类型的全局变量timer,以进行时间叠加更新。添加一个int类型的全局变量time,以记录当前时间进度。添加一个boolean类型的全局变量k并初始化为false,以记录当前时间是否开始累加。
添加一个名为run的函数。在函数体内对变量time赋值为0,对变量k赋值为true。初始化变量timer,并添加字线程,使时间累加量time每隔一秒加1,并通过zsonObject.put(key, value)和updateForm(formId, formBindingData)对控制页面服务卡片的时间进行更新。
添加一个名为initialize的函数。对变量row_0和column_0赋值为0,grids赋值为{{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}},定义一个String[]类型的变量array,并初始化为{“click_up”,“click_left”,“click_right”,“click_down”}。重复50次随机抽取一个字符串,并调用函数upText对游戏页面服务卡片的文本进行打乱操作。然后判断k为true时,则取消时间任务timer。最后调用函数run。
private static Timer timer;
private static int time;
private static boolean k = false;
private void initialize(){
row_0 = 4;
column_0 = 4;
grids = new int[][]{{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}};
String[] array = {"click_up","click_left","click_right","click_down"};
for(int i = 0; i < 50; i++){
double random = Math.floor(Math.random() * 4);
String direction = array[(int) random];
upText(direction);
}
if(k){
timer.cancel();
}
run();
}
private void run(){
time = 0;
k = true;
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
getUITaskDispatcher().asyncDispatch(()->{
time++;
int hou = time / 3600;
int min = (time % 3600) / 60;
int sec = (time % 3600) % 60;
String str_hour = "";
String str_min = "";
String str_sec = "";
if(hou < 10){
str_hour = "0" + hou;
}else{
str_hour = Integer.toString(hou);
}
if(min < 10){
str_min = "0" + min;
}else{
str_min = Integer.toString(min);
}
if(sec < 10){
str_sec = "0" + sec;
}else{
str_sec = Integer.toString(sec);
}
try{
ZSONObject zsonObject = new ZSONObject();
zsonObject.put("time", str_hour + ":" + str_min + ":" + str_sec);
FormBindingData formBindingData = new FormBindingData(zsonObject);
updateForm(ControlPanelFormId, formBindingData);
} catch (Exception e){
}
});
}
}, 0, 1000);
}
6. 响应开始按钮的点击事件
点击事件的回调方法为onTriggerFormEvent()。在onTriggerFormEvent()方法中,通过zsonObject.getString接受事件的参数,在判断GamePanelFormId不等于0的里面,根据参数为click_start来调用函数initialize。
protected void onTriggerFormEvent(long formId, String message) {
......
if(GamePanelFormId != 0){
if (direction.equals("click_up")) {
upText("click_up");
}else if(direction.equals("click_left")){
upText("click_left");
}else if(direction.equals("click_right")){
upText("click_right");
}else if(direction.equals("click_down")){
upText("click_down");
}else if(direction.equals("click_start")) {
initialize();
}
}
}
四、完善其他功能
1. 当游戏页面服务卡片被删除时停止计时
在onDeleteForm()方法中的判断GamePanelFormId==formId中添加一个判断,如果k为true时,通过canel()方法停止计时,并且将k赋值为false。
protected void onDeleteForm(long formId) {
......
if(ControlPanelFormId == formId){
ControlPanelFormId = 0;
}
if(GamePanelFormId == formId){
GamePanelFormId = 0;
if(k){
timer.cancel();
k = false;
}
}
}
2. 游戏结束时停止计时
添加一个名为gameover的函数,判断girds的值是否为游戏成功的数值,若有一个不符合则返回false,否则返回true。
在函数upText中判断函数gameover的返回值和k的值,如果均为真,则通过cancel()方法停止计时,并将k赋值为false。
public void upText(String direction){
......
if(gameover()){
timer.cancel();
k = false;
}
}
private boolean gameover(){
int[][] Grids = new int[][]{{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}};
for(int row = 0; row < 4; row++){
for(int column = 0; column < 4; column++){
if(grids[row][column] != Grids[row][column]){
return false;
}
}
}
return true;
}
写在最后
至此,整个项目就完成了,使用模拟器Mate 30运行即可。
深圳大学木棉花组织是一群热衷于学习鸿蒙相关知识和开发鸿蒙相关应用的开发者们,欢迎与各位感兴趣的开发者一起学习鸿蒙开发,相互交流、共同进步。
更多资料请关注我们的项目 : Awesome-Harmony_木棉花
本项目会长期更新 ,希望随着鸿蒙一同成长变强的既有我们,也有正在看着这个项目的你。明年3月,深大校园内的木棉花会盛开,那时,鸿蒙也会变的更好,愿这花开,有你我的一份。
上次见到楼主的华容道还是在手表上,感觉可以期待楼主能在更多地方展示。
手表 手机 服务卡片 就差分布式了|•'-'•)و✧
学习学习~~~
用卡片实现了游戏也是挺有脑洞的,但是这界面让UI看了落泪,让设计看了想打人。
狗头保命.jpg
溜了溜了~