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/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""tkunity - 纯标准库的 Unity 风格 2D 游戏编辑器与运行时,可把 .pyu 游戏导出为 Android APK 工程。
|
|
3
|
+
|
|
4
|
+
Apache License 2.0 | Copyright 2026 oiiaio-web <3926580760@qq.com>
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .api import GameObject, Project, load, save
|
|
9
|
+
from .components import BUILTIN_ACTIONS, COMPONENT_SCHEMAS, PARTS
|
|
10
|
+
from .pyu import PYU_VERSION, new_project
|
|
11
|
+
|
|
12
|
+
__version__ = "2026.10.31"
|
|
13
|
+
__author__ = "oiiaio-web"
|
|
14
|
+
__author_email__ = "3926580760@qq.com"
|
|
15
|
+
__license__ = "Apache-2.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__", "__author__", "__author_email__", "__license__",
|
|
19
|
+
"PYU_VERSION", "Project", "GameObject", "load", "save", "new_project",
|
|
20
|
+
"PARTS", "COMPONENT_SCHEMAS", "BUILTIN_ACTIONS", "help",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def help():
|
|
25
|
+
"""打印 tkunity 命令帮助(等价命令行 tkunity -h / -help / --help)。"""
|
|
26
|
+
from .cli import show_help
|
|
27
|
+
show_help()
|
tkunity/__main__.py
ADDED
tkunity/api.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""面向对象的建项目 API(无需打开编辑器即可用代码做游戏)。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from . import pyu
|
|
6
|
+
from .components import COMPONENT_SCHEMAS, new_component
|
|
7
|
+
|
|
8
|
+
__all__ = ["Project", "GameObject", "save", "load"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GameObject(object):
|
|
12
|
+
"""场景中的一个物体。name/components 与 .pyu 实体一一对应。"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name, x=0, y=0, width=80, height=60, layer=0):
|
|
15
|
+
self.name = name
|
|
16
|
+
self._components = {}
|
|
17
|
+
self._components["Transform"] = {
|
|
18
|
+
"x": x, "y": y, "width": width, "height": height,
|
|
19
|
+
"rotation": 0.0, "layer": layer,
|
|
20
|
+
}
|
|
21
|
+
self._id = None # 加入 Project 后赋值
|
|
22
|
+
|
|
23
|
+
# ---- 便捷增删组件 ----
|
|
24
|
+
def add_sprite(self, shape="rect", fill="#4C8BF5", outline="#123456", image=""):
|
|
25
|
+
return self.set_component("Sprite", shape=shape, fill=fill, outline=outline, image=image)
|
|
26
|
+
|
|
27
|
+
def add_label(self, text="文本", font_size=18, color="#000000", bold=False):
|
|
28
|
+
return self.set_component("Label", text=text, font_size=font_size, color=color, bold=bold)
|
|
29
|
+
|
|
30
|
+
def add_tap(self, action="none"):
|
|
31
|
+
return self.set_component("TapHandler", action=action)
|
|
32
|
+
|
|
33
|
+
def add_timer(self, interval_ms=1000, action="none"):
|
|
34
|
+
return self.set_component("Timer", interval_ms=interval_ms, action=action)
|
|
35
|
+
|
|
36
|
+
def set_component(self, name, **values):
|
|
37
|
+
comp = self._components.setdefault(name, new_component(name))
|
|
38
|
+
comp.update(values)
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def remove_component(self, name):
|
|
42
|
+
if name != "Transform" and name in self._components:
|
|
43
|
+
del self._components[name]
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def has(self, name):
|
|
47
|
+
return name in self._components
|
|
48
|
+
|
|
49
|
+
def get(self, name, default=None):
|
|
50
|
+
return self._components.get(name, default)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def transform(self):
|
|
54
|
+
return self._components["Transform"]
|
|
55
|
+
|
|
56
|
+
def move(self, dx, dy):
|
|
57
|
+
t = self.transform
|
|
58
|
+
t["x"] += dx
|
|
59
|
+
t["y"] += dy
|
|
60
|
+
|
|
61
|
+
def __getitem__(self, name):
|
|
62
|
+
return self._components[name]
|
|
63
|
+
|
|
64
|
+
def to_dict(self, entity_id=None):
|
|
65
|
+
return {"id": self._id if entity_id is None else entity_id,
|
|
66
|
+
"name": self.name, "components": self._components}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Project(object):
|
|
70
|
+
"""一个游戏项目,等价于一份 .pyu 文件。"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, name="未命名项目", scene_width=960, scene_height=640, background="#1e1e2e"):
|
|
73
|
+
self._project = pyu.new_project(name, scene_width, scene_height, background)
|
|
74
|
+
self.objects = []
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def meta(self):
|
|
78
|
+
return self._project["meta"]
|
|
79
|
+
|
|
80
|
+
def add(self, obj):
|
|
81
|
+
"""把一个 GameObject 加入场景(可链式)。"""
|
|
82
|
+
if not isinstance(obj, GameObject):
|
|
83
|
+
raise TypeError("Project.add 需要 GameObject")
|
|
84
|
+
eid = max([e["id"] for e in self._project["entities"]], default=0) + 1
|
|
85
|
+
obj._id = eid
|
|
86
|
+
self._project["entities"].append(obj.to_dict(eid))
|
|
87
|
+
self.objects.append(obj)
|
|
88
|
+
return obj
|
|
89
|
+
|
|
90
|
+
def embed_image(self, path):
|
|
91
|
+
"""内嵌本地 PNG/GIF 图片,返回资源名。"""
|
|
92
|
+
return pyu.embed_image(self._project, path)
|
|
93
|
+
|
|
94
|
+
def to_dict(self):
|
|
95
|
+
return self._project
|
|
96
|
+
|
|
97
|
+
def save(self, path):
|
|
98
|
+
pyu.save(self._project, path)
|
|
99
|
+
return path
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def save(project, path):
|
|
103
|
+
"""保存 Project(或直接传字典)到 .pyu 文件。"""
|
|
104
|
+
if isinstance(project, Project):
|
|
105
|
+
project = project.to_dict()
|
|
106
|
+
pyu.save(project, path)
|
|
107
|
+
return path
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load(path):
|
|
111
|
+
"""读取 .pyu 文件,返回项目字典(供运行时 / 编辑器使用)。"""
|
|
112
|
+
return pyu.load(path)
|
tkunity/cli.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""命令行入口。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
tkunity # 启动图形编辑器
|
|
6
|
+
tkunity edit [文件.pyu] # 同上,可带文件
|
|
7
|
+
tkunity build 游戏.pyu -o 输出目录 # 导出 Android APK 工程
|
|
8
|
+
tkunity info # 环境信息
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _print_error(msg):
|
|
20
|
+
sys.stderr.write("[tkunity] 错误: %s\n" % msg)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cmd_edit(args):
|
|
24
|
+
from .editor import launch_editor
|
|
25
|
+
launch_editor(args.file)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cmd_build(args):
|
|
29
|
+
from . import export, pyu
|
|
30
|
+
if not os.path.isfile(args.input):
|
|
31
|
+
_print_error("找不到项目文件: %s" % args.input)
|
|
32
|
+
return 1
|
|
33
|
+
project = pyu.load(args.input)
|
|
34
|
+
outdir = args.output or os.path.join(
|
|
35
|
+
os.path.dirname(os.path.abspath(args.input)),
|
|
36
|
+
"android_build_" + project["meta"].get("name", "game"))
|
|
37
|
+
files = export.build(project, outdir)
|
|
38
|
+
print("[tkunity] 已生成 Android 工程(%d 个文件):" % len(files))
|
|
39
|
+
for f in files:
|
|
40
|
+
print(" ", f)
|
|
41
|
+
print("\n下一步:")
|
|
42
|
+
print(" 1) 安装 JDK 17 与 Android SDK,并设置 ANDROID_HOME")
|
|
43
|
+
print(" (或在该目录创建 local.properties: sdk.dir=你的SDK路径)")
|
|
44
|
+
print(" 2) Windows: 双击 build_apk.bat 或 执行 ./build_apk.sh")
|
|
45
|
+
print(" 3) 产物: app/build/outputs/apk/debug/app-debug.apk")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_info(args):
|
|
50
|
+
import tkinter
|
|
51
|
+
print("tkunity %s" % __version__)
|
|
52
|
+
print("Python: %s" % sys.version.split()[0])
|
|
53
|
+
print("tkinter: %s (Tk %s)" % (tkinter.__file__, tkinter.TkVersion))
|
|
54
|
+
print("平台: %s" % sys.platform)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _add_help_alias(parser):
|
|
58
|
+
"""为 parser 增加 -help 别名(与内置 -h / --help 功能一致)。"""
|
|
59
|
+
parser.add_argument("-help", action="help",
|
|
60
|
+
help="显示帮助并退出(同 -h / --help)")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_parser():
|
|
64
|
+
parser = argparse.ArgumentParser(
|
|
65
|
+
prog="tkunity",
|
|
66
|
+
description="tkunity %s - Unity 风格 2D 游戏编辑器(纯标准库,可导出 Android APK 工程)" % __version__,
|
|
67
|
+
)
|
|
68
|
+
_add_help_alias(parser)
|
|
69
|
+
sub = parser.add_subparsers(dest="command")
|
|
70
|
+
parser.add_argument("--version", action="version",
|
|
71
|
+
version="%(prog)s " + __version__)
|
|
72
|
+
|
|
73
|
+
p_edit = sub.add_parser("edit", help="启动图形编辑器(可带 .pyu 文件)")
|
|
74
|
+
_add_help_alias(p_edit)
|
|
75
|
+
p_edit.add_argument("file", nargs="?", default=None)
|
|
76
|
+
p_edit.set_defaults(func=cmd_edit)
|
|
77
|
+
|
|
78
|
+
p_build = sub.add_parser("build", help="把 .pyu 游戏导出为 Android APK 工程")
|
|
79
|
+
_add_help_alias(p_build)
|
|
80
|
+
p_build.add_argument("input", help="项目文件(.pyu)")
|
|
81
|
+
p_build.add_argument("-o", "--output", default=None, help="输出目录")
|
|
82
|
+
p_build.set_defaults(func=cmd_build)
|
|
83
|
+
|
|
84
|
+
p_info = sub.add_parser("info", help="显示环境信息")
|
|
85
|
+
_add_help_alias(p_info)
|
|
86
|
+
p_info.set_defaults(func=cmd_info)
|
|
87
|
+
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def show_help():
|
|
92
|
+
"""打印主帮助文本(等价命令行 tkunity -h / -help / --help)。"""
|
|
93
|
+
_build_parser().print_help()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv=None):
|
|
97
|
+
parser = _build_parser()
|
|
98
|
+
args = parser.parse_args(argv)
|
|
99
|
+
if getattr(args, "command", None) is None or not hasattr(args, "func"):
|
|
100
|
+
cmd_edit(argparse.Namespace(file=None))
|
|
101
|
+
return 0
|
|
102
|
+
return args.func(args) or 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
sys.exit(main())
|
tkunity/components.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""组件注册表与零件库。
|
|
3
|
+
|
|
4
|
+
每个组件描述一组字段(类型 / 默认值),编辑器据此动态生成属性面板,
|
|
5
|
+
导出器据此生成 Java 渲染器,运行时据此执行行为。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"SHAPES", "COMPONENT_SCHEMAS", "PARTS", "TRANSFORM_FIELDS",
|
|
11
|
+
"BUILTIN_ACTIONS", "new_transform", "new_component", "validate_component",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
# 组件类型
|
|
15
|
+
SHAPES = ["rect", "oval", "image"]
|
|
16
|
+
|
|
17
|
+
# 内置动作(桌面预览与 APK 导出通用;APK 只支持这组动作)
|
|
18
|
+
BUILTIN_ACTIONS = [
|
|
19
|
+
"none",
|
|
20
|
+
"print('hello')",
|
|
21
|
+
"move-left", "move-right", "move-up", "move-down",
|
|
22
|
+
"bounce", "stop",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# 组件 -> 字段 -> 字段定义。type 取值: int / float / text / color / choice / bool / file
|
|
26
|
+
COMPONENT_SCHEMAS = {
|
|
27
|
+
"Transform": {
|
|
28
|
+
"fields": {
|
|
29
|
+
"x": {"type": "int", "default": 0, "hint": "X 坐标(px)"},
|
|
30
|
+
"y": {"type": "int", "default": 0, "hint": "Y 坐标(px)"},
|
|
31
|
+
"width": {"type": "int", "default": 80, "hint": "宽度(px)"},
|
|
32
|
+
"height": {"type": "int", "default": 60, "hint": "高度(px)"},
|
|
33
|
+
"rotation": {"type": "float", "default": 0.0, "hint": "旋转角度(度)"},
|
|
34
|
+
"layer": {"type": "int", "default": 0, "hint": "绘制层级,越大越靠上"},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
"Sprite": {
|
|
38
|
+
"fields": {
|
|
39
|
+
"shape": {"type": "choice", "options": SHAPES, "default": "rect", "hint": "形状"},
|
|
40
|
+
"fill": {"type": "color", "default": "#4C8BF5", "hint": "填充色"},
|
|
41
|
+
"outline": {"type": "color", "default": "#123456", "hint": "描边色"},
|
|
42
|
+
"image": {"type": "file", "default": "", "hint": "PNG/GIF 图片路径(shape=image 时)"},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
"Label": {
|
|
46
|
+
"fields": {
|
|
47
|
+
"text": {"type": "text", "default": "文本", "hint": "显示文字"},
|
|
48
|
+
"font_size": {"type": "int", "default": 18, "hint": "字号"},
|
|
49
|
+
"color": {"type": "color", "default": "#000000", "hint": "文字颜色"},
|
|
50
|
+
"bold": {"type": "bool", "default": False, "hint": "加粗"},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"TapHandler": {
|
|
54
|
+
"fields": {
|
|
55
|
+
"action": {"type": "text", "default": "none", "multiline": True,
|
|
56
|
+
"hint": "点击动作:内置动作或 Python 表达式(仅桌面预览)"},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
"Timer": {
|
|
60
|
+
"fields": {
|
|
61
|
+
"interval_ms": {"type": "int", "default": 1000, "hint": "触发间隔(毫秒)"},
|
|
62
|
+
"action": {"type": "text", "default": "none", "multiline": True,
|
|
63
|
+
"hint": "定时动作:内置动作或 Python 表达式(仅桌面预览)"},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
TRANSFORM_FIELDS = COMPONENT_SCHEMAS["Transform"]["fields"]
|
|
69
|
+
|
|
70
|
+
# 零件库:编辑器左侧列表。parts = (显示名, 附加组件字典)
|
|
71
|
+
PARTS = [
|
|
72
|
+
("矩形", {"Sprite": {"shape": "rect"}}),
|
|
73
|
+
("椭圆", {"Sprite": {"shape": "oval"}}),
|
|
74
|
+
("文本", {"Label": {}}),
|
|
75
|
+
("图片", {"Sprite": {"shape": "image"}}),
|
|
76
|
+
("点击触发器", {"TapHandler": {}}),
|
|
77
|
+
("定时器", {"Timer": {}}),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# 可被用户手动增删的组件(Transform 除外)
|
|
81
|
+
ADDABLE_COMPONENTS = ["Sprite", "Label", "TapHandler", "Timer"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def new_transform(x=0, y=0, width=80, height=60, rotation=0.0, layer=0):
|
|
85
|
+
"""创建一份 Transform 默认字段。"""
|
|
86
|
+
t = {k: v["default"] for k, v in TRANSFORM_FIELDS.items()}
|
|
87
|
+
t.update(x=x, y=y, width=width, height=height, rotation=rotation, layer=layer)
|
|
88
|
+
return t
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def new_component(name):
|
|
92
|
+
"""按注册表创建一份组件默认字段字典。"""
|
|
93
|
+
schema = COMPONENT_SCHEMAS[name]
|
|
94
|
+
return {k: v["default"] for k, v in schema["fields"].items()}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_component(name, values):
|
|
98
|
+
"""按注册表字段类型规范化(int/float 强转),返回 (ok, 修正后字典, 错误信息)。"""
|
|
99
|
+
schema = COMPONENT_SCHEMAS.get(name)
|
|
100
|
+
if schema is None:
|
|
101
|
+
return False, values, "未知组件: %s" % name
|
|
102
|
+
out = {}
|
|
103
|
+
for key, spec in schema["fields"].items():
|
|
104
|
+
raw = values.get(key, spec["default"])
|
|
105
|
+
ftype = spec["type"]
|
|
106
|
+
if ftype == "int":
|
|
107
|
+
try:
|
|
108
|
+
out[key] = int(float(raw))
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
return False, values, "%s.%s 需要整数,得到 %r" % (name, key, raw)
|
|
111
|
+
elif ftype == "float":
|
|
112
|
+
try:
|
|
113
|
+
out[key] = float(raw)
|
|
114
|
+
except (TypeError, ValueError):
|
|
115
|
+
return False, values, "%s.%s 需要数字,得到 %r" % (name, key, raw)
|
|
116
|
+
elif ftype == "bool":
|
|
117
|
+
out[key] = bool(raw)
|
|
118
|
+
elif ftype == "choice":
|
|
119
|
+
if raw not in spec.get("options", []):
|
|
120
|
+
out[key] = spec["default"]
|
|
121
|
+
else:
|
|
122
|
+
out[key] = raw
|
|
123
|
+
else: # text / color / file
|
|
124
|
+
out[key] = str(raw) if raw is not None else spec["default"]
|
|
125
|
+
return True, out, ""
|