├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── font
│ │ │ │ └── pac_font.TTF
│ │ │ ├── drawable
│ │ │ │ ├── ghost_orange.png
│ │ │ │ ├── arrow_up.xml
│ │ │ │ ├── arrow_down.xml
│ │ │ │ ├── arrow_right.xml
│ │ │ │ ├── arrow_left.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── drawable-v24
│ │ │ │ ├── ghost_red.png
│ │ │ │ ├── ghost_reverse.png
│ │ │ │ ├── pacman_open.png
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ └── values-night
│ │ │ │ └── themes.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── dbtechprojects
│ │ │ │ └── pacmancompose
│ │ │ │ ├── models
│ │ │ │ ├── DialogState.kt
│ │ │ │ ├── GameStatsModel.kt
│ │ │ │ ├── EnemyMovementModel.kt
│ │ │ │ └── PacFood.kt
│ │ │ │ ├── utils
│ │ │ │ └── GameConstants.kt
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Shape.kt
│ │ │ │ └── GameComposables.kt
│ │ │ │ ├── GameViewModel.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── dbtechprojects
│ │ │ └── pacmancompose
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── dbtechprojects
│ │ └── pacmancompose
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── .idea
├── .gitignore
├── compiler.xml
├── vcs.xml
├── misc.xml
├── deploymentTargetDropDown.xml
└── gradle.xml
├── resources
├── demo1.gif
└── demo2.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/resources/demo1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/resources/demo1.gif
--------------------------------------------------------------------------------
/resources/demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/resources/demo2.gif
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | pacmanCompose
3 |
--------------------------------------------------------------------------------
/app/src/main/res/font/pac_font.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/font/pac_font.TTF
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ghost_orange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/drawable/ghost_orange.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ghost_red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/drawable-v24/ghost_red.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ghost_reverse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/drawable-v24/ghost_reverse.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/pacman_open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/drawable-v24/pacman_open.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielmbutler/Pacman_Compose/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jun 19 21:23:03 BST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/models/DialogState.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.models
2 |
3 | import androidx.compose.runtime.MutableState
4 |
5 | data class DialogState (
6 | val shouldShow: MutableState,
7 | val message: MutableState,
8 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter() // Warning: this repository is going to shut down soon
7 | }
8 | }
9 | rootProject.name = "pacmanCompose"
10 | include ':app'
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/utils/GameConstants.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.utils
2 |
3 | object GameConstants {
4 | const val FOOD_COUNTER = 100 // amount of food
5 | var incrementValue = 75f // amount to move the character
6 | const val RED_ENEMY_SPEED = 3000
7 | const val ORANGE_ENEMY_SPEED = 2500
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_up.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_down.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_right.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_left.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/models/GameStatsModel.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.models
2 |
3 | import androidx.compose.runtime.MutableState
4 |
5 | data class GameStatsModel(
6 | val CharacterXOffset: MutableState,
7 | val CharacterYOffset: MutableState,
8 | val isGameStarted: MutableState,
9 | val isReverseMode: MutableState
10 |
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/models/EnemyMovementModel.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.models
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.ui.geometry.Offset
6 |
7 | data class EnemyMovementModel (
8 | val redEnemyMovement: MutableState = mutableStateOf(Offset(0F, 0F)),
9 | val orangeEnemyMovement: MutableState = mutableStateOf(Offset(0F, 0F))
10 | )
--------------------------------------------------------------------------------
/app/src/test/java/com/dbtechprojects/pacmancompose/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.ui.theme
2 |
3 |
4 | import androidx.compose.ui.graphics.Color
5 |
6 | val PacmanYellow = Color(0xFFFFFF00)
7 | val PacmanRed = Color(0xFFFD0000)
8 | val PacmanPink = Color(0xFFdea185)
9 | val PacmanGreen = Color(0xFF00ff00)
10 | val PacmanWhite = Color(0xFFFFFFFF)
11 | val PacmanBackground = Color(0xFF000000)
12 | val PacmanMazeColor = Color(0xFF0051ff)
13 | val PacmanOrange = Color(0xFFffa500)
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.sp
9 | import com.dbtechprojects.pacmancompose.R
10 |
11 |
12 | // Set of Material typography styles to start with
13 | val Typography = Typography(
14 | body1 = TextStyle(
15 | fontFamily = FontFamily.Default,
16 | fontWeight = FontWeight.Normal,
17 | fontSize = 16.sp
18 | )
19 | )
20 |
21 | val HeaderFont = FontFamily(listOf(Font(R.font.pac_font)))
22 |
23 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/dbtechprojects/pacmancompose/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.dbtechprojects.pacmancompose", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pacman Compose
2 | This Pacman style game is entirely created using Jetpack Compose, still currently a work-in progress.
3 |
4 |
5 |
6 |
7 |
8 |
9 | ## Demo
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Technology used
19 | * All UI created using Jetpack Compose,
20 | * Kotlin
21 | * Animations - used AnimateFloatAsState, and InfiniteTransition
22 | * Canvas API
23 |
24 | ## Article on Hashnode
25 | https://dbtechprojects.hashnode.dev/my-first-attempt-at-creating-a-game-with-jetpack-compose
26 |
27 | Featured in Android Weekly: https://androidweekly.net/issues/issue-475
28 |
29 | ## Resources
30 | https://developer.android.com/jetpack/compose/animation#overview
31 | https://developer.android.com/jetpack/compose/graphics
32 | https://jeffreyrajan.medium.com/canvas-made-easy-in-jetpack-compose-2c632518f8bf
33 |
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = PacmanYellow,
11 | primaryVariant = PacmanWhite,
12 | secondary = PacmanBackground
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = PacmanYellow,
17 | primaryVariant = PacmanWhite,
18 | secondary = PacmanBackground
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun PacmanComposeTheme(
32 | darkTheme: Boolean = isSystemInDarkTheme(),
33 | content: @Composable() () -> Unit
34 | ) {
35 | val colors = if (darkTheme) {
36 | DarkColorPalette
37 | } else {
38 | LightColorPalette
39 | }
40 |
41 | MaterialTheme(
42 | colors = colors,
43 | typography = Typography,
44 | shapes = Shapes,
45 | content = content
46 | )
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.ui.theme
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.layout.size
5 | import androidx.compose.foundation.shape.GenericShape
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material.Shapes
8 | import androidx.compose.material.Surface
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.geometry.Rect
12 | import androidx.compose.ui.geometry.Size
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.Outline
15 | import androidx.compose.ui.graphics.Path
16 | import androidx.compose.ui.graphics.Shape
17 | import androidx.compose.ui.unit.Density
18 | import androidx.compose.ui.unit.LayoutDirection
19 | import androidx.compose.ui.unit.dp
20 |
21 | val Shapes = Shapes(
22 | small = RoundedCornerShape(4.dp),
23 | medium = RoundedCornerShape(4.dp),
24 | large = RoundedCornerShape(0.dp)
25 | )
26 |
27 | @Composable
28 | fun MyStarTrek() {
29 | val myStarTrekShape = GenericShape { size, _ ->
30 | moveTo(size.width / 2f, 0f)
31 | lineTo(size.width, size.height)
32 | quadraticBezierTo(
33 | size.width * 0.6f,
34 | size.height * 0.4f,
35 | 0f,
36 | size.height
37 | )
38 | close()
39 | }
40 | Surface(
41 | shape = myStarTrekShape,
42 | color = Color.Yellow,
43 | border = BorderStroke(3.dp, Color.Black),
44 | modifier = Modifier.size(100.dp)
45 | ) { }
46 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/models/PacFood.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.models
2 |
3 | import android.util.Log
4 | import com.dbtechprojects.pacmancompose.utils.GameConstants
5 | import kotlin.random.Random
6 |
7 |
8 | data class PacFood(
9 | val foodList: ArrayList = ArrayList(),
10 | val bonusFoodList: ArrayList = ArrayList() // bonus food which reverses the enemy path back to their box
11 | ) {
12 | init {
13 | initPacFoodList()
14 | initBonusPacFoodList()
15 | }
16 |
17 | private fun initPacFoodList() {
18 | foodList.clear()
19 |
20 |
21 |
22 | for (i in 0 until GameConstants.FOOD_COUNTER) {
23 | val food = PacFoodModel(
24 | xPos = Random.nextInt(85, 850),
25 | yPos = Random.nextInt(85, 1200),
26 | size = 0.5f
27 | )
28 | Log.w("food", "${food.xPos}")
29 | foodList.add(food)
30 |
31 | }
32 | }
33 |
34 | private fun initBonusPacFoodList() {
35 | bonusFoodList.clear()
36 |
37 | // topLeft
38 | bonusFoodList.add(
39 | PacFoodModel(
40 | xPos = 90,
41 | yPos = 85,
42 | size = 1f
43 | )
44 | )
45 |
46 | // top right
47 | bonusFoodList.add(PacFoodModel(
48 | xPos = 825,
49 | yPos = 85,
50 | size = 1f
51 | ))
52 |
53 | // bottom left
54 | bonusFoodList.add(PacFoodModel(
55 | xPos = 90,
56 | yPos = 1150,
57 | size = 1f
58 | ))
59 |
60 | // bottom Right
61 | bonusFoodList.add(PacFoodModel(
62 | xPos = 825,
63 | yPos = 1150,
64 | size = 1f
65 | ))
66 |
67 |
68 | }
69 |
70 | fun initRedraw() {
71 | initPacFoodList()
72 | initBonusPacFoodList()
73 | }
74 | }
75 |
76 |
77 | data class PacFoodModel(
78 | var xPos: Int,
79 | var yPos: Int,
80 | var size: Float
81 | )
82 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | compileSdk 30
8 | buildToolsVersion "30.0.3"
9 |
10 | defaultConfig {
11 | applicationId "com.dbtechprojects.pacmancompose"
12 | minSdk 21
13 | targetSdk 30
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_1_8
31 | targetCompatibility JavaVersion.VERSION_1_8
32 | }
33 | kotlinOptions {
34 | jvmTarget = '1.8'
35 | useIR = true
36 | }
37 | buildFeatures {
38 | compose true
39 | }
40 | composeOptions {
41 | kotlinCompilerExtensionVersion compose_version
42 | kotlinCompilerVersion '1.4.32'
43 | }
44 | }
45 |
46 | dependencies {
47 |
48 | implementation 'androidx.core:core-ktx:1.5.0'
49 | implementation 'androidx.appcompat:appcompat:1.3.0'
50 | implementation 'com.google.android.material:material:1.3.0'
51 | implementation "androidx.compose.ui:ui:$compose_version"
52 | implementation "androidx.compose.material:material:$compose_version"
53 | implementation "androidx.compose.ui:ui-tooling:$compose_version"
54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
55 | implementation 'androidx.activity:activity-compose:1.3.0-beta02'
56 | testImplementation 'junit:junit:4.+'
57 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
58 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
59 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
60 |
61 | // compose animation
62 | implementation 'androidx.compose.animation:animation:1.0.0-beta09'
63 |
64 | // constraint layout for compose
65 | implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha08"
66 |
67 | // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
68 | implementation("androidx.compose.foundation:foundation:1.0.0-beta09")
69 |
70 | // livedata in Compose
71 | implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-beta09'
72 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/GameViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose
2 |
3 | import android.util.Log
4 | import android.util.Range
5 | import androidx.compose.runtime.MutableState
6 | import androidx.lifecycle.LiveData
7 | import androidx.lifecycle.MutableLiveData
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import com.dbtechprojects.pacmancompose.utils.GameConstants.incrementValue
11 | import kotlinx.coroutines.*
12 |
13 |
14 | class GameViewModel : ViewModel() {
15 |
16 | private val logTag = "GameViewModel"
17 | private var leftPress: Boolean = false
18 | private var rightPress: Boolean = false
19 | private var downPress: Boolean = false
20 | private var upPress: Boolean = false
21 |
22 |
23 | /* handles the direction the character is facing
24 | start angle
25 | when going left = 25f
26 | when going right = 200f
27 | when going down = 100f
28 | when going up = 280f
29 | */
30 | private var _characterStartAngle = MutableLiveData(25f)
31 | val characterStartAngle: LiveData
32 | get() = _characterStartAngle
33 |
34 |
35 | // handle presses
36 | fun rightPress(characterXOffset: MutableState, characterYOffset: MutableState) {
37 | rightPress = true
38 | _characterStartAngle.postValue(25f) // change direction character is facing
39 | viewModelScope.launch {
40 | while (rightPress) {
41 | delay(500)
42 | // move character to opposite wall
43 | if (characterXOffset.value > 315f) characterXOffset.value = -400f
44 |
45 | // implement barrier constraints
46 |
47 | if (
48 | //Top Right
49 | Range.create(-310f, -225f).contains(characterXOffset.value) &&
50 | Range.create(-975f, -900f).contains(characterYOffset.value) ||
51 | // Top Left
52 | Range.create(75f, 150f).contains(characterXOffset.value) &&
53 | Range.create(-975f, -900f).contains(characterYOffset.value) ||
54 | // EnemyBox
55 | Range.create(-150f, -75f).contains(characterXOffset.value) &&
56 | Range.create(-450f, -375f).contains(characterYOffset.value) ||
57 | // Bottom Left
58 | Range.create(-310f, -225f).contains(characterXOffset.value) &&
59 | Range.create(-150f, -75f).contains(characterYOffset.value) ||
60 | // Bottom Right
61 | Range.create(75f, 150f).contains(characterXOffset.value) &&
62 | Range.create(-150f, -75f).contains(characterYOffset.value)
63 |
64 | ) characterXOffset.value += 0f else characterXOffset.value += incrementValue
65 |
66 | Log.d(
67 | logTag,
68 | "rightpress: x: ${characterXOffset.value} y: ${characterYOffset.value}"
69 | )
70 |
71 | }
72 | }
73 | }
74 |
75 | fun leftPress(characterXOffset: MutableState, characterYOffset: MutableState) {
76 | leftPress = true
77 | _characterStartAngle.postValue(200f) // change direction character is facing
78 | viewModelScope.launch {
79 | while (leftPress) {
80 | delay(500)
81 | // move character to opposite wall
82 | if (characterXOffset.value <= -290f) characterXOffset.value = +450f
83 |
84 | // implement barrier constraints
85 |
86 | if (
87 | //Top Right
88 | Range.create(350f, 425f).contains(characterXOffset.value) &&
89 | Range.create(-975f, -900f).contains(characterYOffset.value) ||
90 | // Top Left
91 | Range.create(-150f, -75f).contains(characterXOffset.value) &&
92 | Range.create(-975f, -900f).contains(characterYOffset.value) ||
93 | // EnemyBox
94 | Range.create(75f, 150f).contains(characterXOffset.value) &&
95 | Range.create(-450f, -375f).contains(characterYOffset.value) ||
96 | // Bottom Left
97 | Range.create(-150f, -75f).contains(characterXOffset.value) &&
98 | Range.create(-150f, -75f).contains(characterYOffset.value) ||
99 | // Bottom Right
100 | Range.create(225f, 300f).contains(characterXOffset.value) &&
101 | Range.create(-150f, -75f).contains(characterYOffset.value)
102 |
103 | ) characterXOffset.value -= 0f else characterXOffset.value -= incrementValue
104 |
105 | Log.d(
106 | logTag,
107 | "leftPress: X: ${characterXOffset.value} Y: ${characterYOffset.value}"
108 | )
109 |
110 | }
111 | }
112 | }
113 |
114 | fun upPress(characterYOffset: MutableState, characterXOffset: MutableState) {
115 | upPress = true
116 | _characterStartAngle.postValue(280f) // change direction character is facing
117 | viewModelScope.launch {
118 | while (upPress) {
119 | delay(500)
120 |
121 | // implement barrier constraints
122 |
123 | if (
124 | // keep inside border
125 | characterYOffset.value <= -1000f ||
126 | //Top Right
127 | Range.create(150f, 300f).contains(characterXOffset.value) &&
128 | Range.create(-975f, -900f).contains(characterYOffset.value) ||
129 | // Top Left
130 | Range.create(-225f, -75f).contains(characterXOffset.value) &&
131 | Range.create(-975f, -900f).contains(characterYOffset.value) ||
132 | // EnemyBox
133 | Range.create(-75f, 150f).contains(characterXOffset.value) &&
134 | Range.create(-375f, -300f).contains(characterYOffset.value) ||
135 | // Bottom Left
136 | Range.create(-225f, -0f).contains(characterXOffset.value) &&
137 | Range.create(-150f, -75f).contains(characterYOffset.value) ||
138 | // Bottom Right
139 | Range.create(150f, 300f).contains(characterXOffset.value) &&
140 | Range.create(-150f, -75f).contains(characterYOffset.value)
141 |
142 | ) characterYOffset.value -= 0f else characterYOffset.value -= incrementValue
143 |
144 | Log.d(logTag, "UpPress: Y: ${characterYOffset.value} x: ${characterXOffset.value}")
145 | }
146 | }
147 | }
148 |
149 | fun downPress(characterYOffset: MutableState, characterXOffset: MutableState) {
150 | downPress = true
151 | _characterStartAngle.postValue(100f) // change direction character is facing
152 | viewModelScope.launch {
153 | while (downPress) {
154 | delay(500)
155 |
156 | if (
157 | // keep inside border
158 | characterYOffset.value >= 0f ||
159 | //Top Right
160 | Range.create(125f, 300f).contains(characterXOffset.value) &&
161 | Range.create(-1050f, -825f).contains(characterYOffset.value) ||
162 | // Top Left
163 | Range.create(-250f, -75f).contains(characterXOffset.value) &&
164 | Range.create(-1050f, -825f).contains(characterYOffset.value) ||
165 | // EnemyBox
166 | Range.create(-75f, 150f).contains(characterXOffset.value) &&
167 | Range.create(-450f, -375f).contains(characterYOffset.value) ||
168 | // Bottom Left
169 | Range.create(-225f, -0f).contains(characterXOffset.value) &&
170 | Range.create(-225f, -200f).contains(characterYOffset.value) ||
171 | // Bottom Right
172 | Range.create(150f, 300f).contains(characterXOffset.value) &&
173 | Range.create(-225f, -200f).contains(characterYOffset.value)
174 |
175 | ) characterYOffset.value += 0f else characterYOffset.value += incrementValue
176 |
177 | Log.d(logTag, "downPress: y: ${characterYOffset.value}, x: ${characterXOffset.value}")
178 |
179 | }
180 | }
181 | }
182 |
183 | //Cancel presses
184 |
185 | fun releaseLeft() {
186 | leftPress = false
187 | }
188 |
189 | fun releaseRight() {
190 | rightPress = false
191 | }
192 |
193 | fun releaseUp() {
194 | upPress = false
195 | }
196 |
197 | fun releaseDown() {
198 | downPress = false
199 | }
200 |
201 |
202 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.util.Range
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.viewModels
9 | import androidx.compose.foundation.ExperimentalFoundationApi
10 | import androidx.compose.foundation.background
11 | import androidx.compose.foundation.layout.Column
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.material.Surface
16 | import androidx.compose.material.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.MutableState
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.unit.dp
24 | import androidx.compose.ui.unit.sp
25 | import com.dbtechprojects.pacmancompose.models.DialogState
26 | import com.dbtechprojects.pacmancompose.models.EnemyMovementModel
27 | import com.dbtechprojects.pacmancompose.models.GameStatsModel
28 | import com.dbtechprojects.pacmancompose.models.PacFood
29 | import com.dbtechprojects.pacmancompose.ui.Controls
30 | import com.dbtechprojects.pacmancompose.ui.GameBorder
31 | import com.dbtechprojects.pacmancompose.ui.theme.HeaderFont
32 | import com.dbtechprojects.pacmancompose.ui.theme.PacmanBackground
33 | import com.dbtechprojects.pacmancompose.ui.theme.PacmanComposeTheme
34 | import com.dbtechprojects.pacmancompose.ui.theme.PacmanYellow
35 | import com.dbtechprojects.pacmancompose.utils.GameConstants
36 | import kotlinx.coroutines.InternalCoroutinesApi
37 |
38 | class MainActivity : ComponentActivity() {
39 |
40 | private val gameViewModel: GameViewModel by viewModels()
41 | private lateinit var gameStarted: MutableState
42 | private lateinit var reverseMode: MutableState
43 | private lateinit var characterYOffset: MutableState
44 | private lateinit var characterXOffset: MutableState
45 | private lateinit var enemyMovementModel: MutableState
46 | private lateinit var gameOverDialogState: DialogState
47 | private lateinit var pacFoodState: PacFood
48 | private lateinit var gameStatsModel: GameStatsModel
49 | private lateinit var foodCounter: MutableState
50 |
51 | @InternalCoroutinesApi
52 | @ExperimentalFoundationApi
53 | override fun onCreate(savedInstanceState: Bundle?) {
54 | super.onCreate(savedInstanceState)
55 | setContent {
56 | gameStarted = remember { mutableStateOf(false) }
57 | reverseMode = remember { mutableStateOf(false) } // used when bonus food is eaten
58 | characterYOffset = remember { mutableStateOf(0f) }
59 | characterXOffset = remember { mutableStateOf(0f) }
60 | gameStatsModel = GameStatsModel(characterXOffset, characterYOffset, gameStarted , reverseMode)
61 | enemyMovementModel = remember { mutableStateOf(EnemyMovementModel()) }
62 | gameOverDialogState = remember {
63 | DialogState(
64 | shouldShow = mutableStateOf(false),
65 | mutableStateOf("")
66 | )
67 | }
68 | foodCounter = remember { mutableStateOf(100) }
69 | pacFoodState = remember { PacFood() }
70 | PacmanComposeTheme {
71 | // A surface container using the 'background' color from the theme
72 | Surface(color = MaterialTheme.colors.background) {
73 | MainScreenContent()
74 | }
75 | }
76 |
77 | gameLoop(
78 | gameStatsModel = gameStatsModel,
79 | pacFoodState = pacFoodState,
80 | enemyMovementModel = enemyMovementModel.value,
81 | foodCounter = foodCounter
82 | )
83 | }
84 | }
85 |
86 | @ExperimentalFoundationApi
87 | @Composable
88 | private fun MainScreenContent() {
89 | Column(
90 | modifier = Modifier
91 | .background(color = PacmanBackground)
92 | .padding(16.dp)
93 | .fillMaxSize()
94 | ) {
95 | Text(
96 | text = "Pacman",
97 | fontSize = 36.sp,
98 | fontFamily = HeaderFont,
99 | color = PacmanYellow,
100 | modifier = Modifier
101 | .align(Alignment.CenterHorizontally)
102 | .padding(6.dp)
103 | )
104 | GameBorder(
105 | gameViewModel = gameViewModel,
106 | gameStatsModel = gameStatsModel,
107 | pacFoodState = pacFoodState,
108 | resources = resources,
109 | enemyMovementModel = enemyMovementModel.value,
110 | gameOverDialogState = gameOverDialogState,
111 | redEnemyDrawable = R.drawable.ghost_red,
112 | orangeEnemyDrawable = R.drawable.ghost_orange,
113 | reverseEnemyDrawable = R.drawable.ghost_reverse,
114 |
115 | )
116 | Controls(
117 | gameStatsModel = gameStatsModel,
118 | gameViewModel = gameViewModel,
119 | upButtonDrawable = R.drawable.arrow_up,
120 | leftButtonDrawable = R.drawable.arrow_left,
121 | downButtonDrawable = R.drawable.arrow_down,
122 | rightButtonDrawable = R.drawable.arrow_right
123 | )
124 | }
125 | }
126 |
127 | private fun gameLoop(
128 | foodCounter: MutableState,
129 | pacFoodState: PacFood,
130 | enemyMovementModel: EnemyMovementModel,
131 | gameStatsModel: GameStatsModel,
132 | ) {
133 |
134 | if (gameStatsModel.isGameStarted.value) {
135 | // Collision Check
136 | val characterX = 958.0f / 2 - 90f + gameStatsModel.CharacterXOffset.value
137 | val characterY = 1290.0f - 155f + gameStatsModel.CharacterYOffset.value
138 |
139 | // normal food collision
140 | pacFoodState.foodList.forEach { foodModel ->
141 | if (
142 | Range.create(characterX, characterX + 100).contains(foodModel.xPos.toFloat()) &&
143 | Range.create(characterY, characterY + 100).contains(foodModel.yPos.toFloat())
144 | ) {
145 | // redraw outside box with 0 size and increment score by 1
146 | foodModel.xPos = 1000
147 | foodModel.yPos = 2000
148 | foodCounter.value -= 1
149 | }
150 | }
151 |
152 | // bonus food collision
153 | pacFoodState.bonusFoodList.forEach { foodModel ->
154 | if (
155 | Range.create(characterX, characterX + 100).contains(foodModel.xPos.toFloat()) &&
156 | Range.create(characterY, characterY + 100).contains(foodModel.yPos.toFloat())
157 | ) {
158 | // redraw outside box with 0 size
159 | reverseMode.value = true
160 | foodModel.xPos = 1000
161 | foodModel.yPos = 2000
162 | }
163 | }
164 |
165 | // reverse mode detection
166 |
167 | if(enemyMovementModel.orangeEnemyMovement.value.x == 409.0f &&
168 | enemyMovementModel.orangeEnemyMovement.value.y == 705.0f &&
169 | enemyMovementModel.redEnemyMovement.value.x == 389.0f &&
170 | enemyMovementModel.redEnemyMovement.value.y == 705.0f
171 | ){
172 | /*
173 | if these conditions are true the game is either started or the reverse animation has finished
174 | so reverseMode should be set to false.
175 | */
176 | gameStatsModel.isReverseMode.value = false
177 | }
178 |
179 | // enemy collision detection
180 | Log.d(
181 | "enemyMovement", "" +
182 | "Orange : x: ${enemyMovementModel.orangeEnemyMovement.value.x} " +
183 | "y: ${enemyMovementModel.orangeEnemyMovement.value.y} Red: x:" +
184 | " ${enemyMovementModel.redEnemyMovement.value.x} y: " +
185 | "${enemyMovementModel.redEnemyMovement.value.y} character current position : x:" +
186 | " $characterX y : $characterY"
187 | )
188 |
189 | if (
190 | // if enemy is within 100f of character then a collision has occurred and the game should stop
191 | Range.create(characterX, characterX + 25).contains(
192 | enemyMovementModel.redEnemyMovement.value.x
193 | ) &&
194 | Range.create(characterY, characterY + 25).contains(
195 | enemyMovementModel.redEnemyMovement.value.y
196 | ) ||
197 | Range.create(characterX, characterX + 25).contains(
198 | enemyMovementModel.orangeEnemyMovement.value.x
199 | ) &&
200 | Range.create(characterY, characterY + 25).contains(
201 | enemyMovementModel.orangeEnemyMovement.value.y
202 | )
203 |
204 | ) {
205 | // gameOver, stop game and show dialog
206 | resetGame("GAME OVER")
207 |
208 | }
209 |
210 | // win logic
211 | Log.d("food counter", "counter: ${foodCounter.value} ")
212 | if (foodCounter.value == 0) {
213 | resetGame("YOU WON !")
214 | }
215 |
216 |
217 | }
218 | }
219 |
220 | private fun resetGame(message: String) {
221 | gameStarted.value = false
222 | gameOverDialogState.shouldShow.value = true
223 | gameOverDialogState.message.value = message
224 | foodCounter.value = GameConstants.FOOD_COUNTER // reset counter
225 | pacFoodState.initRedraw()
226 | // reset character position
227 | characterXOffset.value = 0f
228 | characterYOffset.value = 0f
229 |
230 | }
231 |
232 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/dbtechprojects/pacmancompose/ui/GameComposables.kt:
--------------------------------------------------------------------------------
1 | package com.dbtechprojects.pacmancompose.ui
2 |
3 | import android.content.res.Resources
4 | import android.util.Log
5 | import androidx.compose.animation.core.*
6 | import androidx.compose.foundation.Canvas
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.border
10 | import androidx.compose.foundation.gestures.detectTapGestures
11 | import androidx.compose.foundation.layout.*
12 | import androidx.compose.foundation.shape.CircleShape
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.Button
15 | import androidx.compose.material.ButtonDefaults
16 | import androidx.compose.material.Surface
17 | import androidx.compose.material.Text
18 | import androidx.compose.runtime.*
19 | import androidx.compose.runtime.livedata.observeAsState
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.draw.clip
23 | import androidx.compose.ui.geometry.Offset
24 | import androidx.compose.ui.geometry.Size
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.graphics.ImageBitmap
27 | import androidx.compose.ui.graphics.Path
28 | import androidx.compose.ui.graphics.drawscope.Fill
29 | import androidx.compose.ui.graphics.drawscope.Stroke
30 | import androidx.compose.ui.input.pointer.pointerInput
31 | import androidx.compose.ui.res.imageResource
32 | import androidx.compose.ui.res.painterResource
33 | import androidx.compose.ui.text.font.FontWeight
34 | import androidx.compose.ui.text.style.TextAlign
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.unit.sp
37 | import androidx.compose.ui.window.Dialog
38 | import androidx.compose.ui.window.DialogProperties
39 | import androidx.constraintlayout.compose.ConstraintLayout
40 | import com.dbtechprojects.pacmancompose.GameViewModel
41 | import com.dbtechprojects.pacmancompose.models.DialogState
42 | import com.dbtechprojects.pacmancompose.models.EnemyMovementModel
43 | import com.dbtechprojects.pacmancompose.models.GameStatsModel
44 | import com.dbtechprojects.pacmancompose.models.PacFood
45 | import com.dbtechprojects.pacmancompose.ui.theme.*
46 | import com.dbtechprojects.pacmancompose.utils.GameConstants
47 |
48 | @Composable
49 | fun FullScreenDialog(showDialog: MutableState , text: String) {
50 | if (showDialog.value) {
51 | Dialog(
52 | properties =
53 | DialogProperties(
54 | dismissOnBackPress = true,
55 | dismissOnClickOutside = true,
56 | ), onDismissRequest = {}
57 | ) {
58 | Surface(
59 | modifier = Modifier.wrapContentSize(),
60 | shape = RoundedCornerShape(16.dp),
61 | color = PacmanYellow
62 | ) {
63 | Box(
64 | contentAlignment = Alignment.Center,
65 | ) {
66 | Column(
67 | Modifier
68 | .padding(16.dp)
69 | .align(Alignment.BottomCenter)
70 | ) {
71 |
72 | Column(
73 | Modifier.padding(16.dp)
74 | .fillMaxWidth(),
75 | horizontalAlignment = Alignment.CenterHorizontally
76 | ) {
77 |
78 | Text(
79 | text = text,
80 | color = PacmanPink,
81 | fontSize = 24.sp,
82 | fontWeight = FontWeight.Bold,
83 | fontFamily = HeaderFont,
84 | modifier = Modifier.padding(16.dp).fillMaxWidth(),
85 | textAlign = TextAlign.Center,
86 | softWrap = false
87 |
88 | )
89 | Button(
90 | onClick = { showDialog.value = false },
91 | colors = ButtonDefaults.buttonColors(backgroundColor = PacmanPink)
92 | ) {
93 | Text(
94 | text = "Close",
95 | color = Color.White
96 | )
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 | }
104 | }
105 |
106 | @Composable
107 | fun GameBorder(
108 | gameViewModel: GameViewModel,
109 | pacFoodState: PacFood,
110 | enemyMovementModel: EnemyMovementModel,
111 | resources: Resources,
112 | gameOverDialogState: DialogState,
113 | gameStatsModel: GameStatsModel,
114 | redEnemyDrawable: Int,
115 | orangeEnemyDrawable: Int,
116 | reverseEnemyDrawable: Int
117 | ) {
118 | val characterStartAngle by gameViewModel.characterStartAngle.observeAsState()
119 |
120 | FullScreenDialog(showDialog = gameOverDialogState.shouldShow, gameOverDialogState.message.value)
121 |
122 | Box(
123 | modifier = Modifier
124 | .border(6.dp, color = PacmanRed)
125 | .padding(6.dp)
126 | ) {
127 | // used for character and food to draw circle
128 | val radius = 50f
129 |
130 | // animate drawing the pac character at the start of the game
131 | val animateCharacterSweepAngle = remember { Animatable(0f) }
132 | LaunchedEffect(animateCharacterSweepAngle) {
133 | animateCharacterSweepAngle.animateTo(
134 | targetValue = 1f,
135 | animationSpec = tween(durationMillis = 3000, easing = LinearEasing),
136 | )
137 | }
138 |
139 |
140 | // animate continued opening and closing of mouth whilst game is running
141 | val infiniteTransition = rememberInfiniteTransition()
142 | val mouthAnimation by infiniteTransition.animateFloat(
143 | initialValue = 360F,
144 | targetValue = 280F,
145 | animationSpec = infiniteRepeatable(
146 | animation = tween(500, easing = LinearEasing),
147 | repeatMode = RepeatMode.Restart
148 | )
149 | )
150 |
151 | // define enemy values (more to be added)
152 |
153 | //Red Enemy
154 | enemyMovementModel.redEnemyMovement.value = enemyMovement(
155 | duration = GameConstants.RED_ENEMY_SPEED,
156 | gameStats = gameStatsModel,
157 | initialXOffset = 90f
158 | )
159 |
160 | // Orange Enemy
161 | enemyMovementModel.orangeEnemyMovement.value = enemyMovement(
162 | duration = GameConstants.ORANGE_ENEMY_SPEED,
163 | gameStats = gameStatsModel,
164 | initialXOffset = 70f
165 | )
166 |
167 |
168 | // canvas to draw game
169 | Canvas(
170 | modifier = Modifier
171 | .fillMaxWidth()
172 | .fillMaxSize(0.75f)
173 | ) {
174 | val height = this.size.height
175 | val width = this.size.width
176 | Log.d("canvas", "width: $width, height: $height ")
177 |
178 | // character
179 | drawArc(
180 | color = Color.Yellow,
181 | startAngle = characterStartAngle ?: 30f,
182 | sweepAngle = if (gameStatsModel.isGameStarted.value) mouthAnimation else 360f * animateCharacterSweepAngle.value,
183 | useCenter = true,
184 | topLeft = Offset(
185 | size.width / 2 - 90f + gameStatsModel.CharacterXOffset.value,
186 | size.height - 155f + gameStatsModel.CharacterYOffset.value
187 | ),
188 | size = Size(
189 | radius * 2,
190 | radius * 2
191 | ),
192 | style = Fill,
193 |
194 | )
195 |
196 | // food
197 | for (i in pacFoodState.foodList) {
198 | drawArc(
199 | color = Color.Yellow,
200 | startAngle = characterStartAngle ?: 30f,
201 | sweepAngle = 360f,
202 | useCenter = true,
203 | topLeft = Offset(i.xPos.toFloat(), i.yPos.toFloat()),
204 | size = Size(
205 | radius * i.size,
206 | radius * i.size
207 | ),
208 | style = Fill,
209 |
210 | )
211 | }
212 |
213 | // Bonus Food
214 | for(i in pacFoodState.bonusFoodList){
215 | drawArc(
216 | color = PacmanOrange,
217 | startAngle = characterStartAngle ?: 30f,
218 | sweepAngle = 360f,
219 | useCenter = true,
220 | topLeft = Offset(i.xPos.toFloat(), i.yPos.toFloat()),
221 | size = Size(
222 | radius * i.size,
223 | radius * i.size
224 | ),
225 | style = Fill,
226 | )
227 | }
228 |
229 | // Enemy
230 | drawImage(
231 | image = if(gameStatsModel.isReverseMode.value){
232 | ImageBitmap.imageResource(res = resources, reverseEnemyDrawable)
233 | } else {
234 | ImageBitmap.imageResource(res = resources, redEnemyDrawable)
235 | },
236 | topLeft = enemyMovementModel.redEnemyMovement.value
237 |
238 | )
239 | drawImage(
240 | image = if(gameStatsModel.isReverseMode.value){
241 | ImageBitmap.imageResource(res = resources, reverseEnemyDrawable)
242 | } else {
243 | ImageBitmap.imageResource(res = resources, orangeEnemyDrawable)
244 | },
245 | topLeft = enemyMovementModel.orangeEnemyMovement.value
246 |
247 | )
248 |
249 | // Maze
250 | val borderPath = Path()
251 |
252 | // barriers
253 | val barrierPath = Path()
254 |
255 | borderPath.apply {
256 | // border
257 | lineTo(size.width, 0f)
258 | lineTo(size.width, size.height)
259 | lineTo(0f, size.height)
260 | lineTo(0f, 0f)
261 |
262 | // second border
263 | moveTo(50f, 50f)
264 | lineTo(size.width - 50f, 50f)
265 | lineTo(size.width - 50f, size.height - 50f)
266 | lineTo(50f, size.height - 50f)
267 | lineTo(50f, 50f)
268 |
269 | // enemy box
270 | moveTo(size.width / 2 + 90f, size.height / 2 + 90f)
271 | lineTo(size.width / 2 + 90f, size.height / 2 + 180f)
272 | lineTo(size.width / 2 - 120f, size.height / 2 + 180f)
273 | lineTo(size.width / 2 - 120f, size.height / 2 + 90f)
274 |
275 |
276 | }
277 |
278 | barrierPath.apply {
279 | /*
280 | barriers
281 | __________
282 | |___ ___|
283 | |__|
284 | */
285 | //left top corner barrier
286 | moveTo(size.width / 4 + 60f, size.height / 4)
287 | lineTo(size.width / 4 - 20f, size.height / 4) // bottom
288 | lineTo(size.width / 4 - 20f, size.height / 4 - 60f) // left
289 | lineTo(size.width / 4 - 90f, size.height / 4 - 60f) // left angle
290 | lineTo(size.width / 4 - 90f, size.height / 4 - 120f) // left upward line to top
291 | lineTo(size.width / 4 + 120f, size.height / 4 - 120f) // top line
292 | lineTo(size.width / 4 + 120f, size.height / 4 - 60f) // line down to right
293 | lineTo(size.width / 4 + 50f, size.height / 4 - 60f) // line right to center
294 | lineTo(size.width / 4 + 50f, size.height / 4) // bottom line
295 |
296 | // right top corner barrier
297 | moveTo(size.width / 1.5f + 60f, size.height / 4)
298 | lineTo(size.width / 1.5f - 20f, size.height / 4) // bottom
299 | lineTo(size.width / 1.5f - 20f, size.height / 4 - 60f) // left
300 | lineTo(size.width / 1.5f - 90f, size.height / 4 - 60f) // left angle
301 | lineTo(size.width / 1.5f - 90f, size.height / 4 - 120f) // left upward line to top
302 | lineTo(size.width / 1.5f + 120f, size.height / 4 - 120f) // top line
303 | lineTo(size.width / 1.5f + 120f, size.height / 4 - 60f) // line down to right
304 | lineTo(size.width / 1.5f + 50f, size.height / 4 - 60f) // line right to center
305 | lineTo(size.width / 1.5f + 50f, size.height / 4) // bottom line
306 |
307 | // right bottom corner barrier
308 | moveTo(size.width / 1.5f + 60f, size.height / 1.15f)
309 | lineTo(size.width / 1.5f - 20f, size.height / 1.15f) // bottom
310 | lineTo(size.width / 1.5f - 20f, size.height / 1.15f - 60f) // left
311 | lineTo(size.width / 1.5f - 90f, size.height / 1.15f - 60f) // left angle
312 | lineTo(
313 | size.width / 1.5f - 90f,
314 | size.height / 1.15f - 120f
315 | ) // left upward line to top
316 | lineTo(size.width / 1.5f + 120f, size.height / 1.15f - 120f) // top line
317 | lineTo(size.width / 1.5f + 120f, size.height / 1.15f - 60f) // line down to right
318 | lineTo(size.width / 1.5f + 50f, size.height / 1.15f - 60f) // line right to center
319 | lineTo(size.width / 1.5f + 50f, size.height / 1.15f) // bottom line
320 |
321 | //left bottom corner barrier
322 | moveTo(size.width / 4 + 60f, size.height / 1.15f)
323 | lineTo(size.width / 4 - 20f, size.height / 1.15f) // bottom
324 | lineTo(size.width / 4 - 20f, size.height / 1.15f - 60f) // left
325 | lineTo(size.width / 4 - 90f, size.height / 1.15f - 60f) // left angle
326 | lineTo(size.width / 4 - 90f, size.height / 1.15f - 120f) // left upward line to top
327 | lineTo(size.width / 4 + 120f, size.height / 1.15f - 120f) // top line
328 | lineTo(size.width / 4 + 120f, size.height / 1.15f - 60f) // line down to right
329 | lineTo(size.width / 4 + 50f, size.height / 1.15f - 60f) // line right to center
330 | lineTo(size.width / 4 + 50f, size.height / 1.15f) // bottom line
331 |
332 | }
333 | drawPath(
334 | path = borderPath,
335 | color = PacmanMazeColor,
336 | style = Stroke(
337 | width = 6.dp.toPx(),
338 | ),
339 |
340 | )
341 |
342 | drawPath(
343 | path = barrierPath,
344 | color = PacmanMazeColor,
345 | style = Fill,
346 | )
347 |
348 |
349 | }
350 | }
351 | }
352 |
353 |
354 |
355 |
356 | @ExperimentalFoundationApi
357 | @Composable
358 | fun Controls(
359 | gameStatsModel: GameStatsModel,
360 | gameViewModel: GameViewModel,
361 | upButtonDrawable: Int,
362 | downButtonDrawable: Int,
363 | leftButtonDrawable: Int,
364 | rightButtonDrawable: Int
365 | ) {
366 |
367 |
368 | Row(
369 | horizontalArrangement = Arrangement.SpaceAround,
370 | modifier = Modifier
371 | .fillMaxSize()
372 | .padding(top = 6.dp)
373 | ) {
374 | // controls
375 | ConstraintLayout(modifier = Modifier.fillMaxSize(0.5f)) {
376 | // Create references for the composables to constrain
377 | val (upArrow, leftArrow, rightArrow, downArrow) = createRefs()
378 | Image(painter = painterResource(id = upButtonDrawable),
379 | contentDescription = "Up Arrow",
380 | Modifier
381 | .constrainAs(upArrow) {
382 | bottom.linkTo(leftArrow.top)
383 | start.linkTo(leftArrow.end)
384 | }
385 | .size(30.dp)
386 | .pointerInput(Unit) {
387 | detectTapGestures(
388 | onPress = {
389 | if (gameStatsModel.isGameStarted.value) {
390 | gameViewModel.upPress(
391 | characterYOffset = gameStatsModel.CharacterYOffset,
392 | characterXOffset = gameStatsModel.CharacterXOffset
393 | )
394 | tryAwaitRelease()
395 | gameViewModel.releaseUp()
396 | }
397 | }
398 | )
399 | }
400 | )
401 |
402 | Image(painter = painterResource(id = leftButtonDrawable),
403 | contentDescription = "Left Arrow",
404 | Modifier
405 | .constrainAs(leftArrow) {
406 | start.linkTo(parent.start)
407 | bottom.linkTo(parent.bottom)
408 | }
409 | .size(30.dp)
410 | .pointerInput(Unit) {
411 | detectTapGestures(
412 | onPress = {
413 | if (gameStatsModel.isGameStarted.value) {
414 | gameViewModel.leftPress(
415 | characterYOffset = gameStatsModel.CharacterYOffset,
416 | characterXOffset = gameStatsModel.CharacterXOffset
417 | )
418 | tryAwaitRelease()
419 | gameViewModel.releaseLeft()
420 | }
421 | }
422 | )
423 | }
424 | )
425 | Image(painter = painterResource(id = rightButtonDrawable),
426 | contentDescription = "Right Arrow",
427 | Modifier
428 | .constrainAs(rightArrow) {
429 | start.linkTo(upArrow.end)
430 | bottom.linkTo(parent.bottom)
431 | }
432 | .size(30.dp)
433 | .pointerInput(Unit) {
434 | detectTapGestures(
435 | onPress = {
436 | if (gameStatsModel.isGameStarted.value) {
437 | gameViewModel.rightPress(
438 | characterYOffset = gameStatsModel.CharacterYOffset,
439 | characterXOffset = gameStatsModel.CharacterXOffset
440 | )
441 | tryAwaitRelease()
442 | gameViewModel.releaseRight()
443 | }
444 | }
445 | )
446 | }
447 |
448 | )
449 | Image(painter = painterResource(id = downButtonDrawable),
450 | contentDescription = "Down Arrow",
451 | Modifier
452 | .constrainAs(downArrow) {
453 | top.linkTo(rightArrow.bottom)
454 | start.linkTo(leftArrow.end)
455 | }
456 | .size(30.dp)
457 | .pointerInput(Unit) {
458 | detectTapGestures(
459 | onPress = {
460 | if (gameStatsModel.isGameStarted.value) {
461 | gameViewModel.downPress(
462 | characterYOffset = gameStatsModel.CharacterYOffset,
463 | characterXOffset = gameStatsModel.CharacterXOffset
464 | )
465 | tryAwaitRelease()
466 | gameViewModel.releaseDown()
467 | }
468 | }
469 | )
470 | }
471 | )
472 | }
473 |
474 | // start/stop button
475 | Button(
476 | onClick = { gameStatsModel.isGameStarted.value = !gameStatsModel.isGameStarted.value },
477 | Modifier
478 | .clip(CircleShape)
479 | .align(Alignment.CenterVertically),
480 | colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red)
481 | ) {
482 | Text(
483 | text = if (gameStatsModel.isGameStarted.value) "Stop" else "Start",
484 | color = PacmanWhite,
485 | fontWeight = FontWeight.Bold,
486 | fontSize = 18.sp
487 | )
488 | }
489 | }
490 |
491 | }
492 |
493 | /*
494 | function to provide an Offset to set enemy position, enemies position is animated to the
495 | position of the character if the game has started
496 | */
497 |
498 | @Composable
499 | fun enemyMovement(duration: Int, gameStats: GameStatsModel, initialXOffset: Float): Offset {
500 | // X Axis
501 | val enemyMovementXAxis by animateFloatAsState(
502 | targetValue = if (gameStats.isReverseMode.value || !gameStats.isGameStarted.value) {
503 | 958.0f / 2 - initialXOffset // (return to original position )create spacing between enemies in box
504 | } else {
505 | 958.0f / 2 - 90f + gameStats.CharacterXOffset.value
506 |
507 | },
508 | animationSpec = tween(duration, easing = LinearEasing),
509 | finishedListener = {
510 |
511 | }
512 |
513 | )
514 |
515 | // y Axis
516 | val enemyMovementYAxis by animateFloatAsState(
517 | //if game has not started or the game is in reverse mode then move enemies back to enemy box
518 | targetValue = if (gameStats.isReverseMode.value || !gameStats.isGameStarted.value) {
519 | 1290.0f / 2 + 60f
520 | } else {
521 | 1290.0f - 155f + gameStats.CharacterYOffset.value
522 | },
523 | animationSpec = tween(duration, easing = LinearEasing),
524 | finishedListener = {
525 | }
526 | )
527 |
528 | return Offset(enemyMovementXAxis, enemyMovementYAxis)
529 |
530 |
531 | }
--------------------------------------------------------------------------------