钢铁洪流 · Tank Battle:一款纯网页的俯视角坦克射击游戏

还记得童年在小霸王上「打坦克」的日子吗?这次不用模拟器、不装 App,打开网页就能开炮 —— 直接试玩《钢铁洪流 · Tank Battle》。

本文介绍本站自制的 HTML5 单文件游戏《钢铁洪流 / Tank Battle》—— 俯视视角、5 个关卡、4 类敌人与 4 种道具,手机双摇杆与电脑键鼠都能玩,还能留下本地排行榜纪录。

一、游戏是什么

《钢铁洪流(Tank Battle)》是一款运行在浏览器里的俯视角坦克射击游戏。整个游戏就是一个 HTML 文件,不依赖任何后端、不装插件,手机和电脑的浏览器直接打开即玩。

核心循环很经典:驾驶坦克在关卡地形中穿行,消灭场上全部敌人即可过关;每过一关 +500 分,全程共 3 条命,5 座战场全部击溃即通关。

二、操作方式

游戏同时支持键鼠与触屏双摇杆,切换无感:

平台 操作 说明
电脑 WASD / 方向键 移动坦克
电脑 鼠标移动 瞄准
电脑 鼠标左键 / 空格 射击
电脑 P / Esc 或点右上角 ❚❚ 暂停
手机 左半屏摇杆 移动
手机 右半屏摇杆 瞄准(触屏设备默认自动开火)

提示:手机上建议横屏握持,左右摇杆的手感最接近手柄;电脑端则是「鼠标瞄准 + WASD 走位」的经典玩法,点击屏幕或按 Enter 即可开始。

三、5 个关卡

游戏共 5 个关卡,每关地形与敌人配置各不相同,越往后地形越复杂、敌人越硬:

  1. 第 1 关 · 边境平原
  2. 第 2 关 · 双桥要塞
  3. 第 3 关 · 熔岩工厂
  4. 第 4 关 · 寒冰回廊
  5. 第 5 关 · 钢铁堡垒

消灭全部敌人过关,每过一关 +500 分;5 关全清即达成「全部通关」。

四、地形与墙体:三种材质

  • 钢墙:无法摧毁,只能绕行 —— 构成关卡的固定骨架,也是走位的参照。
  • 砖墙:射击 3 下可击穿 —— 既能当掩体,也能自己开出一条路。
  • 水面:阻挡坦克与炮弹通行 —— 天然的护城河,用好了能挡枪,也可能把自己堵死。

五、四种敌人:各有各的打法

  • 快速兵:机动性高,贴脸骚扰,优先点掉。
  • 重甲兵:血厚耐打,需要持续输出,别被它拖住节奏。
  • 狙击手:远程精准点射,逼你不断变换走位。
  • 重炮手:火力猛,正面硬刚容易被压制,建议卡墙体打。

四类敌人的行动模式与血量各不相同,混编出场时得先排好优先级再开火。

六、四种道具:捡到即时生效

击毁敌人会掉落道具,吃到后短时间内生效:

  • 加命:+1 命,残局翻盘的底气。
  • 护盾:6 秒无敌,硬抗一轮弹幕的保命神器。
  • 三向散射:10 秒三向射击,清场效率直接翻倍。
  • 加速:8 秒提速,抢位、拉扯、风筝都更从容。

七、技术亮点

  • 单文件 HTML5:整个游戏就是一个 .html,零依赖、零构建,丢到任意静态空间就能跑。
  • 跨端即开即玩:同一份代码自适应手机与电脑,触屏双摇杆 / 键鼠双输入。
  • 本地排行榜:最高分 TOP 榜保存在浏览器 localStorage,不会上传服务器,关掉网页也不丢。
  • 轻量流畅:纯前端实时渲染,随手打开就能跑,关掉也不占空间。

八、试玩入口

直接打开即可开打:钢铁洪流 · Tank Battle 试玩

游戏免费、即点即玩,适合通勤、摸鱼、等咖啡的碎片时间。欢迎试玩,也欢迎在评论区聊聊手感,或你想加的玩法(比如新关卡、新道具、联机对战)。


本文介绍的游戏由本站开发,试玩与反馈入口见上方链接。

GitHub Copilot 架构升级:支持本地模型,实现云边协同编码

微软于 2026 年 10 月正式宣布 GitHub Copilot 完成架构级升级,将于10 月底全面开放本地 AI 模型推理能力。本次升级是 Copilot 从纯云端服务向「云端 + 本地边缘」混合部署架构的核心演进,基于 Project HydraFusion 编排体系扩展,实现了跨环境的智能任务调度与模型管理。

一、核心架构升级:从多模型编排到云边一体化调度

本次架构升级的核心是将原有的HydraFusion 多模型编排引擎从纯云端扩展到云边双环境,形成统一的跨层调度体系:

  1. 原有基础架构:2026 年 9 月推出的 Project HydraFusion 是一套运行时编排系统,可根据任务复杂度、成本、延迟指标,在多个云端模型之间自动选择「单模型直接求解、级联迭代、交叉评审」三种执行模式,在保持编码质量的同时降低推理成本。
  2. 升级后的扩展能力:调度器新增本地设备算力节点,可依据响应速度、计算成本、生成质量、设备性能、隐私要求等多维指标,自动将推理任务分配到云端或本地端,后台无缝协同两类计算资源,开发者无需感知底层基础设施差异。

二、本地模型的两种运行模式

