[文章]如何使用HarmonyOS实现一个带有儿童模式的简单小游戏

阅读量0
0
1
1. 介绍      本文将介绍合成HarmonyOS小游戏,如下图所示,按照从左到右的顺序,相同的图形碰撞合成下一个图形,最终合成HarmonyOS图形。
35.png
  
效果图预览:

另外,我们新增了儿童模式,授权后才可以进行游戏。效果图展示:
   

2. 搭建HarmonyOS环境   我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
  • 安装DevEco Studio,详情请参考下载和安装软件。
  • 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
    • 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
    • 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
  • 开发者可以参考以下链接,完成设备调试的相关配置:
    • 使用真机进行调试
    • 使用模拟器进行调试
   

3. 代码结构解读      
工程的结构图和相关描述如下:

  • gif:显示gif图片播放的功能。
  • model
    • Product:图形模型。
  • service
    • ProductService:提供图形运动逻辑功能。
  • slice
    • MainAbilitySlice:首页页面入口。
    • SmartWatchSlice:服务端控制类。
  • view
    • ProductContainer:自定义Component类,用于画布绘制功能。
  • utils
    • ComposeUtil:图形碰撞、合成、重叠、掉落的判断。
    • ImageUtil:图片资源加载。
    • NumbersUtil:常量类。
    • DialogUtil:对话框类。
    • LogUtils:log日志类。
    • PermissionsUtils:权限类。
  • MainAbility:主页面。
  • resources/base/layout
    • ability_main.xml:首页入口布局,用于显示游戏界面。
    • ability_smart_watch.xml:手表端授权布局。
    • dialog_comfirm.xml:对话框确认布局。
    • dialog_gif.xml:显示gif图片的布局。
    • exit_dialog.xml:3秒后退出应用的对话框布局。
    • toast_layout.xml:Toast布局。
  • resources/media:product0.png~product6.png为上节的图片资源,firework.gif为gif图片资源。
   
4. 代码主线分析      

下面几张图描述了游戏开始时,第一个图形运动到停止后,再出现下一个图形的一种轨迹:
37.png

38.png

  • 定义图形的属性
    新建Product类,用来定义图形的属性。需要定义:要显示的图形对象PixelMapHolder、图形的半径radius、图形的等级level(用于区分不同的图形),以及图形的圆心坐标和其在x、y方向上的速度。代码如下:
    1. public class Product {
    2.     // 半径
    3.     private int radius;
    4.     // 用于绘制的图像
    5.     private PixelMapHolder pixelMapHolder;
    6.     // 等级
    7.     private int level;
    8.     // X轴坐标
    9.     private float centerX;
    10.     // Y轴坐标
    11.     private float centerY;
    12.     // x方向的速度
    13.     private float speedX;
    14.     // y方向的速度
    15.     private float speedY;
    16. }
    复制代码

  • 定义图形的数据处理
    新建ProductService类,定义一个集合数据,用于存储在屏幕中出现的所有图形数据,并定义dealProduct()方法处理数据计算功能。部分代码如下:
    1. public class ProductService {
    2.     // 所有绘制图形数据集合
    3.     private List productList;

    4.     public void dealProduct() {
    5.         // 处理核心业务
    6.     }
    7. }
    复制代码

  • 定义图形显示的视图
    通过继承Component类,实现Component.DrawTask接口,并重写onDraw方法,绘制图形。从数据层ProductService类中得到图形集合数据,然后调用Canvas类的drawPixelMapHolder()方法绘制图形,再使用EventHandler在子线程中执行任务,该任务主要是做相应的数据计算功能,然后调用invalidate()方法后会再次执行onDraw方法绘制图形。新建ProductContainer类,部分代码如下:
    1. public class ProductContainer extends Component implements Component.DrawTask {
    2.     private ProductService productService;
    3.     private EventHandler runHandler;

    4.     [url=home.php?mod=space&uid=2735960]@Override[/url]
    5.     public void onDraw(Component component, Canvas canvas) {
    6.         // 从数据层得到所有图形集合
    7.         List<Product> products = productService.getProductList();
    8.         // 绘制所有图形
    9.         for (Product product : products) {
    10.             canvas.drawPixelMapHolder(product.getPixelMapHolder(),
    11.                     product.getCenterX() - product.getRadius(),
    12.                     product.getCenterY() - product.getRadius(), new Paint());
    13.         }
    14.         // 在子线程执行任务
    15.         if (runHandler != null) {
    16.             runHandler.postTask(runnable);
    17.         }
    18.     }

    19.     private Runnable runnable = new Runnable() {
    20.         @Override
    21.         public void run() {
    22.             // 数据计算功能
    23.             productService.dealProduct();
    24.             // 更新UI进行绘制
    25.             getContext().getUITaskDispatcher().asyncDispatch(() -> {
    26.                 invalidate();
    27.             });
    28.         }
    29.     };
    30. }
    复制代码

