From 170e99937249ce427d2431001cfb4e16068a95cc Mon Sep 17 00:00:00 2001 From: Saif Khan Date: Tue, 19 Jan 2021 04:30:18 +0100 Subject: [PATCH] Added a Python script to generate tiles, map data and source code from a tile sheet. This generated code can be used with the Tiled library in an Android activity then. --- .idea/modules.xml | 1 + .idea/other.xml | 6 + demo/build.gradle | 2 +- .../aspirasoft/apis/tiled/demo/MarioDemo.kt | 173 +++++++++--------- utils/.gitignore | 143 +++++++++++++++ utils/requirements.txt | 2 + utils/src/Activity.kt | 62 +++++++ utils/src/layout.xml | 9 + utils/src/main.py | 121 ++++++++++++ 9 files changed, 433 insertions(+), 86 deletions(-) create mode 100644 .idea/other.xml create mode 100644 utils/.gitignore create mode 100644 utils/requirements.txt create mode 100644 utils/src/Activity.kt create mode 100644 utils/src/layout.xml create mode 100644 utils/src/main.py diff --git a/.idea/modules.xml b/.idea/modules.xml index cd2be9f..54c03d2 100755 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -5,6 +5,7 @@ + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..a708ec7 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/demo/build.gradle b/demo/build.gradle index 31a55b6..bc0f42f 100755 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -5,7 +5,7 @@ android { compileSdkVersion 29 defaultConfig { - applicationId "co.aspirasoft.android.demo" + applicationId "dev.aspirasoft.apis.tiled.demo" minSdkVersion 15 targetSdkVersion 29 versionCode 1 diff --git a/demo/src/main/java/dev/aspirasoft/apis/tiled/demo/MarioDemo.kt b/demo/src/main/java/dev/aspirasoft/apis/tiled/demo/MarioDemo.kt index 23cc9c1..12ee6a1 100755 --- a/demo/src/main/java/dev/aspirasoft/apis/tiled/demo/MarioDemo.kt +++ b/demo/src/main/java/dev/aspirasoft/apis/tiled/demo/MarioDemo.kt @@ -11,100 +11,103 @@ class MarioDemo : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_demo) + loadMapFromAssets("mario.txt") + } - var reader: BufferedReader? = null - try { - reader = BufferedReader(InputStreamReader(assets.open("mario.txt"), "UTF-8")); - - // do reading, usually loop until end of file reading - var map = "" + /** + * Loads map data from a file in assets folder. + * + * @param filename Name of the file in assets folder. + * @throws Exception If the given file could not be opened. + */ + @Throws(Exception::class) + private fun loadMapFromAssets(filename: String) { + val reader = BufferedReader(InputStreamReader(assets.open(filename), "UTF-8")) + reader.use { + var data = "" var line = reader.readLine() while (line != null) { - map += line + "\n" + data += line + "\n" line = reader.readLine() } - val mapData = arrayOf(map) - arrayOf(R.id.tiledView).withIndex().forEach { (index, id) -> - val tiledView = findViewById(id) - createLookupMap().forEach { (id, image) -> tiledView.addImage(id, image) } - tiledView.post { - tiledView.setData(mapData[index]) - } - tiledView.setOnTileClicked { c: Int, r: Int -> - val char = tiledView[c, r] - val fruits = listOf("e", "n", "o") - if (char in fruits) { - tiledView[c, r] = "" - } - } - } - } catch (ex: Exception) { - //log the exception - } finally { - if (reader != null) { - try { - reader.close(); - } catch (ex: Exception) { - //log the exception - } - } + onMapDataReady(data) + } + } + + /** + * Receives the map data to finish tilemap setup. + * + * @param data The complete tilemap, encoded in a string. + */ + private fun onMapDataReady(data: String) { + val tiledView = findViewById(R.id.tiledView) + createLookupTable(tiledView) + tiledView.post { tiledView.setData(data) } + tiledView.setOnTileClicked { column: Int, row: Int -> + // TODO: Handle clicks on tiles here } } - private fun createLookupMap(): HashMap { - val map = HashMap() - map["a"] = R.drawable.t_mario_a - map["b"] = R.drawable.t_mario_b - map["c"] = R.drawable.t_mario_c - map["d"] = R.drawable.t_mario_d - map["e"] = R.drawable.t_mario_e - map["f"] = R.drawable.t_mario_f - map["g"] = R.drawable.t_mario_g - map["h"] = R.drawable.t_mario_h - map["i"] = R.drawable.t_mario_i - map["j"] = R.drawable.t_mario_j - map["k"] = R.drawable.t_mario_k - map["l"] = R.drawable.t_mario_l - map["m"] = R.drawable.t_mario_m - map["n"] = R.drawable.t_mario_n - map["o"] = R.drawable.t_mario_o - map["p"] = R.drawable.t_mario_p - map["q"] = R.drawable.t_mario_q - map["r"] = R.drawable.t_mario_r - map["s"] = R.drawable.t_mario_s - map["t"] = R.drawable.t_mario_t - map["u"] = R.drawable.t_mario_u - map["v"] = R.drawable.t_mario_v - map["w"] = R.drawable.t_mario_w - map["x"] = R.drawable.t_mario_x - map["y"] = R.drawable.t_mario_y - map["z"] = R.drawable.t_mario_z - map["aa"] = R.drawable.t_mario_aa - map["bb"] = R.drawable.t_mario_bb - map["cc"] = R.drawable.t_mario_cc - map["dd"] = R.drawable.t_mario_dd - map["ee"] = R.drawable.t_mario_ee - map["ff"] = R.drawable.t_mario_ff - map["gg"] = R.drawable.t_mario_gg - map["hh"] = R.drawable.t_mario_hh - map["ii"] = R.drawable.t_mario_ii - map["jj"] = R.drawable.t_mario_jj - map["kk"] = R.drawable.t_mario_kk - map["ll"] = R.drawable.t_mario_ll - map["mm"] = R.drawable.t_mario_mm - map["nn"] = R.drawable.t_mario_nn - map["oo"] = R.drawable.t_mario_oo - map["pp"] = R.drawable.t_mario_pp - map["qq"] = R.drawable.t_mario_qq - map["rr"] = R.drawable.t_mario_rr - map["ss"] = R.drawable.t_mario_ss - map["tt"] = R.drawable.t_mario_tt - map["uu"] = R.drawable.t_mario_uu - map["vv"] = R.drawable.t_mario_vv - map["ww"] = R.drawable.t_mario_ww - map["xx"] = R.drawable.t_mario_xx - return map + /** + * Creates a lookup table for the tilemap. + * + * A lookup table maps keys to a drawable resources which should be + * drawn in place of that key. + * + * @param tilemap The tilemap view for which to create the mapping. + */ + private fun createLookupTable(tilemap: TiledView) { + tilemap.addImage("a", R.drawable.t_mario_a) + tilemap.addImage("b", R.drawable.t_mario_b) + tilemap.addImage("c", R.drawable.t_mario_c) + tilemap.addImage("d", R.drawable.t_mario_d) + tilemap.addImage("e", R.drawable.t_mario_e) + tilemap.addImage("f", R.drawable.t_mario_f) + tilemap.addImage("g", R.drawable.t_mario_g) + tilemap.addImage("h", R.drawable.t_mario_h) + tilemap.addImage("i", R.drawable.t_mario_i) + tilemap.addImage("j", R.drawable.t_mario_j) + tilemap.addImage("k", R.drawable.t_mario_k) + tilemap.addImage("l", R.drawable.t_mario_l) + tilemap.addImage("m", R.drawable.t_mario_m) + tilemap.addImage("n", R.drawable.t_mario_n) + tilemap.addImage("o", R.drawable.t_mario_o) + tilemap.addImage("p", R.drawable.t_mario_p) + tilemap.addImage("q", R.drawable.t_mario_q) + tilemap.addImage("r", R.drawable.t_mario_r) + tilemap.addImage("s", R.drawable.t_mario_s) + tilemap.addImage("t", R.drawable.t_mario_t) + tilemap.addImage("u", R.drawable.t_mario_u) + tilemap.addImage("v", R.drawable.t_mario_v) + tilemap.addImage("w", R.drawable.t_mario_w) + tilemap.addImage("x", R.drawable.t_mario_x) + tilemap.addImage("y", R.drawable.t_mario_y) + tilemap.addImage("z", R.drawable.t_mario_z) + tilemap.addImage("aa", R.drawable.t_mario_aa) + tilemap.addImage("bb", R.drawable.t_mario_bb) + tilemap.addImage("cc", R.drawable.t_mario_cc) + tilemap.addImage("dd", R.drawable.t_mario_dd) + tilemap.addImage("ee", R.drawable.t_mario_ee) + tilemap.addImage("ff", R.drawable.t_mario_ff) + tilemap.addImage("gg", R.drawable.t_mario_gg) + tilemap.addImage("hh", R.drawable.t_mario_hh) + tilemap.addImage("ii", R.drawable.t_mario_ii) + tilemap.addImage("jj", R.drawable.t_mario_jj) + tilemap.addImage("kk", R.drawable.t_mario_kk) + tilemap.addImage("ll", R.drawable.t_mario_ll) + tilemap.addImage("mm", R.drawable.t_mario_mm) + tilemap.addImage("nn", R.drawable.t_mario_nn) + tilemap.addImage("oo", R.drawable.t_mario_oo) + tilemap.addImage("pp", R.drawable.t_mario_pp) + tilemap.addImage("qq", R.drawable.t_mario_qq) + tilemap.addImage("rr", R.drawable.t_mario_rr) + tilemap.addImage("ss", R.drawable.t_mario_ss) + tilemap.addImage("tt", R.drawable.t_mario_tt) + tilemap.addImage("uu", R.drawable.t_mario_uu) + tilemap.addImage("vv", R.drawable.t_mario_vv) + tilemap.addImage("ww", R.drawable.t_mario_ww) + tilemap.addImage("xx", R.drawable.t_mario_xx) } } \ No newline at end of file diff --git a/utils/.gitignore b/utils/.gitignore new file mode 100644 index 0000000..e590055 --- /dev/null +++ b/utils/.gitignore @@ -0,0 +1,143 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Datasets +data/ \ No newline at end of file diff --git a/utils/requirements.txt b/utils/requirements.txt new file mode 100644 index 0000000..3be7a5c --- /dev/null +++ b/utils/requirements.txt @@ -0,0 +1,2 @@ +numpy==1.19.5 +opencv-python==4.5.1.48 diff --git a/utils/src/Activity.kt b/utils/src/Activity.kt new file mode 100644 index 0000000..6728db3 --- /dev/null +++ b/utils/src/Activity.kt @@ -0,0 +1,62 @@ +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import dev.aspirasoft.apis.tiled.TiledView +import java.io.BufferedReader +import java.io.InputStreamReader + +class ACTIVITY_NAME : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.LAYOUT_NAME) + loadMapFromAssets("MAP_DATA_FILE") + } + + /** + * Loads map data from a file in assets folder. + * + * @param filename Name of the file in assets folder. + * @throws Exception If the given file could not be opened. + */ + @Throws(Exception::class) + private fun loadMapFromAssets(filename: String) { + val reader = BufferedReader(InputStreamReader(assets.open(filename), "UTF-8")) + reader.use { + var data = "" + var line = reader.readLine() + while (line != null) { + data += line + "\n" + line = reader.readLine() + } + + onMapDataReady(data) + } + } + + /** + * Receives the map data to finish tilemap setup. + * + * @param data The complete tilemap, encoded in a string. + */ + private fun onMapDataReady(data: String) { + val tiledView = findViewById(R.id.tiledView) + createLookupTable(tiledView) + tiledView.post { tiledView.setData(data) } + tiledView.setOnTileClicked { column: Int, row: Int -> + // TODO: Handle clicks on tiles here + } + } + + /** + * Creates a lookup table for the tilemap. + * + * A lookup table maps keys to a drawable resources which should be + * drawn in place of that key. + * + * @param tilemap The tilemap view for which to create the mapping. + */ + private fun createLookupTable(tilemap: TiledView) { + MAP_LOOKUP_TABLE + } + +} \ No newline at end of file diff --git a/utils/src/layout.xml b/utils/src/layout.xml new file mode 100644 index 0000000..344c2ec --- /dev/null +++ b/utils/src/layout.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/utils/src/main.py b/utils/src/main.py new file mode 100644 index 0000000..94c300c --- /dev/null +++ b/utils/src/main.py @@ -0,0 +1,121 @@ +import argparse +import os + +import cv2 +import numpy as np + +parser = argparse.ArgumentParser(description='Make tiles from an image.') +parser.add_argument('src', help='The image to make tiles from.') +parser.add_argument('t_w', help='The width of a tile. Must be divisible by image width.', type=int) +parser.add_argument('t_h', help='The height of a tile. Must be divisible by image height.', type=int) +parser.add_argument('--save', help='Path where tile images will be saved. Current directory by default.', + default='./') +args = parser.parse_args() + +# Open input image +prefix = os.path.splitext(os.path.split(args.src)[-1])[0].strip().lower().replace('_', '').replace('-', '') +image = cv2.imread(args.src, cv2.IMREAD_UNCHANGED) +h, w, _ = image.shape + +# Confirm tile dimensions are divisible by image dimensions +assert h % args.t_h == 0 +assert w % args.t_w == 0 + +# Define Android project structure +source_dir = os.path.join(args.save, 'app/src/main/java/') +assets_dir = os.path.join(args.save, 'app/src/main/assets/') +images_dir = os.path.join(args.save, 'app/src/main/res/drawable/') +layout_dir = os.path.join(args.save, 'app/src/main/res/layout/') + +# Create project directories if they don't exist +for folder in [source_dir, assets_dir, images_dir, layout_dir]: + if not os.path.exists(folder): + os.makedirs(folder) + +# Calculate number of tiles in image, and create an array to save them +count_h = int(h / args.t_h) +count_w = int(w / args.t_w) +count = (count_h, count_w) + +idx = 0 +valid_ids = [chr(i) for i in range(ord('a'), ord('z') + 1)] +tile_ids = np.zeros(shape=count, dtype=object) +tiles = dict() + +# Slide a tile-sized window on source image +for r in range(0, int(h - args.t_h) + 1, int(args.t_h)): + for c in range(0, int(w - args.t_w) + 1, int(args.t_w)): + ih = int(r / args.t_h) + iw = int(c / args.t_w) + + # Crop out the tile + tile = image[r:r + args.t_h, c:c + args.t_w, :] + + # If this is a transparent tile, ignore it + if (tile == np.zeros(tile.shape)).all(): + continue + + # If a visually similar tile was cropped before, assign this + # tile the same id as that one. + exists = False + for key, v in tiles.items(): + if (v == tile).all(): + tile_ids[ih][iw] = key + exists = True + break + + # If this is a new tile, assign it a unique id + if not exists: + # assign it a unique key + key = '' + for j in range(int(idx / len(valid_ids)) + 1): + key += valid_ids[int(idx % len(valid_ids))] + + # save cropped tile image + outfile = os.path.join(images_dir, f't_{prefix}_{key}.png') + cv2.imwrite(outfile, tile) + + tiles[key] = tile + tile_ids[ih][iw] = key + idx += 1 + +# Generate tilemap data and lookup table +map_data = '' +map_lookup_table = '' +processed = [] +for r in range(tile_ids.shape[0]): + for c in range(tile_ids.shape[1]): + key = tile_ids[r][c] + map_data += f'{key}|' + if key in processed: + continue + + outfile = f't_{prefix}_{key}' + map_lookup_table += f'tilemap.addImage("{key}", R.drawable.{outfile})\n ' + processed.append(key) + map_data += '\n' + +# Save tilemap data +tilemap_data_file_name = f'{prefix}.txt' +with open(os.path.join(assets_dir, tilemap_data_file_name), 'w') as tilemap_data_file: + tilemap_data_file.write(map_data) + +# Generate and save layout code +layout_name = f'activity_{prefix}' +with open('layout.xml', 'r') as layout_template: + template = layout_template.read() + + with open(os.path.join(layout_dir, f'{layout_name}.xml'), 'w') as generated_layout: + generated_layout.write(template) + +# Generate and save activity code +activity_name = f'{prefix.capitalize()}Activity' +with open('Activity.kt', 'r') as activity_template: + template = activity_template.read() + template = template.replace('ACTIVITY_NAME', activity_name) + template = template.replace('LAYOUT_NAME', layout_name) + template = template.replace('MAP_DATA_FILE', tilemap_data_file_name) + template = template.replace('MAP_LOOKUP_TABLE', map_lookup_table) + + with open(os.path.join(source_dir, f'{activity_name}.kt'), 'w') as generated_activity: + generated_activity.write(template)