用户可通过 VS Code 插件、Copilot CLI 命令行工具、独立桌面应用等入口,选择两种本地运行模式:

  • 自动编排模式:系统根据任务特性动态决策部署位置 —— 简单代码补全、语法检查等轻量任务优先走本地;复杂架构重构、跨文件逻辑分析等高阶任务自动调度到云端大模型,兼顾速度与质量。
  • 强制本地模式:全部推理过程完全在设备端完成,数据不经过云端服务器,满足敏感代码开发、离线环境、强合规场景的隐私需求。

三、支持的本地模型与兼容方案

1. 微软官方原生本地模型:MAI Code 1.1 Flash

微软同步推出专为本地编码优化的轻量级大模型,作为 Copilot 本地推理的官方默认选项:

  • 采用混合专家(MoE)架构,总参数量 1370 亿,每轮激活参数约 68 亿
  • 融合量化压缩与推测解码技术,在保留编码质量的同时降低内存占用、提升端到端响应速度
  • 支持最大 256k 上下文窗口,峰值内存占用 75.5GB,可在搭载 NVIDIA RTX AI 算力的 Windows 设备(如 Surface Laptop Ultra)上基于 Windows ML 原生加速运行

2. 第三方本地模型兼容

通过扩展 BYOK(Bring Your Own Key)机制,支持广泛的第三方本地部署模型:

  • 原生支持Ollama作为本地模型提供商,覆盖 VS Code、JetBrains 全系列 IDE、Copilot CLI 等所有终端,可直接选择 Ollama 本地运行的任意模型
  • 兼容所有符合OpenAI Chat Completions API 规范的本地端点,包括 vLLM、Foundry Local 等自部署服务
  • 支持自定义 URL 端点,企业可接入内部私有部署的专属模型,适配定制化开发流程

四、配套安全与能力保障

  • 安全沙箱隔离:微软推出 Microsoft Execution Containers(MXC)执行容器,为本地与云端的代理式编码会话提供隔离环境,精细化控制文件、网络、系统权限与凭证访问,保障本地运行时的系统安全与数据安全
  • 全功能对齐:本地模型完整支持 Copilot 的代码补全、逻辑解释、结构化重构、工具调用、流式输出、跨会话记忆等全部能力,与云端体验保持一致

五、覆盖的产品终端

本次本地模型支持将同步覆盖 GitHub Copilot 全产品线:

  • Visual Studio Code / Visual Studio 2026
  • GitHub Copilot CLI 命令行工具
  • GitHub Copilot 独立桌面应用
  • JetBrains 系列 IDE 插件(已提前上线 Ollama 本地接入能力)

六、升级的核心价值

  1. 隐私合规:敏感代码、内部项目无需上传云端,满足金融、政企等强合规场景的数据安全要求
  2. 离线可用:无网络环境下仍可使用完整的智能编码辅助能力,适配离线开发、内网开发场景
  3. 成本优化:高频轻量任务由本地算力承接,显著降低云端 Token 消耗与订阅成本
  4. 低延迟体验:本地代码补全响应不受网络波动影响,交互更流畅
  5. 灵活定制:开发者可自由选择模型与部署方式,适配不同技术栈与业务流程

星际歼击 Star Strike:一款纯网页的竖屏太空射击游戏

忙碌间隙想来一局爽快的射击?不用下载、不装 App,打开网页就能打。
本文介绍本站自制的 HTML5 游戏《星际歼击 / Star Strike》——竖屏适配、手机手指拖动与电脑键盘都能玩,含多向弹幕、强化道具、每 5 波一场三形态首领战,还有攒满能量一键清屏的终结技。


一、这是什么游戏

《星际歼击(Star Strike)》是一款运行在浏览器里的竖屏太空射击游戏。不依赖任何后端、不装插件,手机和电脑的浏览器直接打开即玩。

核心循环很经典:操控飞船在底部灵活走位,按住持续开火,从弹幕中突围,一波波清掉敌机;每 5 波遭遇一次首领,活得越久、打得越准,分数越高。

二、操作方式

游戏同时支持触屏和键盘,切换无感:

平台 操作 说明
手机 手指拖动屏幕任意位置 飞船跟随手指移动(竖屏推荐)
手机 按住屏幕 持续开火,松开停火
手机 点击终结技按钮 能量满时释放(竖屏在右下角)
电脑 WASD / 方向键 移动飞船
电脑 空格 开火
电脑 ESC / P 暂停

提示:手机上「拖动即移动、按住即开火」手感很顺,单手就能玩;接外接键盘则是标准街机操作。

三、四种强化道具

击落敌人或清波会掉落道具,捡到后短时间生效:

  • M — 多重射击:从单发变为五向扇形弹幕,清场效率直接翻倍。
  • S — 能量护盾:短时间里免疫伤害,硬抗弹幕的保命神器。
  • B — 引擎加速:提升飞船机动性,更容易穿弹幕、抢走位。
  • H — 护甲修复:立即恢复生命值,残血翻盘的资本。

四、首领战:三形态进化

每 5 波 一场首领战,而且首领不是一刀死的木桩——它会三形态进化:

  1. 扇形弹幕:开局铺开大范围扇形,逼你走位。
  2. 螺旋弹幕:弹道变成旋转螺旋,需要贴着缝隙钻。
  3. 狂暴召唤:进入狂暴状态,一边召小兵一边压制。

这套递进让中后段的每一场首领都像一个小 BOSS 关,策略也从「躲」逐步变成「躲 + 抢输出窗口」。

