From 1ae61e8d7e1fa7f2d4cecad4efe96f3c414d685e Mon Sep 17 00:00:00 2001 From: "matthias.lapierre" Date: Thu, 24 Sep 2020 08:50:16 +0200 Subject: [PATCH] Improve injector. --- .../spaceshooter/AppContainer.kt | 14 +- .../spaceshooter/resources/Drawables.kt | 186 ++--------------- .../spaceshooter/resources/Scores.kt | 26 +-- .../spaceshooter/resources/SoundEngine.kt | 157 ++------------ .../spaceshooter/resources/TypefaceHelper.kt | 37 +--- .../resources/impl/DrawablesImpl.kt | 195 ++++++++++++++++++ .../spaceshooter/resources/impl/ScoresImpl.kt | 36 ++++ .../resources/impl/SoundEngineImpl.kt | 164 +++++++++++++++ .../resources/impl/TypefaceHelperImpl.kt | 43 ++++ 9 files changed, 484 insertions(+), 374 deletions(-) create mode 100644 app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/DrawablesImpl.kt create mode 100644 app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/ScoresImpl.kt create mode 100644 app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/SoundEngineImpl.kt create mode 100644 app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/TypefaceHelperImpl.kt diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/AppContainer.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/AppContainer.kt index a354e64..cefef84 100644 --- a/app/src/main/java/com/matthiaslapierre/spaceshooter/AppContainer.kt +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/AppContainer.kt @@ -2,9 +2,13 @@ package com.matthiaslapierre.spaceshooter import android.content.Context import com.matthiaslapierre.spaceshooter.resources.Drawables -import com.matthiaslapierre.spaceshooter.resources.TypefaceHelper import com.matthiaslapierre.spaceshooter.resources.Scores import com.matthiaslapierre.spaceshooter.resources.SoundEngine +import com.matthiaslapierre.spaceshooter.resources.TypefaceHelper +import com.matthiaslapierre.spaceshooter.resources.impl.DrawablesImpl +import com.matthiaslapierre.spaceshooter.resources.impl.ScoresImpl +import com.matthiaslapierre.spaceshooter.resources.impl.SoundEngineImpl +import com.matthiaslapierre.spaceshooter.resources.impl.TypefaceHelperImpl /** * To solve the issue of reusing objects, you can create your own dependencies container class @@ -16,8 +20,8 @@ import com.matthiaslapierre.spaceshooter.resources.SoundEngine class AppContainer( context: Context ) { - val drawables = Drawables(context) - val typefaceHelper = TypefaceHelper(context.assets) - val soundEngine = SoundEngine(context.assets) - val scores = Scores() + val drawables: Drawables = DrawablesImpl(context) + val typefaceHelper: TypefaceHelper = TypefaceHelperImpl(context.assets) + val soundEngine: SoundEngine = SoundEngineImpl(context.assets) + val scores: Scores = ScoresImpl() } \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Drawables.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Drawables.kt index b18f6c0..f1c133e 100644 --- a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Drawables.kt +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Drawables.kt @@ -1,238 +1,80 @@ package com.matthiaslapierre.spaceshooter.resources -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Rect -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import androidx.core.content.ContextCompat -import com.matthiaslapierre.spaceshooter.Constants.EXPLODE_FRAMES_PER_LINE -import com.matthiaslapierre.spaceshooter.Constants.EXPLODE_MAX_FRAMES -import com.matthiaslapierre.spaceshooter.R -import com.matthiaslapierre.spaceshooter.ui.game.sprite.MeteorSprite -import com.matthiaslapierre.spaceshooter.util.Utils -import java.util.* /** * Caches drawable resources. */ -class Drawables( - private val context: Context -) { - - private val cache: Hashtable = Hashtable() +interface Drawables{ /** * Loads drawables. */ - fun load() { - Thread(Runnable { - // Load explosion frames - cacheExplosionFrame() - }).start() - } + fun load() /** * Gets the button background. */ - fun getButton(): Drawable = get(R.drawable.button_blue) + fun getButton(): Drawable /** * Gets a meteor. * @param type * @param size */ - fun getMeteor(type: Int, size: Int): Drawable { - val resIds: Array = when (type) { - MeteorSprite.TYPE_BROWN -> when (size) { - MeteorSprite.SIZE_TINY -> arrayOf( - R.drawable.meteor_brown_tiny1, - R.drawable.meteor_brown_tiny2 - ) - MeteorSprite.SIZE_SMALL -> arrayOf( - R.drawable.meteor_brown_small1, - R.drawable.meteor_brown_small2 - ) - MeteorSprite.SIZE_MEDIUM -> arrayOf( - R.drawable.meteor_brown_med1, - R.drawable.meteor_brown_med2 - ) - MeteorSprite.SIZE_BIG -> arrayOf( - R.drawable.meteor_brown_big1, - R.drawable.meteor_brown_big2, - R.drawable.meteor_brown_big3, - R.drawable.meteor_brown_big4 - ) - else -> arrayOf( - R.drawable.meteor_brown_tiny1, - R.drawable.meteor_brown_tiny2 - ) - } - MeteorSprite.TYPE_GREY -> when (size) { - MeteorSprite.SIZE_TINY -> arrayOf( - R.drawable.meteor_grey_tiny1, - R.drawable.meteor_grey_tiny2 - ) - MeteorSprite.SIZE_SMALL -> arrayOf( - R.drawable.meteor_grey_small1, - R.drawable.meteor_grey_small2 - ) - MeteorSprite.SIZE_MEDIUM -> arrayOf( - R.drawable.meteor_grey_med1, - R.drawable.meteor_grey_med2 - ) - MeteorSprite.SIZE_BIG -> arrayOf( - R.drawable.meteor_grey_big1, - R.drawable.meteor_grey_big2, - R.drawable.meteor_grey_big3, - R.drawable.meteor_grey_big4 - ) - else -> arrayOf( - R.drawable.meteor_grey_tiny1, - R.drawable.meteor_grey_tiny2 - ) - } - else -> arrayOf( - R.drawable.meteor_brown_tiny1, - R.drawable.meteor_brown_tiny2 - ) - } - val index = Utils.getRandomInt(0, resIds.size) - return get(resIds[index]) - } + fun getMeteor(type: Int, size: Int): Drawable /** * Gets a digit. * @param digit 0-9 */ - fun getDigit(digit: Int): Drawable { - val resId = when (digit) { - 1 -> R.drawable.numeral1 - 2 -> R.drawable.numeral2 - 3 -> R.drawable.numeral3 - 4 -> R.drawable.numeral4 - 5 -> R.drawable.numeral5 - 6 -> R.drawable.numeral6 - 7 -> R.drawable.numeral7 - 8 -> R.drawable.numeral8 - 9 -> R.drawable.numeral9 - else -> R.drawable.numeral0 - } - return get(resId) - } + fun getDigit(digit: Int): Drawable /** * Gets a random star. */ - fun getRandomStar(): Drawable { - val resIds = arrayOf(R.drawable.star1, R.drawable.star2, R.drawable.star3) - val index = Utils.getRandomInt(0, resIds.size) - return get(resIds[index]) - } + fun getRandomStar(): Drawable /** * Gets a laser. * @param adverse shot by an enemy ship */ - fun getLaser(adverse: Boolean): Drawable = - if(adverse) { - get(R.drawable.laser_red1) - } else { - get(R.drawable.laser_blue1) - } + fun getLaser(adverse: Boolean): Drawable /** * Gets the player ship. */ - fun getPlayerShip(type: Int): Drawable = when (type) { - 1 -> get(R.drawable.player_ship1_blue) - 2 -> get(R.drawable.player_ship2_blue) - else -> get(R.drawable.player_ship3_blue) - } + fun getPlayerShip(type: Int): Drawable /** * Gets the enemy ship. */ - fun getEnemyShip(): Drawable = get(R.drawable.enemy_red_2) + fun getEnemyShip(): Drawable /** * Gets an explosion frame. * @param frame index of the frame */ - fun getExplosionFrame(frame: Int): Drawable = get("explode_$frame".hashCode()) + fun getExplosionFrame(frame: Int): Drawable /** * Power-up. Space ship upgrade. */ - fun getPowerUpBolt() = get(R.drawable.powerup_bolt) + fun getPowerUpBolt(): Drawable /** * Shield repair. */ - fun getPowerUpShield() = get(R.drawable.powerup_shield) + fun getPowerUpShield(): Drawable /** * +n points */ - fun getPowerUpStar() = get(R.drawable.powerup_star) + fun getPowerUpStar(): Drawable /** * Gets a drawable resource and cache it. */ - fun get(resId: Int): Drawable { - synchronized(cache) { - if (!cache.containsKey(resId)) { - val drawable = ContextCompat.getDrawable(context, resId) - cache[resId] = drawable - } - return cache[resId]!! - } - } - - /** - * Caches explosion frames. - */ - private fun cacheExplosionFrame() { - synchronized(cache) { - splitExplosionAnimation().forEachIndexed { frame, drawable -> - cache["explode_${frame+1}".hashCode()] = drawable - } - } - } - - /** - * Splits a bitmap and returns explosion frames. - */ - private fun splitExplosionAnimation(): List { - val drawables = mutableListOf() - val fullBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.explode) - val frameSize = fullBitmap.width / EXPLODE_FRAMES_PER_LINE - for(frame in 1 until EXPLODE_MAX_FRAMES + 1) { - val line = ((frame - 1) / EXPLODE_FRAMES_PER_LINE) - val column = ((frame - 1) % EXPLODE_FRAMES_PER_LINE) - val frameBitmap = Bitmap.createBitmap(frameSize, frameSize, Bitmap.Config.ARGB_8888) - val canvas = Canvas(frameBitmap) - canvas.drawBitmap( - fullBitmap, - Rect( - column * frameSize, - line * frameSize, - column * frameSize + frameSize, - line * frameSize + frameSize - ), - Rect( - 0, - 0, - frameSize, - frameSize - ), - null - ) - drawables.add(BitmapDrawable(context.resources, frameBitmap)) - } - return drawables - } + fun get(resId: Int): Drawable } \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Scores.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Scores.kt index ea84e16..9fc4052 100644 --- a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Scores.kt +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/Scores.kt @@ -1,44 +1,26 @@ package com.matthiaslapierre.spaceshooter.resources import android.content.Context -import android.content.SharedPreferences /** * Stores scores locally by using the Preferences API. */ -class Scores { - - companion object { - private const val PREF_DEFAULT = "com.matthiaslapierre.spaceshooter.PREF_DEFAULT" - private const val HIGH_SCORE = "high_score" - } +interface Scores { /** * Gets the best score achieved. */ - fun highScore(context: Context): Int { - val p: SharedPreferences = context.getSharedPreferences( - PREF_DEFAULT, - Context.MODE_PRIVATE - ) - return p.getInt(HIGH_SCORE, 0) - } + fun highScore(context: Context): Int /** * Checks if it's the new best score. */ - fun isNewBestScore(context: Context, score: Int): Boolean = score > highScore(context) + fun isNewBestScore(context: Context, score: Int): Boolean /** * Records the new best score. */ - fun storeHighScore(context: Context, score: Int) { - val p: SharedPreferences = context.getSharedPreferences( - PREF_DEFAULT, - Context.MODE_PRIVATE - ) - p.edit().putInt(HIGH_SCORE, score).apply() - } + fun storeHighScore(context: Context, score: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/SoundEngine.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/SoundEngine.kt index cb6c10b..d1380d0 100644 --- a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/SoundEngine.kt +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/SoundEngine.kt @@ -1,205 +1,78 @@ package com.matthiaslapierre.spaceshooter.resources -import android.content.res.AssetFileDescriptor -import android.content.res.AssetManager -import android.media.AudioAttributes -import android.media.AudioManager -import android.media.MediaPlayer -import android.media.SoundPool -import android.os.Build -import com.matthiaslapierre.spaceshooter.Constants -import java.io.IOException - /** * Handles sounds present in the game. */ -class SoundEngine( - private val assets: AssetManager -) { - - companion object{ - private const val SOUND_BTN_PRESS = 0 - private const val SOUND_CRASH = 1 - private const val SOUND_METEOR_EXPLODE = 2 - private const val SOUND_SHIP_EXPLODE = 3 - private const val SOUND_GET_POWER_UP = 4 - private const val SOUND_GAME_OVER = 5 - private const val SOUND_SHOT_HIT = 6 - - private const val MUSIC_MENU = "musics/menu.ogg" - private const val MUSIC_PLAY = "musics/play.ogg" - private const val MUSIC_GAME_OVER = "musics/game_over.ogg" - } - - /** - * To play short sounds. - */ - private val soundPool: SoundPool - - /** - * To play music. - */ - private var player: MediaPlayer? = null - - /** - * Cache. - */ - private val sounds: Array = arrayOfNulls(7) - - init { - soundPool = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val audioAttributes = AudioAttributes - .Builder() - .setUsage(AudioAttributes.USAGE_GAME) - .build() - SoundPool - .Builder() - .setMaxStreams(Constants.SOUND_MAX_STREAMS) - .setAudioAttributes(audioAttributes) - .build() - } else { - SoundPool( - Constants.SOUND_MAX_STREAMS, - AudioManager.STREAM_MUSIC, - 0 - ) - } - } +interface SoundEngine { /** * Loads sound effects. */ - fun load() { - Thread(Runnable { - sounds[SOUND_BTN_PRESS] = soundPool.load(assets.openFd("sounds/sfx_btn_press.ogg"), 1) - sounds[SOUND_CRASH] = soundPool.load(assets.openFd("sounds/sfx_crash.ogg"), 1) - sounds[SOUND_METEOR_EXPLODE] = soundPool.load(assets.openFd("sounds/sfx_explode_meteor.ogg"), 1) - sounds[SOUND_SHIP_EXPLODE] = soundPool.load(assets.openFd("sounds/sfx_explode_ship.ogg"), 1) - sounds[SOUND_GET_POWER_UP] = soundPool.load(assets.openFd("sounds/sfx_get_power_up.ogg"), 1) - sounds[SOUND_GAME_OVER] = soundPool.load(assets.openFd("sounds/sfx_lose.ogg"), 1) - sounds[SOUND_SHOT_HIT] = soundPool.load(assets.openFd("sounds/sfx_shot_hit.ogg"), 1) - }).start() - } + fun load() /** * Unloads sound effects. */ - fun release() { - sounds.filterNotNull().forEach { soundID -> - soundPool.unload(soundID) - } - stopMusic() - } + fun release() /** * Resumes the music. */ - fun resume() { - player?.start() - } + fun resume() /** * Pauses the music. */ - fun pause() { - player?.pause() - } + fun pause() /** * Plays a button press sound effect. */ - fun playBtnPress() = playSound(SOUND_BTN_PRESS) + fun playBtnPress() /** * Plays the crash sound effect. */ - fun playCrash() = playSound(SOUND_CRASH) + fun playCrash() /** * Plays the meteor explode sound effect. */ - fun playMeteorExplode() = playSound(SOUND_METEOR_EXPLODE) + fun playMeteorExplode() /** * Plays the space ship explode sound effect. */ - fun playShipExplode() = playSound(SOUND_SHIP_EXPLODE) + fun playShipExplode() /** * Plays a sound after getting a power-up. */ - fun playGetPowerUp() = playSound(SOUND_GET_POWER_UP) + fun playGetPowerUp() /** * Plays "Game Over" sound effect. */ - fun playGameOver() = playSound(SOUND_GAME_OVER) + fun playGameOver() /** * Plays a sound after the laser shot hits its target. */ - fun playShotHit() = playSound(SOUND_SHOT_HIT) + fun playShotHit() /** * Plays the menu music. */ - fun playMenuMusic() = playMusic(MUSIC_MENU) + fun playMenuMusic() /** * Plays the main music. */ - fun playPlayMusic() = playMusic(MUSIC_PLAY) + fun playPlayMusic() /** * Plays the Game Over music. */ - fun playGameOverMusic() = playMusic(MUSIC_GAME_OVER) - - /** - * Plays a sound if it is in caches - */ - private fun playSound(index: Int, volume: Float = 0.5f, infiniteLoop: Boolean = false) { - sounds[index]?.let { soundId -> - val loop = if(infiniteLoop) -1 else 0 - soundPool.play(soundId, volume, volume, 1, loop, 1f) - } - } - - /** - * Plays a music. - */ - private fun playMusic(filename: String) { - stopMusic() - try { - val afd: AssetFileDescriptor = assets.openFd(filename) - player = MediaPlayer().apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setAudioAttributes( - AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_GAME) - .build() - ) - } - isLooping = true - setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) - setVolume(0.3f, 0.3f) - prepare() - start() - } - } catch (e: IOException) { - e.printStackTrace() - } - } - - /** - * Stops the music. - */ - private fun stopMusic() { - if(player != null) { - player?.release() - player = null - } - } + fun playGameOverMusic() } \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/TypefaceHelper.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/TypefaceHelper.kt index d297de3..196bd80 100644 --- a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/TypefaceHelper.kt +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/TypefaceHelper.kt @@ -1,54 +1,25 @@ package com.matthiaslapierre.spaceshooter.resources -import android.content.res.AssetManager import android.graphics.Typeface -import java.util.* /** * Caches typefaces. */ -class TypefaceHelper( - private val assets: AssetManager -) { - - companion object { - private const val FONT_FUTURE = "kenvector_future" - private const val FONT_FUTURE_THIN = "kenvector_future_thin" - } - - private val cache: Hashtable = Hashtable() +interface TypefaceHelper { /** * Loads typefaces. */ - fun load() { - Thread(Runnable { - get(FONT_FUTURE) - get(FONT_FUTURE_THIN) - }).start() - } + fun load() /** * Gets the main typeface. */ - fun getFutureTypeface(): Typeface = get(FONT_FUTURE)!! + fun getFutureTypeface(): Typeface /** * Gets the thin variant. */ - fun getFutureThinTypeface(): Typeface = get(FONT_FUTURE_THIN)!! - - private fun get(name: String): Typeface? { - synchronized(cache) { - if (!cache.containsKey(name)) { - val t = Typeface.createFromAsset( - assets, - String.format("fonts/%s.ttf", name) - ) - cache[name] = t - } - return cache[name] - } - } + fun getFutureThinTypeface(): Typeface } \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/DrawablesImpl.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/DrawablesImpl.kt new file mode 100644 index 0000000..68396cd --- /dev/null +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/DrawablesImpl.kt @@ -0,0 +1,195 @@ +package com.matthiaslapierre.spaceshooter.resources.impl + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.core.content.ContextCompat +import com.matthiaslapierre.spaceshooter.Constants.EXPLODE_FRAMES_PER_LINE +import com.matthiaslapierre.spaceshooter.Constants.EXPLODE_MAX_FRAMES +import com.matthiaslapierre.spaceshooter.R +import com.matthiaslapierre.spaceshooter.resources.Drawables +import com.matthiaslapierre.spaceshooter.ui.game.sprite.MeteorSprite +import com.matthiaslapierre.spaceshooter.util.Utils +import java.util.* + +/** + * Caches drawable resources. + */ +class DrawablesImpl( + private val context: Context +): Drawables { + + private val cache: Hashtable = Hashtable() + + override fun load() { + Thread(Runnable { + // Load explosion frames + cacheExplosionFrame() + }).start() + } + + override fun getButton(): Drawable = get(R.drawable.button_blue) + + override fun getMeteor(type: Int, size: Int): Drawable { + val resIds: Array = when (type) { + MeteorSprite.TYPE_BROWN -> when (size) { + MeteorSprite.SIZE_TINY -> arrayOf( + R.drawable.meteor_brown_tiny1, + R.drawable.meteor_brown_tiny2 + ) + MeteorSprite.SIZE_SMALL -> arrayOf( + R.drawable.meteor_brown_small1, + R.drawable.meteor_brown_small2 + ) + MeteorSprite.SIZE_MEDIUM -> arrayOf( + R.drawable.meteor_brown_med1, + R.drawable.meteor_brown_med2 + ) + MeteorSprite.SIZE_BIG -> arrayOf( + R.drawable.meteor_brown_big1, + R.drawable.meteor_brown_big2, + R.drawable.meteor_brown_big3, + R.drawable.meteor_brown_big4 + ) + else -> arrayOf( + R.drawable.meteor_brown_tiny1, + R.drawable.meteor_brown_tiny2 + ) + } + MeteorSprite.TYPE_GREY -> when (size) { + MeteorSprite.SIZE_TINY -> arrayOf( + R.drawable.meteor_grey_tiny1, + R.drawable.meteor_grey_tiny2 + ) + MeteorSprite.SIZE_SMALL -> arrayOf( + R.drawable.meteor_grey_small1, + R.drawable.meteor_grey_small2 + ) + MeteorSprite.SIZE_MEDIUM -> arrayOf( + R.drawable.meteor_grey_med1, + R.drawable.meteor_grey_med2 + ) + MeteorSprite.SIZE_BIG -> arrayOf( + R.drawable.meteor_grey_big1, + R.drawable.meteor_grey_big2, + R.drawable.meteor_grey_big3, + R.drawable.meteor_grey_big4 + ) + else -> arrayOf( + R.drawable.meteor_grey_tiny1, + R.drawable.meteor_grey_tiny2 + ) + } + else -> arrayOf( + R.drawable.meteor_brown_tiny1, + R.drawable.meteor_brown_tiny2 + ) + } + val index = Utils.getRandomInt(0, resIds.size) + return get(resIds[index]) + } + + override fun getDigit(digit: Int): Drawable { + val resId = when (digit) { + 1 -> R.drawable.numeral1 + 2 -> R.drawable.numeral2 + 3 -> R.drawable.numeral3 + 4 -> R.drawable.numeral4 + 5 -> R.drawable.numeral5 + 6 -> R.drawable.numeral6 + 7 -> R.drawable.numeral7 + 8 -> R.drawable.numeral8 + 9 -> R.drawable.numeral9 + else -> R.drawable.numeral0 + } + return get(resId) + } + + override fun getRandomStar(): Drawable { + val resIds = arrayOf(R.drawable.star1, R.drawable.star2, R.drawable.star3) + val index = Utils.getRandomInt(0, resIds.size) + return get(resIds[index]) + } + + override fun getLaser(adverse: Boolean): Drawable = + if(adverse) { + get(R.drawable.laser_red1) + } else { + get(R.drawable.laser_blue1) + } + + override fun getPlayerShip(type: Int): Drawable = when (type) { + 1 -> get(R.drawable.player_ship1_blue) + 2 -> get(R.drawable.player_ship2_blue) + else -> get(R.drawable.player_ship3_blue) + } + + override fun getEnemyShip(): Drawable = get(R.drawable.enemy_red_2) + + override fun getExplosionFrame(frame: Int): Drawable = get("explode_$frame".hashCode()) + + override fun getPowerUpBolt(): Drawable = get(R.drawable.powerup_bolt) + + override fun getPowerUpShield(): Drawable = get(R.drawable.powerup_shield) + + override fun getPowerUpStar(): Drawable = get(R.drawable.powerup_star) + + override fun get(resId: Int): Drawable { + synchronized(cache) { + if (!cache.containsKey(resId)) { + val drawable = ContextCompat.getDrawable(context, resId) + cache[resId] = drawable + } + return cache[resId]!! + } + } + + /** + * Caches explosion frames. + */ + private fun cacheExplosionFrame() { + synchronized(cache) { + splitExplosionAnimation().forEachIndexed { frame, drawable -> + cache["explode_${frame+1}".hashCode()] = drawable + } + } + } + + /** + * Splits a bitmap and returns explosion frames. + */ + private fun splitExplosionAnimation(): List { + val drawables = mutableListOf() + val fullBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.explode) + val frameSize = fullBitmap.width / EXPLODE_FRAMES_PER_LINE + for(frame in 1 until EXPLODE_MAX_FRAMES + 1) { + val line = ((frame - 1) / EXPLODE_FRAMES_PER_LINE) + val column = ((frame - 1) % EXPLODE_FRAMES_PER_LINE) + val frameBitmap = Bitmap.createBitmap(frameSize, frameSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(frameBitmap) + canvas.drawBitmap( + fullBitmap, + Rect( + column * frameSize, + line * frameSize, + column * frameSize + frameSize, + line * frameSize + frameSize + ), + Rect( + 0, + 0, + frameSize, + frameSize + ), + null + ) + drawables.add(BitmapDrawable(context.resources, frameBitmap)) + } + return drawables + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/ScoresImpl.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/ScoresImpl.kt new file mode 100644 index 0000000..19c5ea2 --- /dev/null +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/ScoresImpl.kt @@ -0,0 +1,36 @@ +package com.matthiaslapierre.spaceshooter.resources.impl + +import android.content.Context +import android.content.SharedPreferences +import com.matthiaslapierre.spaceshooter.resources.Scores + + +/** + * Stores scores locally by using the Preferences API. + */ +class ScoresImpl: Scores { + + companion object { + private const val PREF_DEFAULT = "com.matthiaslapierre.spaceshooter.PREF_DEFAULT" + private const val HIGH_SCORE = "high_score" + } + + override fun highScore(context: Context): Int { + val p: SharedPreferences = context.getSharedPreferences( + PREF_DEFAULT, + Context.MODE_PRIVATE + ) + return p.getInt(HIGH_SCORE, 0) + } + + override fun isNewBestScore(context: Context, score: Int): Boolean = score > highScore(context) + + override fun storeHighScore(context: Context, score: Int) { + val p: SharedPreferences = context.getSharedPreferences( + PREF_DEFAULT, + Context.MODE_PRIVATE + ) + p.edit().putInt(HIGH_SCORE, score).apply() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/SoundEngineImpl.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/SoundEngineImpl.kt new file mode 100644 index 0000000..d8ea0ac --- /dev/null +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/SoundEngineImpl.kt @@ -0,0 +1,164 @@ +package com.matthiaslapierre.spaceshooter.resources.impl + +import android.content.res.AssetFileDescriptor +import android.content.res.AssetManager +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.MediaPlayer +import android.media.SoundPool +import android.os.Build +import com.matthiaslapierre.spaceshooter.Constants +import com.matthiaslapierre.spaceshooter.resources.SoundEngine +import java.io.IOException + +/** + * Handles sounds present in the game. + */ +class SoundEngineImpl( + private val assets: AssetManager +): SoundEngine { + + companion object{ + private const val SOUND_BTN_PRESS = 0 + private const val SOUND_CRASH = 1 + private const val SOUND_METEOR_EXPLODE = 2 + private const val SOUND_SHIP_EXPLODE = 3 + private const val SOUND_GET_POWER_UP = 4 + private const val SOUND_GAME_OVER = 5 + private const val SOUND_SHOT_HIT = 6 + + private const val MUSIC_MENU = "musics/menu.ogg" + private const val MUSIC_PLAY = "musics/play.ogg" + private const val MUSIC_GAME_OVER = "musics/game_over.ogg" + } + + /** + * To play short sounds. + */ + private val soundPool: SoundPool + + /** + * To play music. + */ + private var player: MediaPlayer? = null + + /** + * Cache. + */ + private val sounds: Array = arrayOfNulls(7) + + init { + soundPool = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val audioAttributes = AudioAttributes + .Builder() + .setUsage(AudioAttributes.USAGE_GAME) + .build() + SoundPool + .Builder() + .setMaxStreams(Constants.SOUND_MAX_STREAMS) + .setAudioAttributes(audioAttributes) + .build() + } else { + SoundPool( + Constants.SOUND_MAX_STREAMS, + AudioManager.STREAM_MUSIC, + 0 + ) + } + } + + override fun load() { + Thread(Runnable { + sounds[SOUND_BTN_PRESS] = soundPool.load(assets.openFd("sounds/sfx_btn_press.ogg"), 1) + sounds[SOUND_CRASH] = soundPool.load(assets.openFd("sounds/sfx_crash.ogg"), 1) + sounds[SOUND_METEOR_EXPLODE] = soundPool.load(assets.openFd("sounds/sfx_explode_meteor.ogg"), 1) + sounds[SOUND_SHIP_EXPLODE] = soundPool.load(assets.openFd("sounds/sfx_explode_ship.ogg"), 1) + sounds[SOUND_GET_POWER_UP] = soundPool.load(assets.openFd("sounds/sfx_get_power_up.ogg"), 1) + sounds[SOUND_GAME_OVER] = soundPool.load(assets.openFd("sounds/sfx_lose.ogg"), 1) + sounds[SOUND_SHOT_HIT] = soundPool.load(assets.openFd("sounds/sfx_shot_hit.ogg"), 1) + }).start() + } + + override fun release() { + sounds.filterNotNull().forEach { soundID -> + soundPool.unload(soundID) + } + stopMusic() + } + + override fun resume() { + player?.start() + } + + override fun pause() { + player?.pause() + } + + override fun playBtnPress() = playSound(SOUND_BTN_PRESS) + + override fun playCrash() = playSound(SOUND_CRASH) + + override fun playMeteorExplode() = playSound(SOUND_METEOR_EXPLODE) + + override fun playShipExplode() = playSound(SOUND_SHIP_EXPLODE) + + override fun playGetPowerUp() = playSound(SOUND_GET_POWER_UP) + + override fun playGameOver() = playSound(SOUND_GAME_OVER) + + override fun playShotHit() = playSound(SOUND_SHOT_HIT) + + override fun playMenuMusic() = playMusic(MUSIC_MENU) + + override fun playPlayMusic() = playMusic(MUSIC_PLAY) + + override fun playGameOverMusic() = playMusic(MUSIC_GAME_OVER) + + /** + * Plays a sound if it is in caches + */ + private fun playSound(index: Int, volume: Float = 0.5f, infiniteLoop: Boolean = false) { + sounds[index]?.let { soundId -> + val loop = if(infiniteLoop) -1 else 0 + soundPool.play(soundId, volume, volume, 1, loop, 1f) + } + } + + /** + * Plays a music. + */ + private fun playMusic(filename: String) { + stopMusic() + try { + val afd: AssetFileDescriptor = assets.openFd(filename) + player = MediaPlayer().apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_GAME) + .build() + ) + } + isLooping = true + setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) + setVolume(0.3f, 0.3f) + prepare() + start() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + /** + * Stops the music. + */ + private fun stopMusic() { + if(player != null) { + player?.release() + player = null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/TypefaceHelperImpl.kt b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/TypefaceHelperImpl.kt new file mode 100644 index 0000000..332ea8a --- /dev/null +++ b/app/src/main/java/com/matthiaslapierre/spaceshooter/resources/impl/TypefaceHelperImpl.kt @@ -0,0 +1,43 @@ +package com.matthiaslapierre.spaceshooter.resources.impl + +import android.content.res.AssetManager +import android.graphics.Typeface +import com.matthiaslapierre.spaceshooter.resources.TypefaceHelper +import java.util.* + +class TypefaceHelperImpl( + private val assets: AssetManager +): TypefaceHelper { + + companion object { + private const val FONT_FUTURE = "kenvector_future" + private const val FONT_FUTURE_THIN = "kenvector_future_thin" + } + + private val cache: Hashtable = Hashtable() + + override fun load() { + Thread(Runnable { + get(FONT_FUTURE) + get(FONT_FUTURE_THIN) + }).start() + } + + override fun getFutureTypeface(): Typeface = get(FONT_FUTURE)!! + + override fun getFutureThinTypeface(): Typeface = get(FONT_FUTURE_THIN)!! + + private fun get(name: String): Typeface? { + synchronized(cache) { + if (!cache.containsKey(name)) { + val t = Typeface.createFromAsset( + assets, + String.format("fonts/%s.ttf", name) + ) + cache[name] = t + } + return cache[name] + } + } + +} \ No newline at end of file