5. 图形的生成与运动      
介绍资源图片的加载、图形怎么生成、图形如何运动。
加载图片资源
将图片资源放入"resources/base/media"目录下,新建ImageUtil类读取资源文件用于生成各种Product对象。部分代码如下:
  1. public class ImageUtil {
  2.     private static int[] imageList = {ResourceTable.Media_product0, ResourceTable.Media_product1,
  3.        ResourceTable.Media_product2, ResourceTable.Media_product3, ResourceTable.Media_product4,
  4.        ResourceTable.Media_product5, ResourceTable.Media_product6};

  5.     // 生产图形
  6.     public static Product generateProduct(Context context, int level) {
  7.         ImageSource imageSource = ImageSource.create(context.getResourceManager().getResource(imageList[level]),null);
  8.         PixelMap pixelMap =  imageSource.createPixelmap(null);
  9.         PixelMapHolder pmh = new PixelMapHolder(pixelMap);
  10.         int radius = pixelMap.getImageInfo().size.width / NumbersUtil.VAULE_TWO;
  11.         Product product = new Product();
  12.         product.setLevel(level);
  13.         product.setRadius(radius);
  14.         product.setPixelMapHolder(pmh);
  15.         return product;
  16.     }
  17. }
复制代码
随机生成图形
39.png

40.png
上面三张图描述了随机生成图形的可能情况,由图可知,每次创建新的图形只需要随机生成level值,然后从图片资源中获取对应的模型。生成新的的图形后,然后随机生成圆心的x坐标值,图形y方向的速度为10。部分代码如下:
  1. public class ProductService {
  2.     // 创建新的图形,并加入图形集合中
  3.     private void createNewProduct() {
  4.         // 随机生成level
  5.         int level = (int) (Math.random() * NumbersUtil.VAULE_FOUR);
  6.         Product newProduct = ImageUtil.generateProduct(context, level);
  7.         // 随机生成x坐标值
  8.         newProduct.setCenterX((float) (newProduct.getRadius() + Math.random() *
  9.                 (Math.abs(width - NumbersUtil.VAULE_TWO * newProduct.getRadius()))));
  10.         newProduct.setCenterY(newProduct.getRadius());
  11.         newProduct.setSpeedX(0);
  12.         newProduct.setSpeedY(ComposeUtil.BIT);
  13.         // 将新生成的图形加入集合中
  14.         productList.add(newProduct);
  15.     }
  16. }
复制代码
图形运动
上图描述了一个图形向下运动的轨迹,图形的移动主要是将图形的圆心x,y坐标值每次递增各自的x,y方向的速度值。部分代码如下:
  1. private void updateView() {
  2.     for (Product product : productList) {
  3.         float centerX = product.getCenterX() + product.getSpeedX();
  4.         float centerY = product.getCenterY() + product.getSpeedY();
  5.         product.setCenterX(centerX);
  6.         product.setCenterY(centerY);
  7.     }
  8. }
复制代码
所以图形静止,只要满足当图形的x,y方向速度的值均为0的时候。部分代码如下:
  1. public class ComposeUtil {
  2.     public static boolean isStopProduct(Product product) {
  3.         if (product.getSpeedX() == 0 && product.getSpeedY() == 0) {
  4.             return true;
  5.         }
  6.         return false;
  7.     }
  8. }
复制代码

6. 图形碰撞
下图描述了整个图形运动处理思路:

  • 首先是从集合数据中获取运动的图形,如果集合中没有运动的图形,那么创建新的图形。
  • 如果集合中有运动的图形,则判断该图形是否与其他图形发生了碰撞。
  • 如果该图形没有与其他图形发生碰撞,则判断该图形是否到达画布的边界。
  • 如果没有到达边界则继续运动,如果到达边界了则停止运动。
  • 如果该图形与其他图形发生了碰撞,则判断该图形与碰撞的图形能否合成。
  • 如果不能合成,则判断该图形是否满足碰撞停止条件。
  • 如果满足则停止运动,如果不满足则改变该图形的速度,继续运动。
  • 如果能合成,则合成新的图形。
    接下来分析图形碰撞检测、图形碰撞停止、图形碰撞变速。