五、终结技:满能量一键清屏

命中敌人会积攒能量条,攒满后按 J(手机点右下角按钮)释放终结技:

  • 时停减速:全场敌人/弹幕大幅减速,给你喘息空间;
  • 贯穿激光:一道贯穿激光持续输出;
  • 清弹:瞬间清空当前屏幕上的弹幕。

它本质上是给玩家一个「被弹幕压死前翻盘」的保险,也是冲高分的爆发窗口。

六、技术亮点

  • 跨端即开即玩:同一份代码自适应手机与电脑,触屏/键盘双输入。
  • 轻量流畅:纯前端实时渲染,随手打开就能跑,关掉也不占空间。
  • 数据本地留存:最高分、到达波次等个人记录存在浏览器本地,关掉网页也不丢。

七、试玩入口

直接打开即可开打:星际歼击 Star Strike 试玩

游戏免费、即点即玩,适合通勤、摸鱼、等咖啡的碎片时间。欢迎试玩,也欢迎在评论区聊聊手感,或你想加的玩法(比如新道具、排行榜、Boss 机制)。


本文介绍的游戏由本站开发,试玩与反馈入口见上方链接。

Godot 4.6 + Android 真机跑通 AdMob 广告(横幅 / 插页 / 奖励)

这份文档里的每一步、每一个坑,都是在真机上一遍遍试出来的,不是照抄文档写的。
实测机型:Redmi 23013RK75C(Android 13 / Adreno 730),Godot 4.6.stable,
插件:Poing Studios godot-admob v4.0.1。

为什么值得专门写一份:这个插件在 Godot 4.6 上有 三个必须手动打补丁 的地方,
不打的话要么 403、要么 JNI 报错、要么横幅完全静默失败(最难查,本文重点)。


一、先说结论:要打三个补丁

# 症状 根因 补丁位置
1 广告请求返回 403,拿不到广告 插件写死 play-services-ads:24.9.0,该版本已被 Google 停服 addons/admob/android/bin/ads/poing_godot_admob_ads.gd
2 报错 Array[String] does not have the same element type Godot 4.6-stable 引擎 bug:java_class_wrapper.cpp:202 把 && 写成 \|\|,任何 typed Array 传 String[] 都失败 src/api/core/AdRequest.gd + 4 处调用点
3 横幅既不成功也不失败,SDK 一个网络包都不发 GDScript int 传给 Java 时被装箱成 Long,插件 aar 里是 checkcast Integer,抛异常后被 JNI 静默吞掉 src/api/AdView.gd

补丁 1、2 相对好查(有明确报错)。补丁 3 才是真正卡人的地方,下面第五节详细讲。


二、环境清单

项目 版本 / 说明
Godot 4.6.stable(用 _console.exe 版本,命令行能看到完整输出)
AdMob 插件 Poing Studios godot-admob v4.0.1
play-services-ads 25.5.0(跟随 Maven latest,不能用插件默认的 24.9.0)
渲染器 Mobile(Vulkan)
Android 构建模板 必须安装(Gradle 构建,约 200MB)
adb Android SDK platform-tools
测试机 开启 USB 调试,屏幕保持解锁

三、第 1 步:建项目

3.1 路径必须全 ASCII

✅ C:\Users\xxx\Documents\AdMobPortrait
❌ C:\Users\xxx\Documents\实验广告

Godot 导出到中文路径会失败,这是硬限制。

3.2 project.godot 的关键项

config/name="AdMobPortrait"
run/main_scene="res://Main.tscn"
config/features=PackedStringArray("4.6", "Mobile")
config/icon="res://icon.svg"          ; 不写会报 "No project icon specified"

[autoload]
AdManager="*res://AdManager.gd"

[display]
window/size/viewport_width=720
window/size/viewport_height=1280
window/handheld/orientation=1          ; 1 = 竖屏
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"

[rendering]
renderer/rendering_method="mobile"
renderer/rendering_method.mobile="mobile"
textures/vram_compression/import_etc2_astc=true   ; ★ 缺了导出会失败

⚠ 坑:导出报 due to configuration errors: 但消息是空的

ERROR: Cannot export project with preset "portrait" due to configuration errors:
(后面什么都没有)

根因在引擎 platform/android/export/export_plugin.cpp:3047,
has_valid_project_configuration() 里有一段不写任何错误信息就直接置 false 的判定:

if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
    valid = false;      // ← 没有 r_error,所以错误消息为空
}

解决:补上 textures/vram_compression/import_etc2_astc=true。


四、第 2 步:装插件

4.1 放插件并启用

把 addons/admob 整个目录放进项目,然后 project.godot 加:

[editor_plugins]
enabled=PackedStringArray("res://addons/admob/plugin.cfg")

4.2 ★ 必须生成一次全局类名缓存

新建项目第一次打开时,脚本里的 AdView / MobileAds / AdRequest 等全局类名
全部报 “not declared”,因为 .godot/global_script_class_cache.cfg 还没生成。

godot --headless --editor --quit

跑一次(会自动生成缓存然后退出),再打开编辑器/导出就正常了。

4.3 安装 Android 构建模板

编辑器菜单:项目 → 安装 Android 构建模板,会生成 android/build/(约 200MB)。

如果从别的项目拷现成的 android/ 目录,要剔除 build/build、.gradle,
但 assetPackInstallTime 是模板自带的,别删。


五、第 3 步:三个必打的补丁 ★核心章节

