安卓开发: Jetpack compose + kotlin 实现 俄罗斯方块游戏

Posted zyw2002

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓开发: Jetpack compose + kotlin 实现 俄罗斯方块游戏相关的知识,希望对你有一定的参考价值。

前言

项目地址:github 开源地址码云开源地址
项目背景:刚刚结束的暑假小学期中,和我的两个神仙队友WYX,XPF(我的队友超级棒!)共同完成的第一个安卓开发项目——俄罗斯方块游戏。
框架和语言:kotlin+ jetpack compose (compose 是谷歌最新推出的开发框架,androidStudio刚出了支持compose的稳定版本)
参考项目:参考了https://github.com/leavesC/compose_tetris
在此基础上增加了许多丰富的功能(如切换游戏难度,切换背景音乐,记录最高分…)

俄罗斯方块开发文档

应用名称:鹅螺蛳方块
应用类型:休闲游戏
小组成员:WYX、ZYW、XPF
开发时间:2021年7月26日 - 2021年8月14日

1.摘要

本组使用 Android studio 作为集成式开发环境,完全自主学习 Kotlin 编程语言和 Jetpack Compose 框架,编写了一个功能齐全、具有动态界面的俄罗斯方块安卓游戏。
主要工作包括:搭建 Compose Activity 项目环境,全栈开发实现俄罗斯方块的后端游戏逻辑、界面交互、前端界面设计、音效播放、动画效果等功能(详见“设计需求”一节)。
学习使用全新的 Jetpack Compose 框架是本项目最大的挑战。Jetpack Compose 是谷歌推出的新式声明性界面工具包 ,包括一整套全新的渲染、布局、事件、刷新机制,需要一段时间才能入门,但有效提高了本小组的开发效率。
本文档用于描述《计算机科学与技术专业实践与训练》课程所编写程序的设计方案,文档阅读对象为本课程授课教师及本课堂同学。

2.开发工具选取

Jetpack Compose 是用于构建原生界面的最新的 Android 工具包,结合了反应式编程模型和 Kotlin 编程语言的简洁性和易用性。

2.1.Compose 的自身优点

  • 采用声明式 UI 设计
  • 提供丰富的 Material 组件,加速开发
  • 具有直观的 Kotlin API
  • 拥有更简单的、自定义的实时交互预览功能

2.2.数据驱动界面

游戏的执行程序可以概括为不断等待用户的输入信息,进行状态查询,渲染界面的过程。而这种游戏模型的逻辑非常符合前端开发的思想:数据驱动界面。

当下的小游戏多以前端为主,客户端开发成本较大,而使用 Compose 可以降低开发成本。Compose 实现数据驱动是依赖类似 Flutter 的 Provide 一样的更新机制,但并没有采用采用了传统 UI 的多层继承结构,而是多个 View 组合成一个 View ,更新数据只要在使用的实体上面加注解 @Model ,在更新实体的同时,会通知 UI 进行修改。

3.设计需求

3.1.功能需求

3.1.1.基本游戏功能

  • 实现俄罗斯方块的游戏的基本逻辑:控制方块的运动和旋转
  • 实现游戏整体功能控制:开始游戏、暂停游戏、重新开始、音乐开启关闭;

3.1.2.拓展功能

  • 实现游戏高级设置:调整游戏难度,游戏界面主题切换,更改游戏背景音乐;
  • 增加计分系统,在游戏界面实时显示目前得分,统计历史最高得分;
  • 增加玩家档案信息,使玩家可以输入保存自己的姓名;
  • 增加游戏说明的信息,使得玩家了解游戏规则和技巧;
  • 实现关于我们页面:展示应用版本信息、开发团队的留言、产品设计理念;

3.2.界面需求

3.2.1.整体界面设计

  • 设计导航栏使得界面的跳转更加简明快捷。
  • 设计垂直滚动式页面增加单个页面展示的空间。
  • 设计白天与黑夜两种模式的界面配色,启动应用时会自动根据手机夜间模式切换,界面内包括太阳和月亮的动画。
  • 设计多种模式的渐变色按钮,增加按钮的质感以提升玩家的体验。
  • 设计多种风格的字体样式以区分不同的内容和突出重点。
  • 设计多种矢量图标,使得信息的展现更加形象。