图形碰撞检测
如上图所示,两个图形若满足它们的的圆心距离小于它们半径的总和,则认为它们发生了碰撞。部分代码如下:
  1. public class ComposeUtil {
  2.     // 两个图形是否发成碰撞
  3.     private static boolean isCollision(Product productA, Product productB) {
  4.         double minLength = productA.getRadius() + productB.getRadius();
  5.         double maxLength = Math.pow(Math.abs(productA.getCenterX() - productB.getCenterX()), NumbersUtil.VAULE_TWO) +
  6.                 Math.pow(Math.abs(productA.getCenterY() - productB.getCenterY()), NumbersUtil.VAULE_TWO);
  7.         // 发生碰撞
  8.         if (maxLength <= Math.pow(minLength, NumbersUtil.VAULE_TWO)) {
  9.             return true;
  10.         }
  11.         return false;
  12.     }
  13. }
复制代码
图形碰撞停止
上面两张图描述了图形运动到碰撞其他图形后停止运动的情况。满足碰撞的个数大于1个认为碰撞停止,或者碰撞个数为1同时图形与屏幕边界有接触,也认为碰撞停止。
碰撞停止时只需要设置图形的x、y方向的速度为0,这样图形就不会再运动了,部分代码如下:

  1. private void dealRunProduct(Product runProduct) {
  2.     runProduct.setSpeedX(0);
  3.     runProduct.setSpeedY(0);
  4. }
复制代码
图形碰撞变速
上图描述了图形碰撞后改变该图形速度的轨迹,当碰撞的图形既不满足合成条件也不满足碰撞停止条件,就需要变更运动速度的方向与大小。部分代码如下:
  1. public class ProductFun {
  2.     private List<Product> productList;

  3.     private void dealRunProduct(Product runProduct) {
  4.         List<Product> occursProductArray = ComposeUtil.occursCollision(runProduct, productList);
  5.         Product occursProduct=occursProductArray.get(0);
  6.         float speed;
  7.         if (runProduct.getCenterX() > occursProduct.getCenterX()) {
  8.             speed = ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDX;
  9.         } else if (runProduct.getCenterX() < occursProduct.getCenterX()) {
  10.             speed = -ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDX;
  11.         } else {
  12.             speed = 0;
  13.         }
  14.         runProduct.setSpeedX(speed);
  15.         runProduct.setSpeedY(ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDY);
  16.     }  
  17. }
复制代码

7. 图形合成图形合成
上图展示了两个相同类型的图形发生碰撞后,变成一个新的图形。通过判断两个图形的等级level值是否相等,若相等就可以进行合成,部分代码如下:
  1. public class ComposeUtil {
  2.     public static boolean isCompose(Product productA, Product productB) {
  3.         return productA.getLevel() == productB.getLevel();
  4.     }
  5. }
复制代码
然后合成新的图形,新的图形的等级需要在旧图形的等级level值上加1,圆心坐标可选取在两个旧图形的空间范围内,y方向的速度初始化为10,让其为运动状态。部分代码如下:
  1. public class ComposeUtil {
  2.     // 将两个图形合成一个新的图形
  3.     public static Product getComposeProduct(Context context, Product productA, Product productB) {
  4.         // 新图形的level
  5.         int level = productA.getLevel() + 1;
  6.         // 开始创建新图形
  7.         Product newProduct = ImageUtil.generateProduct(context, level);
  8.         // 新图形的中心x位置
  9.         float centerX = Math.abs((productA.getCenterX() + productB.getCenterX()) / NumbersUtil.VAULE_TWO);
  10.         // 获取下落的图形
  11.         Product temp = productA.getCenterY() < productB.getCenterY() ? productA : productB;
  12.         // 图形的中心y位置
  13.         float centerY = temp.getCenterY() - temp.getRadius() + newProduct.getRadius();

  14.         newProduct.setCenterX(centerX);
  15.         newProduct.setCenterY(centerY);

  16.         newProduct.setSpeedX(0);
  17.         newProduct.setSpeedY(ComposeUtil.BIT);

  18.         return newProduct;
  19.     }
  20. }