补丁 1 —— play-services-ads 升到 25.5.0(解决 403)

文件:addons/admob/android/bin/ads/poing_godot_admob_ads.gd

# 原值 24.9.0 已被 Google 停服,服务端直接返回 403
var _dependency_library := ["com.google.android.gms:play-services-ads:25.5.0"]

版本号别写死,去
https://dl.google.com/dl/android/maven2/com/google/android/gms/play-services-ads/maven-metadata.xml
看 <latest> 是多少,跟着走。

补丁 2 —— typed Array 换 PackedStringArray(解决 JNI 报错)

根因:Godot 4.6-stable 的 platform/android/java_class_wrapper.cpp:202:

if (arr.is_typed() && (builtin != STRING || builtin != STRING_NAME))
//                                      ^^ 应该是 &&

|| 导致这个条件恒为真,于是任何 typed Array 传 String[] 都被拒。
(master / 4.7-dev 已修正为 &&。)

改法 —— 在 src/api/core/AdRequest.gd 加一个转换函数:

func jni_keywords() -> PackedStringArray:
    return PackedStringArray(keywords)

然后把 4 处调用点 的第 3 个参数改掉:

文件 行 改法
src/api/AdView.gd 89 ... ad_request.jni_keywords())
src/api/InterstitialAdLoader.gd 44 ad_request.jni_keywords()
src/api/RewardedAdLoader.gd 44 ad_request.jni_keywords()
src/api/RewardedInterstitialAdLoader.gd 44 ad_request.jni_keywords()

另外 src/api/MobileAds.gd:42 也要改:

_plugin.set_request_configuration(
    request_configuration.convert_to_dictionary(),
    PackedStringArray(request_configuration.test_device_ids))   # ← 加 PackedStringArray

补丁 3 —— Integer 装箱(解决横幅完全静默失败)★最难查

症状

插页、奖励都正常,只有横幅:发出加载请求之后,
既没有 on_ad_loaded,也没有 on_ad_failed_to_load,SDK 一个包都不发。
日志里干干净净,什么都不报错。

排查三步法(这套手法可以复用到别的插件问题)

第 1 步:对照插件日志找断点

adb logcat -d -v brief | grep -i "poing-godot-admob\|godot   "

正常应该看到 safeInsetRect 之后紧跟着:

D/poing-godot-admob: Safe Area of screen: Rect(...).
D/poing-godot-admob: marginTop: 0

如果 Safe Area of screen: 从没出现,说明 Banner 构造中途就断了。

第 2 步:看视图层级,确认 AdView 到底建出来没有

adb shell dumpsys activity top

往下找 godot_fragment_container,正常应该能看到
com.google.android.gms.ads.AdView。看不到就说明 addView 那段没执行。

第 3 步:反编译 aar 看字节码

unzip -o addons/admob/android/bin/ads/libs/poing-godot-admob-ads-release.aar
cd <解压目录> && unzip -o classes.jar
javap -p -c com/poingstudios/godot/admob/ads/adformats/Banner.class

在 Banner.<init> 的字节码里会看到:

63: checkcast  java/lang/Integer     // ad_position

convertToAdSize 里也有两处(width / height)。

根因

GDScript int  ──写进 Dictionary──▶  Godot 装箱成 java.lang.Long
                                            │
                                   Java 侧 checkcast java/lang/Integer
                                            │
                                   ClassCastException
                                            │
                                   被 JNI 层静默吞掉 → Banner 构造中断
                                            │
                              runOnUiThread{ 创建 AdView + addView } 永不执行
                                            │
                                   没有 AdView → 零网络请求 → 零回调

Godot 的 int 是 int64,装箱成 Long;而插件 aar 里的 Kotlin 是 this["ad_position"] as Int。
这个异常发生在 safeArea 属性初始化之后、runOnUiThread 之前,
所以 Banner 没被加进 banners 列表,后续 load_ad(uid) 取 banners[uid] 越界 —— 整条链静默。

注意:GitHub master 上的 DictionaryExtensions.kt 已经改成安全的
(this[key] as? Number)?.toInt(),但发布的 aar 4.0.1 仍是旧代码。
这就是为什么”看源码明明修过了,跑起来还是崩”。

修复代码

文件:addons/admob/src/api/AdView.gd

# Godot 的 JavaClass 构造器方法名 = 类的简单名
# (java_class_wrapper.cpp:1488: java_constructor_name = 类名最后一段)
# 所以是 Integer.Integer(v),走 env->NewObjectA(..., "<init>", ...),
# 返回 JavaObject;JavaObject 进 Dictionary 时引擎直接透传 jobject。
static var _integer_class = null
static var _integer_class_ready := false

static func _java_int(value: int):
    if not _integer_class_ready:
        _integer_class_ready = true
        if OS.get_name() == "Android":
            _integer_class = JavaClassWrapper.wrap("java.lang.Integer")
    if _integer_class == null:
        return value
    return _integer_class.Integer(value)

然后 _init() 里三个 int 全部走它:

var ad_view_dictionary := {
    "ad_unit_id": ad_unit_id,
    "ad_position": _java_int(ad_position),
    "ad_size": {
        "width": _java_int(ad_size.width),
        "height": _java_int(ad_size.height)
    }
}

为什么插页/奖励不受影响

它们的 AdRequest.convert_to_dictionary() 里只有 String 和空 Dictionary,
没有 int,碰不到这个 checkcast。这也解释了”为什么偏偏只有横幅坏”。


