1. 介绍 本文将介绍合成HarmonyOS小游戏,如下图所示,按照从左到右的顺序,相同的图形碰撞合成下一个图形,最终合成HarmonyOS图形。
效果图预览:
另外,我们新增了儿童模式,授权后才可以进行游戏。效果图展示:
2. 搭建HarmonyOS环境 我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
- 安装DevEco Studio,详情请参考下载和安装软件。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
- 开发者可以参考以下链接,完成设备调试的相关配置:
3. 代码结构解读
工程的结构图和相关描述如下:
- gif:显示gif图片播放的功能。
- model
- 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. 代码主线分析
下面几张图描述了游戏开始时,第一个图形运动到停止后,再出现下一个图形的一种轨迹:
- 定义图形的属性
新建Product类,用来定义图形的属性。需要定义:要显示的图形对象PixelMapHolder、图形的半径radius、图形的等级level(用于区分不同的图形),以及图形的圆心坐标和其在x、y方向上的速度。代码如下:
- public class Product {
- // 半径
- private int radius;
- // 用于绘制的图像
- private PixelMapHolder pixelMapHolder;
- // 等级
- private int level;
- // X轴坐标
- private float centerX;
- // Y轴坐标
- private float centerY;
- // x方向的速度
- private float speedX;
- // y方向的速度
- private float speedY;
- }
复制代码
- 定义图形的数据处理
新建ProductService类,定义一个集合数据,用于存储在屏幕中出现的所有图形数据,并定义dealProduct()方法处理数据计算功能。部分代码如下:
- public class ProductService {
- // 所有绘制图形数据集合
- private List productList;
-
- public void dealProduct() {
- // 处理核心业务
- }
- }
复制代码
- 定义图形显示的视图
通过继承Component类,实现Component.DrawTask接口,并重写onDraw方法,绘制图形。从数据层ProductService类中得到图形集合数据,然后调用Canvas类的drawPixelMapHolder()方法绘制图形,再使用EventHandler在子线程中执行任务,该任务主要是做相应的数据计算功能,然后调用invalidate()方法后会再次执行onDraw方法绘制图形。新建ProductContainer类,部分代码如下:
- public class ProductContainer extends Component implements Component.DrawTask {
- private ProductService productService;
- private EventHandler runHandler;
-
- [url=home.php?mod=space&uid=2735960]@Override[/url]
- public void onDraw(Component component, Canvas canvas) {
- // 从数据层得到所有图形集合
- List<Product> products = productService.getProductList();
- // 绘制所有图形
- for (Product product : products) {
- canvas.drawPixelMapHolder(product.getPixelMapHolder(),
- product.getCenterX() - product.getRadius(),
- product.getCenterY() - product.getRadius(), new Paint());
- }
- // 在子线程执行任务
- if (runHandler != null) {
- runHandler.postTask(runnable);
- }
- }
-
- private Runnable runnable = new Runnable() {
- @Override
- public void run() {
- // 数据计算功能
- productService.dealProduct();
- // 更新UI进行绘制
- getContext().getUITaskDispatcher().asyncDispatch(() -> {
- invalidate();
- });
- }
- };
- }
复制代码
5. 图形的生成与运动 介绍资源图片的加载、图形怎么生成、图形如何运动。
加载图片资源将图片资源放入"resources/base/media"目录下,新建ImageUtil类读取资源文件用于生成各种Product对象。部分代码如下:
- public class ImageUtil {
- private static int[] imageList = {ResourceTable.Media_product0, ResourceTable.Media_product1,
- ResourceTable.Media_product2, ResourceTable.Media_product3, ResourceTable.Media_product4,
- ResourceTable.Media_product5, ResourceTable.Media_product6};
-
- // 生产图形
- public static Product generateProduct(Context context, int level) {
- ImageSource imageSource = ImageSource.create(context.getResourceManager().getResource(imageList[level]),null);
- PixelMap pixelMap = imageSource.createPixelmap(null);
- PixelMapHolder pmh = new PixelMapHolder(pixelMap);
- int radius = pixelMap.getImageInfo().size.width / NumbersUtil.VAULE_TWO;
- Product product = new Product();
- product.setLevel(level);
- product.setRadius(radius);
- product.setPixelMapHolder(pmh);
- return product;
- }
- }
复制代码 随机生成图形上面三张图描述了随机生成图形的可能情况,由图可知,每次创建新的图形只需要随机生成level值,然后从图片资源中获取对应的模型。生成新的的图形后,然后随机生成圆心的x坐标值,图形y方向的速度为10。部分代码如下:
- public class ProductService {
- // 创建新的图形,并加入图形集合中
- private void createNewProduct() {
- // 随机生成level
- int level = (int) (Math.random() * NumbersUtil.VAULE_FOUR);
- Product newProduct = ImageUtil.generateProduct(context, level);
- // 随机生成x坐标值
- newProduct.setCenterX((float) (newProduct.getRadius() + Math.random() *
- (Math.abs(width - NumbersUtil.VAULE_TWO * newProduct.getRadius()))));
- newProduct.setCenterY(newProduct.getRadius());
- newProduct.setSpeedX(0);
- newProduct.setSpeedY(ComposeUtil.BIT);
- // 将新生成的图形加入集合中
- productList.add(newProduct);
- }
- }
复制代码 图形运动上图描述了一个图形向下运动的轨迹,图形的移动主要是将图形的圆心x,y坐标值每次递增各自的x,y方向的速度值。部分代码如下:
- private void updateView() {
- for (Product product : productList) {
- float centerX = product.getCenterX() + product.getSpeedX();
- float centerY = product.getCenterY() + product.getSpeedY();
- product.setCenterX(centerX);
- product.setCenterY(centerY);
- }
- }
复制代码 所以图形静止,只要满足当图形的x,y方向速度的值均为0的时候。部分代码如下:
- public class ComposeUtil {
- public static boolean isStopProduct(Product product) {
- if (product.getSpeedX() == 0 && product.getSpeedY() == 0) {
- return true;
- }
- return false;
- }
- }
复制代码
6. 图形碰撞
下图描述了整个图形运动处理思路:
- 首先是从集合数据中获取运动的图形,如果集合中没有运动的图形,那么创建新的图形。
- 如果集合中有运动的图形,则判断该图形是否与其他图形发生了碰撞。
- 如果该图形没有与其他图形发生碰撞,则判断该图形是否到达画布的边界。
- 如果没有到达边界则继续运动,如果到达边界了则停止运动。
- 如果该图形与其他图形发生了碰撞,则判断该图形与碰撞的图形能否合成。
- 如果不能合成,则判断该图形是否满足碰撞停止条件。
- 如果满足则停止运动,如果不满足则改变该图形的速度,继续运动。
- 如果能合成,则合成新的图形。
接下来分析图形碰撞检测、图形碰撞停止、图形碰撞变速。
图形碰撞检测如上图所示,两个图形若满足它们的的圆心距离小于它们半径的总和,则认为它们发生了碰撞。部分代码如下:
- public class ComposeUtil {
- // 两个图形是否发成碰撞
- private static boolean isCollision(Product productA, Product productB) {
- double minLength = productA.getRadius() + productB.getRadius();
- double maxLength = Math.pow(Math.abs(productA.getCenterX() - productB.getCenterX()), NumbersUtil.VAULE_TWO) +
- Math.pow(Math.abs(productA.getCenterY() - productB.getCenterY()), NumbersUtil.VAULE_TWO);
- // 发生碰撞
- if (maxLength <= Math.pow(minLength, NumbersUtil.VAULE_TWO)) {
- return true;
- }
- return false;
- }
- }
复制代码 图形碰撞停止
上面两张图描述了图形运动到碰撞其他图形后停止运动的情况。满足碰撞的个数大于1个认为碰撞停止,或者碰撞个数为1同时图形与屏幕边界有接触,也认为碰撞停止。
碰撞停止时只需要设置图形的x、y方向的速度为0,这样图形就不会再运动了,部分代码如下:
- private void dealRunProduct(Product runProduct) {
- runProduct.setSpeedX(0);
- runProduct.setSpeedY(0);
- }
复制代码 图形碰撞变速上图描述了图形碰撞后改变该图形速度的轨迹,当碰撞的图形既不满足合成条件也不满足碰撞停止条件,就需要变更运动速度的方向与大小。部分代码如下:
- public class ProductFun {
- private List<Product> productList;
-
- private void dealRunProduct(Product runProduct) {
- List<Product> occursProductArray = ComposeUtil.occursCollision(runProduct, productList);
- Product occursProduct=occursProductArray.get(0);
- float speed;
- if (runProduct.getCenterX() > occursProduct.getCenterX()) {
- speed = ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDX;
- } else if (runProduct.getCenterX() < occursProduct.getCenterX()) {
- speed = -ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDX;
- } else {
- speed = 0;
- }
- runProduct.setSpeedX(speed);
- runProduct.setSpeedY(ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDY);
- }
- }
复制代码
7. 图形合成图形合成上图展示了两个相同类型的图形发生碰撞后,变成一个新的图形。通过判断两个图形的等级level值是否相等,若相等就可以进行合成,部分代码如下:
- public class ComposeUtil {
- public static boolean isCompose(Product productA, Product productB) {
- return productA.getLevel() == productB.getLevel();
- }
- }
复制代码然后合成新的图形,新的图形的等级需要在旧图形的等级level值上加1,圆心坐标可选取在两个旧图形的空间范围内,y方向的速度初始化为10,让其为运动状态。部分代码如下:
- public class ComposeUtil {
- // 将两个图形合成一个新的图形
- public static Product getComposeProduct(Context context, Product productA, Product productB) {
- // 新图形的level
- int level = productA.getLevel() + 1;
- // 开始创建新图形
- Product newProduct = ImageUtil.generateProduct(context, level);
- // 新图形的中心x位置
- float centerX = Math.abs((productA.getCenterX() + productB.getCenterX()) / NumbersUtil.VAULE_TWO);
- // 获取下落的图形
- Product temp = productA.getCenterY() < productB.getCenterY() ? productA : productB;
- // 图形的中心y位置
- float centerY = temp.getCenterY() - temp.getRadius() + newProduct.getRadius();
-
- newProduct.setCenterX(centerX);
- newProduct.setCenterY(centerY);
-
- newProduct.setSpeedX(0);
- newProduct.setSpeedY(ComposeUtil.BIT);
-
- return newProduct;
- }
- }
复制代码最后需要从集合中移除之前的两个图形,并将新合成的图形添加到集合中。部分代码如下:
- public class ProductService {
- private List<Product> productList;
-
- // 处理运动的图形
- private void dealRunProduct(Product runProduct) {
- Product composeProduct = ComposeUtil.getComposeProduct(context, runProduct, product);
- if (composeProduct != null) {
- productList.remove(runProduct);
- productList.remove(product);
- productList.add(composeProduct);
- }
- }
- }
复制代码 合成后引发的问题如上图显示,在图形合成后,可能会造成其他已经停止的图形会有继续运动的趋势。
这里目前只认为需要继续运动的图形满足:
- 周围没有碰撞的图形。
- 碰撞的图形个数只有1个。
- 碰撞的图形个数为两个,但都在当前图形的上方。
部分代码如下:
- public class ComposeUtil {
- public static boolean dropProduct(List products, int width, int height) {
- boolean drop = false;
- for (Product product : products) {
- if (isStopProduct(product)) {
- if (product.getCenterY() + product.getRadius() >= height) {
- continue;
- }
- List occursProductArray = occursCollision(product, products);
- // 没有任何接触的图形可以掉落
- if (occursProductArray.size() == 0) {
- product.setSpeedY(ComposeUtil.BIT * NumbersUtil.COLLISION_SPEEDY);
- drop = true;
- }
-
- // 接触的图形仅1个需要掉落
- boolean result = dropProduct(occursProductArray, product, width);
- if (result) {
- drop = true;
- }
- // 碰撞的图形个数为两个,但都在当前图形的上方
- result = dropProduct(occursProductArray, product);
- if (result) {
- drop = true;
- }
- }
- }
- return drop;
- }
- }
复制代码 8. 儿童模式 上面四张图展示了儿童启动游戏后,需要请求周边手表与之通信,手表端授权是否可以开启游戏权限,如果拒绝,则手机端的游戏退出。
显示设备
这是调用流转任务管理服务来显示设备,具体步骤如下:
步骤 1 - config.json中声明多设备协同访问的权限:ohos.permission.DISTRIBUTED_DATASYNC。配置信息如下:
- "module": {
- "reqPermissions": [
- {
- "name": "ohos.permission.DISTRIBUTED_DATASYNC"
- }
- ]
- }
复制代码然后手动申请权限,代码如下:
- requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC"}, 0);
复制代码步骤 2 - 注册流转任务管理服务。代码如下:
- private IContinuationRegisterManager continuationRegisterManager;
-
- private void continuationRegister() {
- continuationRegisterManager = getContinuationRegisterManager();
- // 注册FA流转管理服务
- continuationRegisterManager.register(getBundleName(), new ExtraParams(), callback, requestCallback);
- }
复制代码步骤 3 - 设置注册流转任务管理服务回调,得到Ability token。代码如下:
- private int abilityToken;
-
- private RequestCallback requestCallback = new RequestCallback() {
- @Override
- public void onResult(int result) {
- abilityToken = result;
- }
- };
复制代码步骤 4 - 通过调用IContinuationRegisterManager类的showDeviceList()方法显示同一网段下的设备。代码如下:
- private void showDeviceList() {
- ExtraParams extraParams = new ExtraParams();
- extraParams.setDevType(new String[]{ExtraParams.DEVICETYPE_SMART_TV,
- ExtraParams.DEVICETYPE_SMART_PAD,
- ExtraParams.DEVICETYPE_SMART_PHONE});
- extraParams.setDescription("小设备合成");
- continuationRegisterManager.showDeviceList(abilityToken, extraParams, null);
- }
复制代码步骤 5 - 设置流转任务管理服务设备状态变更的回调,然后在其onDeviceConnectDone()方法中,会返回远程设备的唯一标识deviceId。代码如下:
- private IContinuationDeviceCallback callback = new IContinuationDeviceCallback() {
- @Override
- public void onDeviceConnectDone(String deviceId, String s1) {
- continuationRegisterManager.updateConnectStatus(abilityToken,deviceId, DeviceConnectState.IDLE.getState(), null);
- }
-
- @Override
- public void onDeviceDisconnectDone(String deviceId) {
- }
- };
复制代码 手机与手表通信这里采用分布式调度方式来进行双方通信,即启动FA的模式完成通信。
手机启动手表FA,这里使用同一个MainAbility不同的AbilitySlice来区分手机手表的页面。
- 在config.json中的MainAbility下配置action内容为"action.smart",代码如下
- "abilities": [
- {
- "skills": [
- {
- "actions": [
- "action.system.home",
- "action.smart"
- ]
- }
- ],
- "name": "com.huawei.codelab.MainAbility"
- }
- ]
复制代码
- 在MainAbility中配置action的页面为SmartWatchSlice,代码如下:
- @Override
- public void onStart(Intent intent) {
- super.onStart(intent);
- super.setMainRoute(MainAbilitySlice.class.getName());
- addActionRoute("action.smart", SmartWatchSlice.class.getName());
- }
复制代码
- 通过配置action启动该页面,同时设置远程设备id和分布式调度系统多设备启动的标识来启动远程FA,并将手机的设备id传到手表端。代码如下:
- private void startAbilityFA(String deviceId) {
- String localDeviceId = KvManagerFactory.getInstance().createKvManager(
- new KvManagerConfig(this)).getLocalDeviceInfo().getId();
- Intent intent = new Intent();
- Operation operation =
- new Intent.OperationBuilder()
- .withDeviceId(deviceId) // 手表端设备ID
- .withBundleName(getBundleName()) // 手机端与手表端同一个应用,所以使用同一个bundle
- .withAbilityName(MainAbility.class.getName())
- .withAction("action.smart") // 配置跳转的路由
- .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) // 分布式调度标识
- .build();
- intent.setOperation(operation);
- intent.setParam(MainAbilitySlice.DEVICEID, localDeviceId); // 手机端设备id传给手表端
- startAbility(intent);
- }
复制代码手表启动手机FA,然后手机端应用内部通过广播发送到MainSlice中。
- 在手表端的SmartWatchSlice类中启动手机端FA,设置传入的远程设备remoteDeviceId和分布式调度系统多设备启动的标识来启动远程FA。代码如下:
- private void sendMessage(int type) {
- Intent intent = new Intent();
- Operation operation =
- new Intent.OperationBuilder()
- .withDeviceId(remoteDeviceId) // 手机端设备ID
- .withBundleName(getBundleName()) // 手机端与手表端同一个应用,所以使用同一个bundle
- .withAbilityName(MainAbility.class.getName())
- .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) // 分布式调度标识
- .build();
- intent.setOperation(operation);
- intent.setParam(CODE, type); // type为是否同意授权的标识
- startAbility(intent);
- }
复制代码
- 在config.json中配置MainAbility的启动模式为单例模式,代码如下:
- "abilities": [
- {
- "name": "com.huawei.codelab.MainAbility"
- "launchType": "singleton"
- }
- ]
复制代码
- 当MainAbility设置为singleton模式时,并且MainAbility实例存在时,再调用startAbility()方法,会触发onNewIntent()方法的执行,在这里发送公共事件。代码如下:<
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- // 发送公共事件
- CommonEventManager.publishCommonEvent();
- }
复制代码
- 最后在MainAbilitySlice类中注册公共事件并处理该事件即可。代码如下:
- // 注册公共事件
- CommonEventManager.subscribeCommonEvent(new EventSubscriber());
-
- private class EventSubscriber extends CommonEventSubscriber {
- @Override
- public void onReceiveEvent(CommonEventData commonEventData) {
- // 处理公共事件
- }
- }
复制代码
9. 最终实现效果 10. 恭喜你 通过本篇codelab,你可以学到:
- Canvas绘制图形
- 线程间通信
- 图形合成思路分析
- 流转
11. 参考