复制代码
最后需要从集合中移除之前的两个图形,并将新合成的图形添加到集合中。部分代码如下:
  1. public class ProductService {
  2.     private List<Product> productList;

  3.     // 处理运动的图形
  4.     private void dealRunProduct(Product runProduct) {
  5.         Product composeProduct = ComposeUtil.getComposeProduct(context, runProduct, product);
  6.         if (composeProduct != null) {
  7.             productList.remove(runProduct);
  8.             productList.remove(product);
  9.             productList.add(composeProduct);
  10.         }
  11.     }
  12. }
复制代码
合成后引发的问题
如上图显示,在图形合成后,可能会造成其他已经停止的图形会有继续运动的趋势。
这里目前只认为需要继续运动的图形满足:
  • 周围没有碰撞的图形。
  • 碰撞的图形个数只有1个。
  • 碰撞的图形个数为两个,但都在当前图形的上方。
部分代码如下:
  1. public class ComposeUtil {
  2.     public static boolean dropProduct(List products, int width, int height) {
  3.         boolean drop = false;
  4.         for (Product product : products) {
  5.             if (isStopProduct(product)) {
  6.                 if (product.getCenterY() + product.getRadius() >= height) {
  7.                     continue;
  8.                 }
  9.                 List occursProductArray = occursCollision(product, products);
  10.                 // 没有任何接触的图形可以掉落
  11.                 if (occursProductArray.size() == 0) {
  12.                     product.setSpeedY(ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDY);
  13.                     drop = true;
  14.                 }

  15.                 // 接触的图形仅1个需要掉落
  16.                 boolean result = dropProduct(occursProductArray, product, width);
  17.                 if (result) {
  18.                     drop = true;
  19.                 }
  20.                 // 碰撞的图形个数为两个,但都在当前图形的上方
  21.                 result = dropProduct(occursProductArray, product);
  22.                 if (result) {
  23.                     drop = true;
  24.                 }
  25.             }
  26.         }
  27.         return drop;
  28.     }
  29. }
复制代码
8. 儿童模式  
上面四张图展示了儿童启动游戏后,需要请求周边手表与之通信,手表端授权是否可以开启游戏权限,如果拒绝,则手机端的游戏退出。
显示设备
41.png
这是调用流转任务管理服务来显示设备,具体步骤如下:
步骤 1 -  config.json中声明多设备协同访问的权限:ohos.permission.DISTRIBUTED_DATASYNC。配置信息如下:
  1. "module": {
  2.   "reqPermissions": [
  3.     {
  4.       "name": "ohos.permission.DISTRIBUTED_DATASYNC"
  5.     }
  6.   ]
  7. }
复制代码
然后手动申请权限,代码如下:
  1. requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC"}, 0);
复制代码
步骤 2 -  注册流转任务管理服务。代码如下:
  1. private IContinuationRegisterManager continuationRegisterManager;

  2. private void continuationRegister() {
  3.     continuationRegisterManager = getContinuationRegisterManager();
  4.     // 注册FA流转管理服务
  5.     continuationRegisterManager.register(getBundleName(), new ExtraParams(), callback, requestCallback);
  6. }
复制代码
步骤 3 -  设置注册流转任务管理服务回调,得到Ability token。代码如下:
  1. private int abilityToken;

  2. private RequestCallback requestCallback = new RequestCallback() {
  3.     @Override
  4.     public void onResult(int result) {
  5.         abilityToken = result;
  6.     }
  7. };
复制代码
步骤 4 -  通过调用IContinuationRegisterManager类的showDeviceList()方法显示同一网段下的设备。代码如下:
  1. private void showDeviceList() {
  2.     ExtraParams extraParams = new ExtraParams();
  3.     extraParams.setDevType(new String[]{ExtraParams.DEVICETYPE_SMART_TV,
  4.             ExtraParams.DEVICETYPE_SMART_PAD,
  5.             ExtraParams.DEVICETYPE_SMART_PHONE});
  6.     extraParams.setDescription("小设备合成");
  7.     continuationRegisterManager.showDeviceList(abilityToken, extraParams, null);
  8. }
复制代码
步骤 5 -  设置流转任务管理服务设备状态变更的回调,然后在其onDeviceConnectDone()方法中,会返回远程设备的唯一标识deviceId。代码如下:
  1. private IContinuationDeviceCallback callback = new IContinuationDeviceCallback() {
  2.     @Override
  3.     public void onDeviceConnectDone(String deviceId, String s1) {
  4.         continuationRegisterManager.updateConnectStatus(abilityToken,deviceId, DeviceConnectState.IDLE.getState(), null);
  5.     }

  6.     @Override
  7.     public void onDeviceDisconnectDone(String deviceId) {
  8.     }
  9. };