六、第 4 步:业务代码

6.1 官方测试广告单元 ID(任何包名下都能出测试广告)

const BANNER_ID       := "ca-app-pub-3940256099942544/6300978111"
const INTERSTITIAL_ID := "ca-app-pub-3940256099942544/1033173712"
const REWARDED_ID     := "ca-app-pub-3940256099942544/5224354917"

6.2 注册测试设备

真机上跑测试广告必须把设备注册成测试设备,否则拿不到测试广告。
设备哈希从 logcat 里 SDK 自己打印的那句取:

Use RequestConfiguration.Builder().setTestDeviceIds(Arrays.asList("599DD0E66111F3FAC24560DEABA32062"))
adb logcat -d | grep -i "setTestDeviceIds"

6.3 AdManager.gd(AutoLoad)

func _ready() -> void:
    if OS.get_name() != "Android" and OS.get_name() != "iOS":
        return

    # 1) 注册测试设备
    var cfg := RequestConfiguration.new()
    cfg.test_device_ids = [TEST_DEVICE_ID]
    MobileAds.set_request_configuration(cfg)

    # 2) 初始化(异步)
    var listener := OnInitializationCompleteListener.new()
    listener.on_initialization_complete = func(_status: InitializationStatus) -> void:
        _initialized = true
    MobileAds.initialize(listener)

横幅(贴在竖屏底部):

func load_banner() -> void:
    if _banner_view != null:
        _banner_view.destroy()
        _banner_view = null

    var ad_size := AdSize.get_current_orientation_anchored_adaptive_banner_ad_size(AdSize.FULL_WIDTH)
    var view := AdView.new(BANNER_ID, ad_size, AdPosition.Values.BOTTOM)

    view.ad_listener.on_ad_loaded = func() -> void:
        view.show()                      # 加载成功才 show
    view.ad_listener.on_ad_failed_to_load = func(err: LoadAdError) -> void:
        print("横幅失败 code=%d %s" % [err.code, err.message])

    _banner_view = view
    view.load_ad(AdRequest.new())

插页:

func load_interstitial() -> void:
    var cb := InterstitialAdLoadCallback.new()
    cb.on_ad_loaded = func(ad: InterstitialAd) -> void:
        _interstitial = ad
        ad.full_screen_content_callback.on_ad_dismissed_full_screen_content = func() -> void:
            _interstitial.destroy()
            _interstitial = null
            load_interstitial()          # 关闭后自动预加载下一则
    cb.on_ad_failed_to_load = func(err: LoadAdError) -> void:
        print("插页失败 code=%d %s" % [err.code, err.message])
    InterstitialAdLoader.new().load(INTERSTITIAL_ID, AdRequest.new(), cb)

奖励:

func show_rewarded() -> void:
    if _rewarded == null:
        load_rewarded()
        return
    var listener := OnUserEarnedRewardListener.new()
    listener.on_user_earned_reward = func(item: RewardedItem) -> void:
        print("发放奖励 type=%s amount=%d" % [item.type, item.amount])
    _rewarded.show(listener)

6.4 界面布局要点

横幅是 Android 原生 View,叠在 Godot 画面上方,会盖住底部内容。
所以 UI 底部要留白:

root.add_theme_constant_override("margin_bottom", 140)   # 给横幅留位

6.5 时序

MobileAds.initialize 是异步的,别在 _ready() 里立刻发请求:

await get_tree().create_timer(2.5).timeout
AdManager.load_interstitial()
AdManager.load_rewarded()
await get_tree().create_timer(3.0).timeout
AdManager.load_banner()

七、第 5 步:导出 APK

7.1 export_presets.cfg 最小配置

[preset.0]
name="portrait"
platform="Android"
export_path="./AdMobPortrait.apk"

[preset.0.options]
gradle_build/use_gradle_build=true
gradle_build/export_format=0
architectures/arm64-v8a=true
package/unique_name="com.example.admobportrait"
package/signed=true
version/code=1
version/name="1.0"
screen/immersive_mode=true

7.2 ★ 每次导出前先清 *.tmp

中断导出会留下
android/build/res/values-*/godot_project_name_string.xmlNNNN.tmp,
下次构建直接失败:

mergeStandardDebugResources FAILED - The file name must end with .xml
find android/build/res -name "*.tmp" -delete

7.3 导出命令

godot --headless --export-debug "portrait" "C:/path/to/AdMobPortrait.apk"

首次 Gradle 构建约 7~15 分钟(之后有缓存会快很多)。
APK 出现在 build/ 下时就已构建完成,Godot 进程可能还在做收尾。


八、第 6 步:装到手机

⚠ 坑:INSTALL_FAILED_USER_RESTRICTED: Install canceled by user

adb install -r -t app.apk
# → INSTALL_FAILED_USER_RESTRICTED: Install canceled by user

试过 keyevent 224 唤醒、wm dismiss-keyguard、pm install、文件管理器安装,都无效。
真正的原因是 AppOps 权限被关:

adb shell appops set com.android.shell REQUEST_INSTALL_PACKAGES allow
adb install -r -t app.apk
# → Success

在 Git Bash 下记得 export MSYS_NO_PATHCONV=1,否则 /data/local/tmp/...
这类路径会被自动转换。


九、第 7 步:验证广告真的出来了

9.1 抓日志

adb logcat -c                                        # 先清空
adb shell monkey -p com.example.admobportrait -c android.intent.category.LAUNCHER 1
sleep 14
adb logcat -d -v brief | grep -i "poing-godot-admob\|godot   "

