tkunity 2026.10.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tkunity/__init__.py +27 -0
- tkunity/__main__.py +8 -0
- tkunity/api.py +112 -0
- tkunity/cli.py +106 -0
- tkunity/components.py +125 -0
- tkunity/editor.py +702 -0
- tkunity/export.py +740 -0
- tkunity/py.typed +0 -0
- tkunity/pyu.py +125 -0
- tkunity/runtime.py +232 -0
- tkunity-2026.10.31.dist-info/LICENSE +216 -0
- tkunity-2026.10.31.dist-info/METADATA +194 -0
- tkunity-2026.10.31.dist-info/RECORD +16 -0
- tkunity-2026.10.31.dist-info/WHEEL +5 -0
- tkunity-2026.10.31.dist-info/entry_points.txt +2 -0
- tkunity-2026.10.31.dist-info/top_level.txt +1 -0
tkunity/export.py
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""APK 导出器:把 .pyu 项目字典生成一个可直接构建的 Android Studio/Gradle 工程。
|
|
3
|
+
|
|
4
|
+
产物特点:
|
|
5
|
+
- 纯 Java + 系统自带 org.json / android.graphics,零第三方依赖;
|
|
6
|
+
- minSdk 26 / targetSdk 34,JDK 17,AGP 8.2.2,Gradle 8.5;
|
|
7
|
+
- 内置动作与桌面预览一致(BUILTIN_ACTIONS:move-* / bounce / stop / print);
|
|
8
|
+
- 自定义 Python 表达式只用于桌面预览,APK 不支持(文档已注明)。
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .pyu import MAGIC
|
|
17
|
+
|
|
18
|
+
__all__ = ["build", "DEFAULT_PACKAGE"]
|
|
19
|
+
|
|
20
|
+
DEFAULT_PACKAGE = "com.oiiaio.tkunity"
|
|
21
|
+
APP_NAME = "TkUnityGame"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build(project, outdir, package=DEFAULT_PACKAGE, app_name=None):
|
|
25
|
+
"""生成 Android 工程到 outdir,返回生成的文件路径列表。"""
|
|
26
|
+
meta = project.get("meta", {})
|
|
27
|
+
name = app_name or meta.get("name", APP_NAME)
|
|
28
|
+
files = []
|
|
29
|
+
paths = {
|
|
30
|
+
"settings": os.path.join(outdir, "settings.gradle"),
|
|
31
|
+
"root_build": os.path.join(outdir, "build.gradle"),
|
|
32
|
+
"gradle_props": os.path.join(outdir, "gradle.properties"),
|
|
33
|
+
"app_build": os.path.join(outdir, "app", "build.gradle"),
|
|
34
|
+
"manifest": os.path.join(outdir, "app", "src", "main", "AndroidManifest.xml"),
|
|
35
|
+
"strings": os.path.join(outdir, "app", "src", "main", "res", "values", "strings.xml"),
|
|
36
|
+
"pyu": os.path.join(outdir, "app", "src", "main", "assets", "game.pyu"),
|
|
37
|
+
"activity": os.path.join(outdir, "app", "src", "main", "java", package.replace(".", "/"), "MainActivity.java"),
|
|
38
|
+
"gameview": os.path.join(outdir, "app", "src", "main", "java", package.replace(".", "/"), "GameView.java"),
|
|
39
|
+
"bat": os.path.join(outdir, "build_apk.bat"),
|
|
40
|
+
"sh": os.path.join(outdir, "build_apk.sh"),
|
|
41
|
+
"readme": os.path.join(outdir, "README_BUILD.md"),
|
|
42
|
+
"local_props": os.path.join(outdir, "local.properties.example"),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for path in paths.values():
|
|
46
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
47
|
+
|
|
48
|
+
def write(key, content):
|
|
49
|
+
with open(paths[key], "w", encoding="utf-8", newline="\n") as fh:
|
|
50
|
+
fh.write(content)
|
|
51
|
+
files.append(paths[key])
|
|
52
|
+
|
|
53
|
+
write("settings", _settings_gradle())
|
|
54
|
+
write("root_build", _root_build_gradle())
|
|
55
|
+
write("gradle_props", _gradle_properties())
|
|
56
|
+
write("app_build", _app_build_gradle(package, name))
|
|
57
|
+
write("manifest", _manifest(package))
|
|
58
|
+
write("strings", _strings(name))
|
|
59
|
+
write("pyu", _pyu_blob(project))
|
|
60
|
+
write("activity", _main_activity(package))
|
|
61
|
+
write("gameview", _game_view(package, project))
|
|
62
|
+
write("bat", _build_bat())
|
|
63
|
+
write("sh", _build_sh())
|
|
64
|
+
write("readme", _build_readme(outdir, name))
|
|
65
|
+
write("local_props", "# 复制为 local.properties 并填写你的 Android SDK 路径\n# sdk.dir=C:\\\\Users\\\\you\\\\AppData\\\\Local\\\\Android\\\\Sdk\nsdk.dir=/path/to/your/android/sdk\n")
|
|
66
|
+
return files
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------- 内容生成
|
|
70
|
+
def _pyu_blob(project):
|
|
71
|
+
blob = json.dumps(project, ensure_ascii=False, indent=1,
|
|
72
|
+
separators=(",", ": ")).encode("utf-8")
|
|
73
|
+
return (MAGIC + blob).decode("utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _settings_gradle():
|
|
77
|
+
return """pluginManagement {
|
|
78
|
+
repositories {
|
|
79
|
+
google()
|
|
80
|
+
mavenCentral()
|
|
81
|
+
gradlePluginPortal()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
dependencyResolutionManagement {
|
|
85
|
+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
86
|
+
repositories {
|
|
87
|
+
google()
|
|
88
|
+
mavenCentral()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
rootProject.name = "TkUnityGame"
|
|
92
|
+
include ':app'
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _root_build_gradle():
|
|
97
|
+
return """plugins {
|
|
98
|
+
id 'com.android.application' version '8.2.2' apply false
|
|
99
|
+
}
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _gradle_properties():
|
|
104
|
+
return """org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
105
|
+
android.useAndroidX=false
|
|
106
|
+
android.enableJetifier=false
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _app_build_gradle(package, name):
|
|
111
|
+
return """plugins {
|
|
112
|
+
id 'com.android.application'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
android {
|
|
116
|
+
namespace '%s'
|
|
117
|
+
compileSdk 34
|
|
118
|
+
|
|
119
|
+
defaultConfig {
|
|
120
|
+
applicationId "%s"
|
|
121
|
+
minSdk 26
|
|
122
|
+
targetSdk 34
|
|
123
|
+
versionCode 1
|
|
124
|
+
versionName "%s"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
compileOptions {
|
|
128
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
129
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
buildTypes {
|
|
133
|
+
release {
|
|
134
|
+
minifyEnabled false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// tkunity 游戏:零第三方依赖,无需任何 dependencies 块
|
|
140
|
+
""" % (package, package, __version__)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _manifest(package):
|
|
144
|
+
return """<?xml version="1.0" encoding="utf-8"?>
|
|
145
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
146
|
+
|
|
147
|
+
<application
|
|
148
|
+
android:label="@string/app_name"
|
|
149
|
+
android:allowBackup="true"
|
|
150
|
+
android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen">
|
|
151
|
+
|
|
152
|
+
<activity
|
|
153
|
+
android:name=".MainActivity"
|
|
154
|
+
android:exported="true"
|
|
155
|
+
android:screenOrientation="landscape"
|
|
156
|
+
android:configChanges="orientation|screenSize|keyboardHidden">
|
|
157
|
+
<intent-filter>
|
|
158
|
+
<action android:name="android.intent.action.MAIN" />
|
|
159
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
160
|
+
</intent-filter>
|
|
161
|
+
</activity>
|
|
162
|
+
</application>
|
|
163
|
+
|
|
164
|
+
</manifest>
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _strings(name):
|
|
169
|
+
return """<resources>
|
|
170
|
+
<string name="app_name">%s</string>
|
|
171
|
+
</resources>
|
|
172
|
+
""" % name
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _main_activity(package):
|
|
176
|
+
return """package %s;
|
|
177
|
+
|
|
178
|
+
import android.app.Activity;
|
|
179
|
+
import android.os.Bundle;
|
|
180
|
+
import android.view.WindowManager;
|
|
181
|
+
|
|
182
|
+
public class MainActivity extends Activity {
|
|
183
|
+
@Override
|
|
184
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
185
|
+
super.onCreate(savedInstanceState);
|
|
186
|
+
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
187
|
+
setContentView(new GameView(this));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
""" % package
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _build_readme(outdir, name):
|
|
194
|
+
return """# %s - Android 构建说明
|
|
195
|
+
|
|
196
|
+
本目录由 tkunity %s 导出,是一个 **零第三方依赖** 的纯 Java Android 工程。
|
|
197
|
+
|
|
198
|
+
## 构建要求
|
|
199
|
+
- JDK 17
|
|
200
|
+
- Android SDK(compileSdk 34)
|
|
201
|
+
- 设置环境变量 ANDROID_HOME,或把 local.properties.example 复制为 local.properties 并填写 sdk.dir
|
|
202
|
+
- 无需 Android Studio,命令行即可构建
|
|
203
|
+
|
|
204
|
+
## 构建
|
|
205
|
+
Windows:
|
|
206
|
+
build_apk.bat
|
|
207
|
+
Linux/macOS:
|
|
208
|
+
./build_apk.sh
|
|
209
|
+
|
|
210
|
+
脚本会自动下载 Gradle 8.5(若本机未安装 gradle)。
|
|
211
|
+
|
|
212
|
+
## 产物
|
|
213
|
+
app/build/outputs/apk/debug/app-debug.apk
|
|
214
|
+
|
|
215
|
+
## 说明
|
|
216
|
+
- 游戏数据在 app/src/main/assets/game.pyu(.pyu 格式,图片 base64 内嵌)
|
|
217
|
+
- 渲染器 GameView.java 使用 android.graphics.Canvas 实现,支持:
|
|
218
|
+
网格背景 / rect / oval / 图片 / 文本 / 层级 / 触摸拖动 / 点击动作 / 定时器
|
|
219
|
+
- 动作支持(与桌面预览通用):move-left move-right move-up move-down bounce stop print(...)
|
|
220
|
+
- 自定义 Python 表达式仅用于桌面预览;如需更多玩法,直接修改 GameView.java 扩展。
|
|
221
|
+
""" % (name, __version__)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _build_bat():
|
|
225
|
+
return """@echo off
|
|
226
|
+
setlocal enabledelayedexpansion
|
|
227
|
+
set "GRADLE_VERSION=8.5"
|
|
228
|
+
set "DIR=%~dp0"
|
|
229
|
+
set "TOOL_DIR=%DIR%.gradle-tool"
|
|
230
|
+
set "GRADLE_HOME=%TOOL_DIR%\\gradle-%GRADLE_VERSION%"
|
|
231
|
+
|
|
232
|
+
set "GRADLE_CMD="
|
|
233
|
+
where gradle >nul 2>nul && set "GRADLE_CMD=gradle"
|
|
234
|
+
if not defined GRADLE_CMD (
|
|
235
|
+
if not exist "%GRADLE_HOME%\\bin\\gradle.bat" (
|
|
236
|
+
echo [tkunity] 正在下载 Gradle %GRADLE_VERSION% ...
|
|
237
|
+
if not exist "%TOOL_DIR%" mkdir "%TOOL_DIR%"
|
|
238
|
+
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
|
239
|
+
"$u='https://services.gradle.org/distributions/gradle-%GRADLE_VERSION%-bin.zip';" ^
|
|
240
|
+
"$z='%TOOL_DIR%\\gradle.zip';" ^
|
|
241
|
+
"[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;" ^
|
|
242
|
+
"Invoke-WebRequest -Uri $u -OutFile $z;" ^
|
|
243
|
+
"Expand-Archive -Path $z -DestinationPath '%TOOL_DIR%' -Force"
|
|
244
|
+
if errorlevel 1 (
|
|
245
|
+
echo [tkunity] Gradle 下载失败,请手动下载后重试:
|
|
246
|
+
echo https://services.gradle.org/distributions/gradle-%GRADLE_VERSION%-bin.zip
|
|
247
|
+
exit /b 1
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
set "GRADLE_CMD=%GRADLE_HOME%\\bin\\gradle.bat"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if not defined ANDROID_HOME (
|
|
254
|
+
if not exist "%DIR%local.properties" (
|
|
255
|
+
echo [tkunity] 未设置 ANDROID_HOME 且缺少 local.properties
|
|
256
|
+
echo 请创建 %DIR%local.properties,内容: sdk.dir=C:\\\\你的Android\\\\SDK\\\\路径
|
|
257
|
+
exit /b 1
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
cd /d "%DIR%"
|
|
262
|
+
call "%GRADLE_CMD%" assembleDebug
|
|
263
|
+
if errorlevel 1 (
|
|
264
|
+
echo [tkunity] 构建失败
|
|
265
|
+
exit /b 1
|
|
266
|
+
)
|
|
267
|
+
echo.
|
|
268
|
+
echo APK: %DIR%app\\build\\outputs\\apk\\debug\\app-debug.apk
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _build_sh():
|
|
273
|
+
return """#!/usr/bin/env bash
|
|
274
|
+
set -e
|
|
275
|
+
GRADLE_VERSION=8.5
|
|
276
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
277
|
+
TOOL_DIR="$DIR/.gradle-tool"
|
|
278
|
+
GRADLE_HOME="$TOOL_DIR/gradle-$GRADLE_VERSION"
|
|
279
|
+
|
|
280
|
+
if command -v gradle >/dev/null 2>&1; then
|
|
281
|
+
GRADLE_CMD=gradle
|
|
282
|
+
else
|
|
283
|
+
if [ ! -x "$GRADLE_HOME/bin/gradle" ]; then
|
|
284
|
+
echo "[tkunity] downloading Gradle $GRADLE_VERSION ..."
|
|
285
|
+
mkdir -p "$TOOL_DIR"
|
|
286
|
+
(curl -fsSL -o "$TOOL_DIR/gradle.zip" \\
|
|
287
|
+
"https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip") \\
|
|
288
|
+
|| (wget -q -O "$TOOL_DIR/gradle.zip" \\
|
|
289
|
+
"https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip")
|
|
290
|
+
unzip -q -o "$TOOL_DIR/gradle.zip" -d "$TOOL_DIR"
|
|
291
|
+
fi
|
|
292
|
+
GRADLE_CMD="$GRADLE_HOME/bin/gradle"
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
if [ -z "$ANDROID_HOME" ] && [ ! -f "$DIR/local.properties" ]; then
|
|
296
|
+
echo "[tkunity] ANDROID_HOME not set and no local.properties found."
|
|
297
|
+
echo " Create $DIR/local.properties with: sdk.dir=/path/to/your/android/sdk"
|
|
298
|
+
exit 1
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
cd "$DIR"
|
|
302
|
+
"$GRADLE_CMD" assembleDebug
|
|
303
|
+
echo
|
|
304
|
+
echo "APK: $DIR/app/build/outputs/apk/debug/app-debug.apk"
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _game_view(package, project):
|
|
309
|
+
"""生成 GameView.java 渲染器(含场景内置动作执行逻辑)。"""
|
|
310
|
+
return _GAME_VIEW_TEMPLATE.replace("@PACKAGE@", package)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
_GAME_VIEW_TEMPLATE = """package @PACKAGE@;
|
|
314
|
+
|
|
315
|
+
import android.content.Context;
|
|
316
|
+
import android.graphics.Bitmap;
|
|
317
|
+
import android.graphics.BitmapFactory;
|
|
318
|
+
import android.graphics.Canvas;
|
|
319
|
+
import android.graphics.Color;
|
|
320
|
+
import android.graphics.Paint;
|
|
321
|
+
import android.graphics.Rect;
|
|
322
|
+
import android.graphics.Typeface;
|
|
323
|
+
import android.os.Handler;
|
|
324
|
+
import android.os.Looper;
|
|
325
|
+
import android.util.AttributeSet;
|
|
326
|
+
import android.util.Base64;
|
|
327
|
+
import android.util.Log;
|
|
328
|
+
import android.view.MotionEvent;
|
|
329
|
+
import android.view.View;
|
|
330
|
+
|
|
331
|
+
import org.json.JSONArray;
|
|
332
|
+
import org.json.JSONObject;
|
|
333
|
+
|
|
334
|
+
import java.io.ByteArrayOutputStream;
|
|
335
|
+
import java.io.InputStream;
|
|
336
|
+
import java.nio.charset.StandardCharsets;
|
|
337
|
+
import java.util.ArrayList;
|
|
338
|
+
import java.util.Collections;
|
|
339
|
+
import java.util.Comparator;
|
|
340
|
+
import java.util.HashMap;
|
|
341
|
+
import java.util.List;
|
|
342
|
+
import java.util.Map;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* tkunity 游戏渲染器:纯 Android 系统 API,零第三方依赖。
|
|
346
|
+
* 读取 assets/game.pyu(.pyu 格式),渲染场景并驱动动作。
|
|
347
|
+
*/
|
|
348
|
+
public class GameView extends View {
|
|
349
|
+
private static final String TAG = "tkunity";
|
|
350
|
+
private static final int GRID = 20;
|
|
351
|
+
private static final int TICK_MS = 16;
|
|
352
|
+
|
|
353
|
+
private JSONObject project;
|
|
354
|
+
private final List<Entity> entities = new ArrayList<>();
|
|
355
|
+
private final Map<String, Bitmap> bitmaps = new HashMap<>();
|
|
356
|
+
private final Handler handler = new Handler(Looper.getMainLooper());
|
|
357
|
+
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
358
|
+
|
|
359
|
+
private Entity dragging;
|
|
360
|
+
private float dragDX, dragDY;
|
|
361
|
+
private int sceneW = 960;
|
|
362
|
+
private int sceneH = 640;
|
|
363
|
+
private int bgColor = Color.rgb(30, 30, 46);
|
|
364
|
+
private int offsetX = 0;
|
|
365
|
+
private int offsetY = 0;
|
|
366
|
+
private long startTime;
|
|
367
|
+
|
|
368
|
+
private final Runnable ticker = new Runnable() {
|
|
369
|
+
@Override
|
|
370
|
+
public void run() {
|
|
371
|
+
long now = System.currentTimeMillis();
|
|
372
|
+
long elapsed = now - startTime;
|
|
373
|
+
for (Entity e : entities) {
|
|
374
|
+
e.tick(elapsed, sceneW, sceneH);
|
|
375
|
+
}
|
|
376
|
+
invalidate();
|
|
377
|
+
handler.postDelayed(this, TICK_MS);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
public GameView(Context context) {
|
|
382
|
+
super(context);
|
|
383
|
+
init();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public GameView(Context context, AttributeSet attrs) {
|
|
387
|
+
super(context, attrs);
|
|
388
|
+
init();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
public GameView(Context context, AttributeSet attrs, int defStyle) {
|
|
392
|
+
super(context, attrs, defStyle);
|
|
393
|
+
init();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private void init() {
|
|
397
|
+
loadProject();
|
|
398
|
+
startTime = System.currentTimeMillis();
|
|
399
|
+
handler.post(ticker);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private void loadProject() {
|
|
403
|
+
try {
|
|
404
|
+
InputStream in = getContext().getAssets().open("game.pyu");
|
|
405
|
+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
406
|
+
byte[] buf = new byte[8192];
|
|
407
|
+
int n;
|
|
408
|
+
while ((n = in.read(buf)) != -1) {
|
|
409
|
+
bos.write(buf, 0, n);
|
|
410
|
+
}
|
|
411
|
+
in.close();
|
|
412
|
+
String text = new String(bos.toByteArray(), StandardCharsets.UTF_8);
|
|
413
|
+
int idx = text.indexOf('{');
|
|
414
|
+
project = new JSONObject(text.substring(idx));
|
|
415
|
+
|
|
416
|
+
JSONObject meta = project.optJSONObject("meta");
|
|
417
|
+
if (meta != null) {
|
|
418
|
+
sceneW = meta.optInt("scene_width", 960);
|
|
419
|
+
sceneH = meta.optInt("scene_height", 640);
|
|
420
|
+
bgColor = parseColor(meta.optString("background", "#1e1e2e"));
|
|
421
|
+
}
|
|
422
|
+
JSONArray arr = project.optJSONArray("entities");
|
|
423
|
+
if (arr == null) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
for (int i = 0; i < arr.length(); i++) {
|
|
427
|
+
entities.add(Entity.fromJson(arr.optJSONObject(i)));
|
|
428
|
+
}
|
|
429
|
+
} catch (Exception e) {
|
|
430
|
+
Log.e(TAG, "load project failed", e);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@Override
|
|
435
|
+
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
436
|
+
super.onSizeChanged(w, h, oldw, oldh);
|
|
437
|
+
offsetX = Math.max(0, (w - sceneW) / 2);
|
|
438
|
+
offsetY = Math.max(0, (h - sceneH) / 2);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@Override
|
|
442
|
+
protected void onDraw(Canvas canvas) {
|
|
443
|
+
super.onDraw(canvas);
|
|
444
|
+
canvas.drawColor(Color.rgb(32, 34, 40));
|
|
445
|
+
canvas.translate(offsetX, offsetY);
|
|
446
|
+
|
|
447
|
+
// 场景背景
|
|
448
|
+
canvas.drawRect(0, 0, sceneW, sceneH, paint);
|
|
449
|
+
|
|
450
|
+
// 网格
|
|
451
|
+
paint.setStyle(Paint.Style.STROKE);
|
|
452
|
+
paint.setStrokeWidth(1);
|
|
453
|
+
paint.setColor(Color.rgb(58, 58, 58));
|
|
454
|
+
for (int x = 0; x <= sceneW; x += GRID) {
|
|
455
|
+
canvas.drawLine(x, 0, x, sceneH, paint);
|
|
456
|
+
}
|
|
457
|
+
for (int y = 0; y <= sceneH; y += GRID) {
|
|
458
|
+
canvas.drawLine(0, y, sceneW, y, paint);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 实体按 layer 升序绘制
|
|
462
|
+
List<Entity> sorted = new ArrayList<>(entities);
|
|
463
|
+
Collections.sort(sorted, new Comparator<Entity>() {
|
|
464
|
+
@Override
|
|
465
|
+
public int compare(Entity a, Entity b) {
|
|
466
|
+
int c = Integer.compare(a.layer, b.layer);
|
|
467
|
+
return c != 0 ? c : Integer.compare(a.id, b.id);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
for (Entity e : sorted) {
|
|
471
|
+
drawEntity(canvas, e);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private void drawEntity(Canvas canvas, Entity e) {
|
|
476
|
+
if ("image".equals(e.shape) && !e.image.isEmpty()) {
|
|
477
|
+
Bitmap bmp = getBitmap(e.image);
|
|
478
|
+
if (bmp != null) {
|
|
479
|
+
canvas.drawBitmap(bmp, null,
|
|
480
|
+
new Rect(e.x, e.y, e.x + e.width, e.y + e.height), paint);
|
|
481
|
+
}
|
|
482
|
+
} else if ("oval".equals(e.shape)) {
|
|
483
|
+
paint.setStyle(Paint.Style.FILL);
|
|
484
|
+
paint.setColor(e.fill);
|
|
485
|
+
canvas.drawOval(e.x, e.y, e.x + e.width, e.y + e.height, paint);
|
|
486
|
+
paint.setStyle(Paint.Style.STROKE);
|
|
487
|
+
paint.setStrokeWidth(2);
|
|
488
|
+
paint.setColor(e.outline);
|
|
489
|
+
canvas.drawOval(e.x, e.y, e.x + e.width, e.y + e.height, paint);
|
|
490
|
+
} else {
|
|
491
|
+
paint.setStyle(Paint.Style.FILL);
|
|
492
|
+
paint.setColor(e.fill);
|
|
493
|
+
canvas.drawRect(e.x, e.y, e.x + e.width, e.y + e.height, paint);
|
|
494
|
+
paint.setStyle(Paint.Style.STROKE);
|
|
495
|
+
paint.setStrokeWidth(2);
|
|
496
|
+
paint.setColor(e.outline);
|
|
497
|
+
canvas.drawRect(e.x, e.y, e.x + e.width, e.y + e.height, paint);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (e.labelText != null && !e.labelText.isEmpty()) {
|
|
501
|
+
paint.setStyle(Paint.Style.FILL);
|
|
502
|
+
paint.setColor(e.labelColor);
|
|
503
|
+
paint.setTextSize(e.fontSize * getResources().getDisplayMetrics().density);
|
|
504
|
+
paint.setTypeface(e.bold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
|
|
505
|
+
Paint.Align oldAlign = paint.getTextAlign();
|
|
506
|
+
paint.setTextAlign(Paint.Align.CENTER);
|
|
507
|
+
Paint.FontMetrics fm = paint.getFontMetrics();
|
|
508
|
+
float cy = e.y + e.height / 2f - (fm.ascent + fm.descent) / 2f;
|
|
509
|
+
canvas.drawText(e.labelText, e.x + e.width / 2f, cy, paint);
|
|
510
|
+
paint.setTextAlign(oldAlign);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private Bitmap getBitmap(String assetName) {
|
|
515
|
+
if (bitmaps.containsKey(assetName)) {
|
|
516
|
+
return bitmaps.get(assetName);
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
JSONObject assets = project.optJSONObject("assets");
|
|
520
|
+
if (assets == null) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
String b64 = assets.optString(assetName, "");
|
|
524
|
+
if (b64.isEmpty()) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
byte[] data = Base64.decode(b64, Base64.DEFAULT);
|
|
528
|
+
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
|
|
529
|
+
bitmaps.put(assetName, bmp);
|
|
530
|
+
return bmp;
|
|
531
|
+
} catch (Exception e) {
|
|
532
|
+
Log.e(TAG, "decode image failed: " + assetName, e);
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// -------------------------------------------------- 触摸
|
|
538
|
+
@Override
|
|
539
|
+
public boolean onTouchEvent(MotionEvent event) {
|
|
540
|
+
float x = event.getX() - offsetX;
|
|
541
|
+
float y = event.getY() - offsetY;
|
|
542
|
+
switch (event.getActionMasked()) {
|
|
543
|
+
case MotionEvent.ACTION_DOWN: {
|
|
544
|
+
Entity hit = hitTest(x, y);
|
|
545
|
+
if (hit != null) {
|
|
546
|
+
dragging = hit;
|
|
547
|
+
dragDX = x - hit.x;
|
|
548
|
+
dragDY = y - hit.y;
|
|
549
|
+
hit.runAction(hit.tapAction);
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
case MotionEvent.ACTION_MOVE: {
|
|
554
|
+
if (dragging != null) {
|
|
555
|
+
dragging.x = Math.round(x - dragDX);
|
|
556
|
+
dragging.y = Math.round(y - dragDY);
|
|
557
|
+
invalidate();
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
case MotionEvent.ACTION_UP:
|
|
562
|
+
case MotionEvent.ACTION_CANCEL:
|
|
563
|
+
dragging = null;
|
|
564
|
+
return true;
|
|
565
|
+
default:
|
|
566
|
+
return super.onTouchEvent(event);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private Entity hitTest(float x, float y) {
|
|
571
|
+
List<Entity> sorted = new ArrayList<>(entities);
|
|
572
|
+
Collections.sort(sorted, new Comparator<Entity>() {
|
|
573
|
+
@Override
|
|
574
|
+
public int compare(Entity a, Entity b) {
|
|
575
|
+
int c = Integer.compare(b.layer, a.layer);
|
|
576
|
+
return c != 0 ? c : Integer.compare(b.id, a.id);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
for (Entity e : sorted) {
|
|
580
|
+
if (x >= e.x && x <= e.x + e.width && y >= e.y && y <= e.y + e.height) {
|
|
581
|
+
return e;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// -------------------------------------------------- 工具
|
|
588
|
+
static int parseColor(String hex) {
|
|
589
|
+
try {
|
|
590
|
+
if (hex == null || hex.isEmpty()) {
|
|
591
|
+
return Color.BLACK;
|
|
592
|
+
}
|
|
593
|
+
hex = hex.trim();
|
|
594
|
+
if (hex.startsWith("#")) {
|
|
595
|
+
hex = hex.substring(1);
|
|
596
|
+
}
|
|
597
|
+
if (hex.length() == 6) {
|
|
598
|
+
return Color.rgb(Integer.parseInt(hex.substring(0, 2), 16),
|
|
599
|
+
Integer.parseInt(hex.substring(2, 4), 16),
|
|
600
|
+
Integer.parseInt(hex.substring(4, 6), 16));
|
|
601
|
+
}
|
|
602
|
+
if (hex.length() == 8) {
|
|
603
|
+
return Color.argb(Integer.parseInt(hex.substring(6, 8), 16),
|
|
604
|
+
Integer.parseInt(hex.substring(0, 2), 16),
|
|
605
|
+
Integer.parseInt(hex.substring(2, 4), 16),
|
|
606
|
+
Integer.parseInt(hex.substring(4, 6), 16));
|
|
607
|
+
}
|
|
608
|
+
} catch (Exception ignored) {
|
|
609
|
+
// fall through
|
|
610
|
+
}
|
|
611
|
+
return Color.BLACK;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// -------------------------------------------------- 实体
|
|
615
|
+
public static class Entity {
|
|
616
|
+
int id;
|
|
617
|
+
String name = "obj";
|
|
618
|
+
int x, y, width = 80, height = 60, layer;
|
|
619
|
+
float rotation;
|
|
620
|
+
String shape = "rect";
|
|
621
|
+
int fill = Color.rgb(76, 139, 245);
|
|
622
|
+
int outline = Color.rgb(18, 52, 86);
|
|
623
|
+
String image = "";
|
|
624
|
+
String labelText = "";
|
|
625
|
+
int fontSize = 18;
|
|
626
|
+
int labelColor = Color.BLACK;
|
|
627
|
+
boolean bold = false;
|
|
628
|
+
String tapAction = "none";
|
|
629
|
+
String timerAction = "none";
|
|
630
|
+
int intervalMs = 1000;
|
|
631
|
+
long lastTick = 0;
|
|
632
|
+
long lastMove = 0;
|
|
633
|
+
int vx = 0;
|
|
634
|
+
int vy = 0;
|
|
635
|
+
|
|
636
|
+
static Entity fromJson(JSONObject o) throws Exception {
|
|
637
|
+
Entity e = new Entity();
|
|
638
|
+
e.id = o.optInt("id");
|
|
639
|
+
e.name = o.optString("name", "obj");
|
|
640
|
+
JSONObject comps = o.optJSONObject("components");
|
|
641
|
+
if (comps == null) {
|
|
642
|
+
return e;
|
|
643
|
+
}
|
|
644
|
+
JSONObject t = comps.optJSONObject("Transform");
|
|
645
|
+
if (t != null) {
|
|
646
|
+
e.x = t.optInt("x");
|
|
647
|
+
e.y = t.optInt("y");
|
|
648
|
+
e.width = t.optInt("width", 80);
|
|
649
|
+
e.height = t.optInt("height", 60);
|
|
650
|
+
e.layer = t.optInt("layer", 0);
|
|
651
|
+
e.rotation = (float) t.optDouble("rotation", 0.0);
|
|
652
|
+
}
|
|
653
|
+
JSONObject s = comps.optJSONObject("Sprite");
|
|
654
|
+
if (s != null) {
|
|
655
|
+
e.shape = s.optString("shape", "rect");
|
|
656
|
+
e.fill = parseColor(s.optString("fill", "#4C8BF5"));
|
|
657
|
+
e.outline = parseColor(s.optString("outline", "#123456"));
|
|
658
|
+
e.image = s.optString("image", "");
|
|
659
|
+
}
|
|
660
|
+
JSONObject l = comps.optJSONObject("Label");
|
|
661
|
+
if (l != null) {
|
|
662
|
+
e.labelText = l.optString("text", "");
|
|
663
|
+
e.fontSize = l.optInt("font_size", 18);
|
|
664
|
+
e.labelColor = parseColor(l.optString("color", "#000000"));
|
|
665
|
+
e.bold = l.optBoolean("bold", false);
|
|
666
|
+
}
|
|
667
|
+
JSONObject tap = comps.optJSONObject("TapHandler");
|
|
668
|
+
if (tap != null) {
|
|
669
|
+
e.tapAction = tap.optString("action", "none");
|
|
670
|
+
}
|
|
671
|
+
JSONObject tm = comps.optJSONObject("Timer");
|
|
672
|
+
if (tm != null) {
|
|
673
|
+
e.timerAction = tm.optString("action", "none");
|
|
674
|
+
e.intervalMs = tm.optInt("interval_ms", 1000);
|
|
675
|
+
}
|
|
676
|
+
return e;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
void tick(long elapsed, int sceneW, int sceneH) {
|
|
680
|
+
if ((vx != 0 || vy != 0) && elapsed - lastMove >= TICK_MS) {
|
|
681
|
+
lastMove = elapsed;
|
|
682
|
+
x += vx;
|
|
683
|
+
y += vy;
|
|
684
|
+
if (x < 0) {
|
|
685
|
+
x = 0;
|
|
686
|
+
vx = -vx;
|
|
687
|
+
}
|
|
688
|
+
if (x + width > sceneW) {
|
|
689
|
+
x = sceneW - width;
|
|
690
|
+
vx = -vx;
|
|
691
|
+
}
|
|
692
|
+
if (y < 0) {
|
|
693
|
+
y = 0;
|
|
694
|
+
vy = -vy;
|
|
695
|
+
}
|
|
696
|
+
if (y + height > sceneH) {
|
|
697
|
+
y = sceneH - height;
|
|
698
|
+
vy = -vy;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!"none".equals(timerAction) && intervalMs > 0) {
|
|
702
|
+
if (elapsed - lastTick >= intervalMs) {
|
|
703
|
+
lastTick = elapsed;
|
|
704
|
+
runAction(timerAction);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
void runAction(String action) {
|
|
710
|
+
if (action == null) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
action = action.trim();
|
|
714
|
+
if (action.isEmpty() || "none".equals(action)) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if ("move-left".equals(action)) {
|
|
718
|
+
vx = -4; vy = 0;
|
|
719
|
+
} else if ("move-right".equals(action)) {
|
|
720
|
+
vx = 4; vy = 0;
|
|
721
|
+
} else if ("move-up".equals(action)) {
|
|
722
|
+
vx = 0; vy = -4;
|
|
723
|
+
} else if ("move-down".equals(action)) {
|
|
724
|
+
vx = 0; vy = 4;
|
|
725
|
+
} else if ("bounce".equals(action)) {
|
|
726
|
+
if (vx == 0 && vy == 0) {
|
|
727
|
+
vx = 4;
|
|
728
|
+
vy = 4;
|
|
729
|
+
}
|
|
730
|
+
} else if ("stop".equals(action)) {
|
|
731
|
+
vx = 0; vy = 0;
|
|
732
|
+
} else if (action.startsWith("print(")) {
|
|
733
|
+
Log.i(TAG, action);
|
|
734
|
+
}
|
|
735
|
+
// 其余自定义 Python 表达式仅在桌面预览中执行
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
"""
|