复制代码
手机与手表通信
这里采用分布式调度方式来进行双方通信,即启动FA的模式完成通信。
手机启动手表FA,这里使用同一个MainAbility不同的AbilitySlice来区分手机手表的页面。
  • 在config.json中的MainAbility下配置action内容为"action.smart",代码如下
    1. "abilities": [
    2.   {
    3.     "skills": [
    4.       {
    5.         "actions": [
    6.           "action.system.home",
    7.           "action.smart"
    8.         ]
    9.       }
    10.     ],
    11.     "name": "com.huawei.codelab.MainAbility"
    12.   }
    13. ]
    复制代码

  • 在MainAbility中配置action的页面为SmartWatchSlice,代码如下:
    1. @Override
    2. public void onStart(Intent intent) {
    3.     super.onStart(intent);
    4.     super.setMainRoute(MainAbilitySlice.class.getName());
    5.     addActionRoute("action.smart", SmartWatchSlice.class.getName());
    6. }
    复制代码

  • 通过配置action启动该页面,同时设置远程设备id和分布式调度系统多设备启动的标识来启动远程FA,并将手机的设备id传到手表端。代码如下:
  1. private void startAbilityFA(String deviceId) {
  2.     String localDeviceId = KvManagerFactory.getInstance().createKvManager(
  3.             new KvManagerConfig(this)).getLocalDeviceInfo().getId();
  4.     Intent intent = new Intent();
  5.     Operation operation =
  6.             new Intent.OperationBuilder()
  7.                     .withDeviceId(deviceId) // 手表端设备ID
  8.                     .withBundleName(getBundleName()) // 手机端与手表端同一个应用,所以使用同一个bundle
  9.                     .withAbilityName(MainAbility.class.getName())
  10.                     .withAction("action.smart") // 配置跳转的路由
  11.                     .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) // 分布式调度标识
  12.                     .build();
  13.     intent.setOperation(operation);
  14.     intent.setParam(MainAbilitySlice.DEVICEID, localDeviceId); // 手机端设备id传给手表端
  15.     startAbility(intent);
  16. }
复制代码
手表启动手机FA,然后手机端应用内部通过广播发送到MainSlice中。
  • 在手表端的SmartWatchSlice类中启动手机端FA,设置传入的远程设备remoteDeviceId和分布式调度系统多设备启动的标识来启动远程FA。代码如下:
    1. private void sendMessage(int type) {
    2.     Intent intent = new Intent();
    3.     Operation operation =
    4.             new Intent.OperationBuilder()
    5.                     .withDeviceId(remoteDeviceId) // 手机端设备ID
    6.                     .withBundleName(getBundleName()) // 手机端与手表端同一个应用,所以使用同一个bundle
    7.                     .withAbilityName(MainAbility.class.getName())
    8.                     .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) // 分布式调度标识
    9.                     .build();
    10.     intent.setOperation(operation);
    11.     intent.setParam(CODE, type); // type为是否同意授权的标识
    12.     startAbility(intent);
    13. }
    复制代码

  • 在config.json中配置MainAbility的启动模式为单例模式,代码如下:
    1. "abilities": [
    2.   {
    3.     "name": "com.huawei.codelab.MainAbility"
    4.     "launchType": "singleton"
    5.   }
    6. ]
    复制代码

  • 当MainAbility设置为singleton模式时,并且MainAbility实例存在时,再调用startAbility()方法,会触发onNewIntent()方法的执行,在这里发送公共事件。代码如下:<
    1. @Override
    2. protected void onNewIntent(Intent intent) {
    3.     super.onNewIntent(intent);
    4.     // 发送公共事件
    5.     CommonEventManager.publishCommonEvent();
    6. }
    复制代码

  • 最后在MainAbilitySlice类中注册公共事件并处理该事件即可。代码如下:
    1. // 注册公共事件
    2. CommonEventManager.subscribeCommonEvent(new EventSubscriber());

    3. private class EventSubscriber extends CommonEventSubscriber {
    4.     @Override
    5.     public void onReceiveEvent(CommonEventData commonEventData) {
    6.         // 处理公共事件
    7.     }
    8. }
    复制代码

9. 最终实现效果  
10. 恭喜你      
通过本篇codelab,你可以学到:
  • Canvas绘制图形
  • 线程间通信
  • 图形合成思路分析
  • 流转
11. 参考

回帖

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
链接复制成功,分享给好友