9.2 健康日志长这样

[07:23:02] 正在初始化 MobileAds ...
[07:23:03] MobileAds 初始化完成 ✔
[07:23:06] 插页:已发起加载请求 ...
[07:23:06] 奖励:已发起加载请求 ...
[07:23:09] 横幅尺寸:411 x 64 dp
[07:23:09] 横幅:java.lang.Integer 装箱 ✔
D/poing-godot-admob: Safe Area of screen: Rect(0, 104 - 0, 0).
D/Ads: AdRequestServiceImpl: Sending request: .../mads/gma     ← 真的发出请求了
[07:23:11] 横幅:加载成功 ✔ → 自动展示
[07:23:11] 横幅:已展示(impression)
[07:23:11] 插页:加载成功 ✔ 可以展示了
[07:23:11] 奖励:加载成功 ✔ 可以展示了

9.3 自动点按钮

Godot 的按钮是自绘的,uiautomator 定位不到,只能用坐标点:

adb shell wm size            # 确认分辨率,例如 1080x2400
adb shell input tap 540 690  # 点"显示插页广告"

9.4 截图确认

adb exec-out screencap -p > screen.png

底部能看到 Google 测试横幅(「测试广告 / Nice job! This is a 320×50 test ad.」)即为成功。


十、排错速查表

现象 根因 解决
导出报 due to configuration errors: 消息为空 import_etc2_astc 未开 project.godot 加 textures/vram_compression/import_etc2_astc=true
脚本报 AdView / MobileAds not declared 全局类名缓存未生成 跑一次 godot --headless --editor --quit
No project icon specified 缺图标 加 config/icon="res://icon.svg"
mergeStandardDebugResources FAILED ... must end with .xml 上次导出中断留了 *.tmp find android/build/res -name "*.tmp" -delete
INSTALL_FAILED_USER_RESTRICTED AppOps 权限被关 adb shell appops set com.android.shell REQUEST_INSTALL_PACKAGES allow
广告请求 403 play-services-ads 版本停服 补丁 1,升到 Maven latest
Array[String] does not have the same element type Godot 4.6 typed-array bug 补丁 2,改 PackedStringArray
横幅无任何回调、SDK 不发请求 int 装箱成 Long,checkcast Integer 崩溃 补丁 3,_java_int()
插页/奖励一直”未准备好” 没等初始化完成就发请求 初始化回调后再等 2~3 秒
横幅盖住 UI 底部 原生 View 叠在画面上 UI 底部留 140px 空白

十一、换成自己的广告 ID 时要做什么

  1. AdMob 后台创建应用 + 三个广告单元,拿到真实 ID,替换 3 个常量。
  2. 把测试设备注册那段删掉或改成只在 debug 生效,否则线上用户拿不到真实广告。
  3. 改包名 package/unique_name 为你自己的域名反写。
  4. 发布前换成 release 导出(--export-release)并配置正式签名。
  5. 欧盟/英国用户需要接 UMP consent 流程(插件里 PoingGodotAdMobUserMessagingPlatform 已提供)。

附:本项目文件清单

AdMobPortrait/
├── project.godot              竖屏 + AutoLoad + import_etc2_astc
├── export_presets.cfg         preset 名 "portrait"
├── AdManager.gd               广告管理器(AutoLoad)
├── Main.gd / Main.tscn        竖屏演示 UI(纯代码搭建)
├── icon.svg
├── addons/admob/              插件(含 3 处补丁)
│   ├── src/api/AdView.gd                      ← 补丁 3
│   ├── src/api/core/AdRequest.gd              ← 补丁 2
│   ├── src/api/MobileAds.gd                   ← 补丁 2
│   ├── src/api/{Interstitial,Rewarded,RewardedInterstitial}AdLoader.gd  ← 补丁 2
│   └── android/bin/ads/poing_godot_admob_ads.gd   ← 补丁 1
├── android/                   Android 构建模板
└── build/
    ├── admob-portrait.apk     已验证可安装的 debug 包
    └── screen_*.png           真机验证截图

2025年流行的Python部署

传统的真机Python+pip部署方式早已被淘汰了。

而venv虚拟部署的方式还有很多人在用,也确实比传统真机部署好很多,但也再被渐渐淘汰。

目前较为流行的一种模式是:Conda+UV的组合方式。

conda:核心是环境管理工具,负责创建隔离的虚拟环境(可包含不同 Python 版本、系统级依赖甚至非 Python 包,如 C 库)。

uv:核心是Python 包管理器,专注于快速安装 / 管理 Python 包(类似 pip,但速度更快、依赖解析更高效),本身不管理环境,而是依赖于当前激活的 Python 环境。

建议在激活 conda 环境后安装 uv,避免全局安装导致的环境混淆。

pip install uv

uv的使用例子:

uv pip install transformers==4.24.0  # 安装指定版本
uv pip uninstall jieba  # 卸载包
uv sync  # 同步依赖(根据 pyproject.toml 或 requirements.txt)

注意事项(避免潜在问题):

1、不混用 conda 和 uv 安装同一 Python 包

例如,不要用 conda install pandas 后再用 uv pip install pandas,可能导致版本冲突(conda 和 pip/uv 管理的包路径虽然一致,但依赖解析逻辑不同)。

  • 优先用 uv 管理纯 Python 包(如 transformers、jieba);
  • 用 conda 管理含系统依赖的包(如 numpy、opencv,conda 会自动安装对应的 C 库)。