3.2.2.特色界面设计

  • 游戏屏幕可由用户自己调节大小,并当调节范围超出最大和最小的界限时,设计弹窗提醒用户。
  • 游戏屏幕边框实现色彩的渐变,增添游戏的炫酷感。
  • 设计手势控制的旋转圆形头像,可变带宽和模糊度的圆环边框,可变模糊度和曲线形状的顶部背景图片。
  • 设计 3D 翻折动画,隐藏和展开游戏说明,使得界面更加简洁灵动。
  • 设计水墨渲染动画,通过手势将灰度图片点亮,增添界面的高级感。
  • 设计上下两个图层,通过下图层的阴影使得可360°翻转的图片有肉眼3D的效果
  • 设计星空背景和火箭发射的动画,更形象的展示我们的理念和深层寓意。
  • 设计色彩渐变的字体,是界面更加有趣美观。
  • 仿照胶片样式,设计横向滚动的图像卡片,展示开发者留言。
  • 设计关键信息可复制的文本,展示项目地址,开发者联系方式。

4.项目文件及其功能

4.1.游戏架构基于 MVI 设计

MVI 即 Model-View-Intent ,提倡一种单向数据流的设计思想,非常适合在 Compose 项目中实现逻辑部分,可以彻底贯彻“数据驱动 UI”的核心思想。

  • View 层:基于 Compose 打造,所有 UI 元素都由代码实现
  • Model 层:ViewModel 维护 State 的变化,游戏逻辑交由 reduce 处理
  • V-M 通信:通过 StateFlow 驱动 Compose 刷新,用户事件由 Action 分发至 ViewModel

4.2.源文件功能简述表

如下表所示,所有源代码部分都在 com.android_tetris 包内:

4.3 功能索引



5.遇到的困难和解决方案

5.1.零基础学习新的语言和框架

本程序完全使用 Kotlin 语言开发,并选用 Jetpack Compose 框架作为前端框架。
我们完全自主学习了 Jetpack Compose (以下简称 Compose )这个还未推出正式版的声明式 UI 框架。由于谷歌暂未推出 Compose 的正式版本,现在网上相关资料和教程都还非常稀少,API文档还是全英文的,我们常常在意想不到的问题上被卡住还没有什么办法。
虽然入门过程极其艰难,但是在学习使用 Compose 的全新框架开发的过程中,我们的英语水平、信息检索能力、解决问题的能力,都得到了极大的锻炼,相信这段艰苦的自主学习经历对我们之后的编程之路一定有所帮助。

5.2.使用 Gitee 托管仓库

由于这是一个工程量比较大的项目,我们采用 Gitee 进行版本管理。但在使用过程中,我们上传经常遇到“文件超过 100M ”的错误提示。
为解决这一问题,我们学习了 .gitignore 文件的使用方法:在该文件按优先级从高到低,写明让 Git 仓库上传时忽略掉的文件目录/后缀名, Git 就会主动忽略这些文件。
我们可以用这一办法处理项目编译产生的文件和庞大的开发环境文件。

5.3.编辑 build.gradle 环境配置文件

开发初期,由于 Kotlin 和 Compose 还在频繁更新 API ,版本迭代很快,因此我们遇到了很多次 build 不成功的情况,报错位置都出现在 build.gradle 文件。
查阅资料发现, Project 层级和 Module 层级各有相对应的两个 build.gradle 文件,在 Android studio 中分别显示为 build.gradle (Project: 项目名) 和 build.gradle (Module: 项目名.app) 。
在 build.gradle (Project: 项目名) 中, buildscript { } 代码块是该项目的 gradle 配置,只存放用到的代码托管库和项目构建级别的依赖:

buildscript {
    ext {
        compose_version = '1.0.0-beta08'
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.1.0-alpha02'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        // (此处的注释提示:不要把应用程序的依赖库在此引用)
    }
}