2、uv 不处理非 Python 依赖

uv 仅负责 Python 包,若包依赖系统级库(如 libssl、libffi),仍需通过 conda 或系统包管理器(apt)安装:

# 例如安装需要系统库的包,先用 conda 安装依赖 conda install -c conda-forge libsndfile # 安装音频处理的系统库 uv pip install soundfile # 再用 uv 安装依赖该库的 Python 包

3、环境迁移时需同步两者的配置

若要复制 conda 环境,除了用 conda env export,还需同步 uv/pip 管理的包(通过 uv pip freeze > requirements.txt 导出)。

总结

uv 和 conda 是互补工具:conda 负责环境隔离和系统级依赖,uv 负责高效管理 Python 包,在 conda 环境中使用 uv 不仅安全,还能显著提升包安装速度,推荐结合使用。

国内使用建议:

/.config/uv/uv.toml
index-url = “https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple”

另外,uv也是可以直接运行py文件的。如图所示。

用树莓派Pico和镍铬丝制作电烙铁

1. 硬件准备

  • 树莓派Pico: 作为控制核心。
  • 镍铬丝: 作为加热元件。
  • MOSFET(如IRF520): 用于控制镍铬丝的电流。
  • 电阻(如220Ω): 用于限制Pico GPIO引脚的电流。
  • 电源: 提供足够的电流和电压。
  • 导线: 耐高温导线。
  • 烙铁头(可选): 可以将铜丝弯曲成烙铁头形状。
  • 隔热材料: 用于包裹镍铬丝,防止烫伤。

2. 硬件连接

  • 将镍铬丝连接到MOSFET的漏极(Drain)。
  • 将MOSFET的源极(Source)连接到GND。
  • 将树莓派Pico的GPIO引脚(例如GPIO15)通过电阻连接到MOSFET的栅极(Gate)。
  • 连接电源到镍铬丝和MOSFET。
  • 将隔热材料包裹在镍铬丝周围。

3. MicroPython编程

  • Thonny IDE: 推荐使用Thonny IDE,方便编写和上传MicroPython代码。
  • GPIO控制: 使用MicroPython的machine库,控制GPIO引脚输出高低电平,控制MOSFET的导通和截止。

示例代码:

from machine import Pin
import time

fire_pin = Pin(15, Pin.OUT)  # 点火引脚,连接到MOSFET栅极

def fire():
    fire_pin.value(1)  # 开启加热
    print("Heating...")

def stop_fire():
    fire_pin.value(0) # 关闭加热
    print("Stop Heating.")

while True:
    input_value = input("Enter '1' to heat, '0' to stop: ")
    if input_value == '1':
        fire()
    elif input_value == '0':
        stop_fire()
    else:
        print("Invalid input.")

4. 安全注意事项

  • 高温风险: 镍铬丝工作时会产生高温,务必小心操作,防止烫伤。
  • 电路安全: 大电流电路存在风险,务必小心操作。
  • 防火措施: 准备灭火器材,防止火灾发生。
  • 测试验证: 在实际焊接前,进行充分的测试验证。
  • 警告:
    • 电烙铁具有一定的危险性,请在安全的环境下操作。
    • 制作和使用电烙铁时,务必遵守相关安全规范。
    • 如果对于电子电路不是十分了解,请勿自行制作。请寻找专业人员的帮助。

5. 补充说明

  • 根据镍铬丝的电阻和电源电压,选择合适的MOSFET。
  • 可以添加温度传感器和PID控制算法,实现温度控制功能。
  • 关于烙铁头的制作:
    • 可以找一段粗的铜丝,然后用锤子敲打铜丝的一端,敲打成扁平状,然后打磨成自己需要的烙铁头形状。
    • 铜具有非常优秀的导热性,所以铜是制作烙铁头的优秀材料。

江浙沪猫窝设计概要

最近弄了一个室外的猫窝,理解了猫科动物对于卧室的要求。

猫的特点:敏感、警惕性高、爱干净、不喜潮湿、不喜光线直射、怕吵、怕冻、喜欢纸盒。

江浙沪特点:多雨潮湿、夏热冬冷、蛇虫鼠蚁多。

对应猫窝要点:结构简单牢固、防雨防晒、排水良好、透气性好、隐蔽性好、安全性高。

于是,我找了个餐厅用的304不锈钢大桶作为基础。灵感来自集装箱做的房子。

第一个版本的猫窝,没有特别布置,只在里面加上硬纸板。结果位置较低,下雨后里面进水,猫弹射跑路。(失败)

第二个版本,将大桶下方垫上两砖的高度,门口加矮墙。结果只能防小雨,雨一大,还是进水。猫坚持了五分钟,雨一大马上弹射跑路。(失败)

第三个版本,在桶内加垫料,增加床(大纸盒)的高度,床下留出排水通道。同时增加纵深,免得离门口太近。纸盒底部用透明塑料带加一隔水层。最后外面再挂一块布做门帘,遮阳加挡雨。(成功)

终于,大雨倾盆也不怕了。外面暴雨,它在里面踏实躺着梳理毛发。

Json文件的加解密

HTTPS可以防止传输过程中数据被偷看,但无法阻止客户端本地的攻击。比如Json之类的文件,如果完全明文,可能会让一些黑客看到敏感信息。

是否可以对其进行一层简单的加密?

以Python为例子,用一个加密库就可以。

首先执行:pip install cryptography

然后开始编写如下代码。

from cryptography.fernet import Fernet
import json

# 生成密钥
key = Fernet.generate_key()
cipher_suite = Fernet(key)

# JSON 数据
data = {
    "name": "Alice",
    "age": 30,
    "city": "Wonderland"
}

# 将 JSON 数据转换为字符串
json_data = json.dumps(data)

# 加密 JSON 数据
encrypted_data = cipher_suite.encrypt(json_data.encode())

print("加密后的数据:", encrypted_data)
print("密钥:", key)

它会输出:

加密后的数据: b’gAAAAABmraTSlRWANyVFWRKm4CgjpoJEhyLID-ri64K3RP2yItbM-4POPid1dVh_1mWVItoSzZy-bPC_pObCCDUJ-ktuxWbi5zAcSn8VDrTiYiyqwXWvygLcBHmwToapDEDoqA6RjHQs_WeOCKAZqPnge5Q-J4xNpw==’
密钥: b’MvHuTCwAj3SNz93MZtI_1Ww_tHn9XVUFxyARGyGLTxQ=’

要解密的时候怎么办呢?

from cryptography.fernet import Fernet
import json
daa = b'gAAAAABmraTSlRWANyVFWRKm4CgjpoJEhyLID-ri64K3RP2yItbM-4POPid1dVh_1mWVItoSzZy-bPC_pObCCDUJ-ktuxWbi5zAcSn8VDrTiYiyqwXWvygLcBHmwToapDEDoqA6RjHQs_WeOCKAZqPnge5Q-J4xNpw=='
key = b'MvHuTCwAj3SNz93MZtI_1Ww_tHn9XVUFxyARGyGLTxQ='
decrypted_data = cipher_suite.decrypt(daa).decode()
json_data = json.loads(decrypted_data)
print(json_data)

它会输出:
{‘name’: ‘Alice’, ‘age’: 30, ‘city’: ‘Wonderland’}

以上代码使用了 Fernet 加密算法,这是对称加密的一种实现。Fernet 是一种基于 AES(高级加密标准) 的加密方案,具体来说,它使用 AES-128 在 CBC(密码分组链接)模式 下进行加密,并结合 PKCS7 填充 和 HMAC(基于哈希的消息认证码) 来确保数据的完整性和保密性。

树莓派AI 套件上市,带NPU售价 70 美元

如果你曾经想在 Raspberry Pi 5 上尝试神经网络、人工智能和机器学习的世界,树莓派这次推出了官方AI 套件。该 AI 套件是与 Hailo 合作开发的,它可将本地高效的推理能力,集成到各种应用程序中。现已上架 Raspberry Pi 的经销商网络,价格 70 美元。

Raspberry Pi AI 套件拆解

Raspberry Pi AI 套件包括我们的 M.2 HAT+ 和 Hailo-8L AI 加速器模块。AI 套件安装在 Raspberry Pi 5 上,可让你快速构建复杂的 AI 视觉应用程序,实时运行。具有低延迟和低功耗要求。用于对象检测、语义和实例分割、姿态估计和面部识别(仅举几例)的先进神经网络完全在 Hailo-8L 协处理器上运行,使 Raspberry Pi 5 CPU 可以自由地执行其它任务。

Raspberry Pi AI 套件的主要功能包括:

  • 每秒 13 万亿次运算 (TOPS) 的推理性能
  • 以 8Gbps 运行的单通道 PCIe 3.0 连接
  • 与 Raspberry Pi 图像软件子系统完全集成
  • 与第一方或第三方相机的兼容性
  • 加速器硬件的高效调度:在单个摄像头上运行多个神经网络,或者同时在两个摄像头上运行单个 / 多个神经网络

搭载AI Kit的树莓派5

Hailo创建了一个庞大的模型库,用户可以在这里找到各种各样的预训练神经网络模型,可以在AI Kit上部署和优化。

地址:

https://github.com/hailo-ai/hailo_model_zoo/tree/master/docs/public_models/HAILO8L

软件方面

最新版本的 Raspberry Pi OS 会自动检测 Hailo 模块,因此操作系统和利用该模块的应用程序可以立即使用它。rpicam-apps 套件现在有一个后处理模板,用于在摄像头管道中集成实时运行的神经网络推理。

“通过使用预安装的 Hailo Tappas 后处理库,仅几百行 C++ 代码就能创建基于 AI 的高级应用程序。类似级别的集成到我们的 Picamera2 框架中很快就会实现。”

除了可以在 rpicam-apps 或 Picamera2 中使用 Hailo-8L 协处理器外。Raspberry Pi AI Kit 中还打包了集成在 GStreamer 框架和本地 Python 或 C/C++ 应用程序中的 API。包括非摄像机用例,例如在预先录制的视频文件上运行推理。

近看Hailo模块

软件安装步骤非常简单。通过apt安装几个软件包,重新启动,你就可以在几分钟内尝试我们的一些AI演示。完整的说明可以在我们的入门指南中找到。下面是我们的一些演示的预览,你可以通过rpicam-apps运行。

详细步骤:

https://www.raspberrypi.com/documentation/accessories/ai-kit.html#getting-started

有了树莓派AI Kit,你不但可以在picam-apps或Picamera2中使用Hailo-8L协处理器。树莓派还打包了一个集成在GStreamer框架和本地Python或C/C++应用程序中的API。这包括非相机用例,例如在预先录制的视频文件上运行推理。