在 build.gradle (Module: 项目名.app) 中, build.gradle 主要用于配置模块级别的配置信息和依赖:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdk 30
    buildToolsVersion "30.0.3"
    defaultConfig {
        applicationId "com.android_tetris"
        minSdk 21
        targetSdk 30
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }
    
    // 输出 APK 用到的应用签名信息
    signingConfigs {
        releaseConfig {
            storeFile file("..\\\\key.jks")
            storePassword "123456"
            keyAlias "key0"
            keyPassword "123456"
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }

    buildTypes {
        debug {
            signingConfig signingConfigs.releaseConfig
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        release {
            signingConfig signingConfigs.releaseConfig
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
        kotlinCompilerVersion '1.5.10'
    }
}

dependencies {
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    ...
}

5.4.用 Kotlin 编写游戏逻辑实现 Model 层次

为遵守 MVI 设计逻辑,对于游戏界面,所有会由游戏内行为发出再导致前端显示刷新的变化,都集中存放在 TetrisState.kt 文件中。界面的所有变化都依赖后端 State 的变化而刷新。
俄罗斯方块共有 7 种形状,每种形状有随着旋转还会有几种不同形态,本项目在 MockData.kt 文件中选用 44 的二维 IntArray 数组储存方块形状,在 TetrisState.kt 中选用 1024 的二维 IntArray 数组表示存放方块的屏幕。
以下是 TetrisState.kt 文件中一部分重要函数和类的代码:

// class Tetris:对于单个俄罗斯方块的 State 状态类
data class Tetris constructor(
    val shapes: List<List<Location>>, // 此方块所有可能的旋转结果
    val type: Int,                 // 用于标记当前处于哪种旋转状态
    val offset: Location,          // 方块相对屏幕左上角的偏移量
) { ... }

// class TetrisState:游戏的 State 状态类
data class TetrisState(
    val brickArray: Array<IntArray>,    //  屏幕坐标系
    val tetris: Tetris,                 //  下落的方块
    val gameStatus: GameStatus = GameStatus.Welcome, //  游戏状态
    val soundEnable: Boolean = true,    //  是否开启音效
    val nextTetris: Tetris = Tetris(),  //  下一个方块
    var currentScore: Int = infoStorage.currentScore //  当前分数
) { ... }

// class Action:接受用户 View 层次传入信号的 Action 类
sealed class Action {            
    object Welcome : Action()
    object Start : Action()
    object Pause : Action()
    object Reset : Action()
    object Sound : Action()
    object Settings : Action()
    object Background : Action()
    object Resume : Action()
    data class Transformation(val transformationType: TransformationType) : Action()
}

// fun combinedPlayListener() 函数:Action 行为的监听器,监听器接收到信号后,分别再发送给 private fun TetrisState.onStart() 等 TetrisState 的私有函数,进行处理
fun combinedPlayListener(        
    onStart: () -> Unit = {},
    onPause: () -> Unit = {},
    onReset: () -> Unit = {},
    onSound: () -> Unit = {},
    onSettings: () -> Unit = {},
    onTransformation: (TransformationType) -> Unit = {}
) = PlayListener(
    onStart = onStart,
    onPause = onPause,
    onReset = onReset,
    onSound = onSound,
    onSettings = onSettings,
    onTransformation = onTransformation
)

5.5.View 层和 Model 层的交互机制
以下是 TetrisViewModel 类声明部分代码 :

class TetrisViewModel : ViewModel() {
    //功能:接收 Action 信号,由用户的动作改变 ViewModel
	fun dispatch(action: Action) {...}  
    //功能:改变下落速度
    fun changeDownSpeed(newSpeed :Long){...}  
    //功能:初始进入软件时的欢迎效果
    private fun onWelcome() {...}
    //功能:开始游戏
    private fun onStartGame(){...}
    //功能:暂停游戏
    private fun onPauseGame(){...}
    //功能:结束游戏
    private fun onGameOver(){...}
    //功能:开始清空界面
    private fun startClearScreenJob(invokeOnCompletion: () -> Unit){...}
    //功能:取消清空界面
    private fun cancelClearScreenJob(){...}
    //功能:方块下降
    private fun startDownJob() {...}
    //功能:暂停方块下降
    private fun cancelDownJob() {...}
    //功能:将当前界面状态传递给用户
    private fun dispatchState(tetrisState: TetrisState) {..}
    //功能:游戏背景音乐播放
    private fun playSound(action: Action) {...}
    //功能:播放不同类型的背景音乐
    private fun playSound(soundType: SoundType){...}
}

5.6.基于 Compose 实现 View 层次

由于文档篇幅有限,详细的媒体内容展示可参考源代码、 PPT、和应用展示视频。

俄罗斯方块游戏屏幕

主要用到了 Compose 的自定义图像核心可组合项 Canvas ,预览效果如下图:

主要逻辑功能包括:绘制游戏屏幕背景、绘制以难度指定速度不断下落的方块、为方块提供按键移动功能、判断是否进行消行、如果方块超出当前屏幕则结束游戏。
代码如下:

// 源文件:TetrisScreen.kt 
package com.android_tetris.ui
import...
@Composable
fun TetrisScreen(tetrisState: TetrisState) {...  
    Canvas(modifier=Modifier.(...)){...
        kotlin.run { //绘制方块矩阵
            screenMatrix.forEachIndexed { y, ints ->
                ints.forEachIndexed { x, isFill ->
                    translate(...) {drawBrick(...)}
                }
            }
        } 
        kotlin.run { ...drawPath(...)}//绘制下落方块        
        kotlin.run { drawRightPanel(...)}//绘制右侧得分栏   
		kotlin.run { drawHint(...)} //绘制提示文字
    }
}
//绘制单个方块
fun DrawScope.drawBrick(brickSize: Float,//每一个方块的size
                        color: Color,//砖块颜色
                        background: Color //背景颜色
) {...
    translate(...) {drawRect(...)}//绘制外部矩形边框
   	translate(...) {drawRect(...)}//绘制内部矩形边框
}
private fun DrawScope.drawRightPanel(...) {...}//绘制右侧得分栏
private fun DrawScope.drawHint(...) {...}//绘制提示文字

俄罗斯方块游戏按钮布局

预览效果如下图:

ConstraintLayout 是一种根据可组合项的相对位置关系显示的布局类型,代码如下:

//TetrisButton.kt
fun TetrisButton(
    playListener: PlayListener = combinedPlayListener()
) {
	Column(...){
		Row(...){
			ControlButton(...){ playListener.onStart()}//开始游戏
			ControlButton(...){ playListener.onPause()}//暂停游戏
			ControlButton(...){ playListener.onReset()}//重新开始
			ControlButton(...){ playListener.onSound()}//音乐开关
		}
		ConstraintLayout(...){
			PlayButton(...){playListener.onTransformation(Rotate)}//旋转方块
			PlayButton(...){playListener.onTransformation(Fall)}//方块加速下落
			PlayButton(...){ playListener.onTransformation(Left)}//方块左移
			PlayButton(...){playListener.onTransformation(Right)}//方块右移
			PlayButton(...){playListener.onTransformation(FastDown)}//方块直接降落到最底部
		}	
	}
}

俄罗斯方块游戏机背景

预览效果如下图:

代码如下:
// TetrisBody.kt
package com.android_tetris.ui
fun TetrisBody(
    tetrisScreen: @Composable (() -> Unit), 
    tetrisButton: @Composable (() -> Unit),
){...
  	val size by animateDpAsState(...)//改变大小状态
  	val color by infiniteTransition.animateColor(..,)//改变颜色状态
	Column(...){
		TopBar(...){
			Row(...){
				Icon(...); Text(...)//转到setting界面
				Icon(...); Text(...)//转到More界面
				Icon(...); Text(...)//增大屏幕
				Icon(...); Text(...)//减小屏幕
			}
		}
		Box(...){
			Column(...){ tetrisScreen()}//游戏屏幕区
		}
	}
	tetrisButton()//游戏按钮区
}

最终主界面的整体组合效果:

游戏设置页面

预览效果如下图:

代码如下:

//TetrisSettingScreen.kt
fun SettingsScreen(){
	Box(modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())//垂直滚动
	){
		Box(...){
			Box{
				Column(...){
					Box(...){
						 LoginPageTopBlurImage(...)//顶部模糊背景
						 LoginPageTopRotaAndScaleImage(...)//顶部旋转头像
					}
					Column(...){
						Row{
							Text(...)//game setting 图标
							if(...){... PlanetMoon()}//黑夜模式展示月亮动画
							else AnimateSun(Modifier.size(50.dp))//白天模式展示太阳
						}
						ConstraintLayout(...){...
							Row(...){...//难度选择
								Column(...){
									 RadioselectionButton(label = "Easy",...)//简单
									 RadioSelectionButton(label = "Normal",...)//普通
									 RadioSelectionButton(label = "Hard",...)//困难
								}
							}
							Row(..以上是关于安卓开发: Jetpack compose + kotlin 实现 俄罗斯方块游戏的主要内容,如果未能解决你的问题,请参考以下文章

安卓开发: Jetpack compose + kotlin 实现 俄罗斯方块游戏

用Jetpack Compose构建更快的安卓应用

Android安卓进阶技术之——Jetpack Compose

Android高级Jetpack架构组件+Jetpack compose强化实战

Android原生UI开发框架 《Jetpack Compose入门到精通》最全上手指南

Android全新UI编程 - Jetpack Compose 超详细教程