Android: ландшафт, HUD над системными панелями, прыжок по тапу
- MainActivity фиксирует ландшафт (SDL перебивал манифест FULL_USER) и передаёт путь к ассетам через getExternalFilesDir; - короткий тап по JUMP не теряется между кадрами (jump_pending); - HUD сдвинут внутрь от статус-бара и нижней панели; - логгер на Android собирается без неиспользуемой ветки.
This commit is contained in:
parent
e07f39d8dc
commit
8d572b8190
9 changed files with 180 additions and 23 deletions
|
|
@ -31,9 +31,9 @@
|
|||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||
|
||||
<activity
|
||||
android:name="org.libsdl.app.SDLActivity"
|
||||
android:name="org.gmodrs.client.MainActivity"
|
||||
android:label="gmod-rs"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:screenOrientation="landscape"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
|
||||
|
|
|
|||
|
|
@ -33,16 +33,27 @@ adb install -r target/android/gmod-rs-debug.apk
|
|||
```bash
|
||||
adb shell mkdir -p /sdcard/Android/data/org.gmodrs.client/files/gmod_server
|
||||
adb push ~/gmod_server/server /sdcard/Android/data/org.gmodrs.client/files/gmod_server/
|
||||
# adb создаёт каталоги от shell, а приложению нужен доступ:
|
||||
adb shell chmod -R 777 /sdcard/Android/data/org.gmodrs.client/files/gmod_server
|
||||
```
|
||||
|
||||
Приложение по умолчанию открывает `gm_construct` и ищет ассеты в
|
||||
`/sdcard/Android/data/org.gmodrs.client/files/gmod_server/server`. Другой путь
|
||||
можно задать переменной `GMOD_RS_ASSETS` (например, через `adb shell setprop`
|
||||
не сработает — переменные окружения Android-приложения задаются только из
|
||||
Java/обвязки; для отладки удобнее положить ассеты по стандартному пути).
|
||||
`/sdcard/Android/data/org.gmodrs.client/files/gmod_server/server` (точный путь
|
||||
передаёт `MainActivity` через `getExternalFilesDir`). Другой путь можно задать
|
||||
переменной `GMOD_RS_ASSETS` — но на телефоне переменные окружения задать
|
||||
непросто, поэтому проще положить ассеты по стандартному пути.
|
||||
|
||||
Минимально для `gm_construct` достаточно `garrysmod/` (карты и контент игры) и
|
||||
`sourceengine/` (текстуры/модели HL2); `platform/` нужен для части эффектов.
|
||||
Проверено на усечённом наборе (garrysmod VPK + карта): карта грузится и
|
||||
рисуется, часть материалов заменяется заглушкой.
|
||||
|
||||
## Запуск и логи
|
||||
|
||||
```bash
|
||||
adb shell am start -n org.gmodrs.client/.MainActivity
|
||||
adb logcat -s gmod-rs SDL
|
||||
```
|
||||
|
||||
## Управление
|
||||
|
||||
|
|
@ -57,5 +68,9 @@ Java/обвязки; для отладки удобнее положить ас
|
|||
команда `net` недоступна.
|
||||
- Lua на Android — Lua 5.4 (vendored), а не LuaJIT: кросс-сборка LuaJIT под
|
||||
Android в mlua не поддерживается. API GMod-скриптов тот же.
|
||||
- Проверено только arm64-v8a; x86_64-эмулятор требует отдельной сборки
|
||||
(см. `ANDROID_ABI`/`TARGET` в `build.sh`).
|
||||
- Собран и проверен `arm64-v8a` (APK для телефона) и `x86_64` (эмулятор).
|
||||
Проверка в эмуляторе API 35: карта грузится, кадры рисуются через
|
||||
SwiftShader (11–14 fps на софтверном GPU), стик/обзор/прыжок работают.
|
||||
На реальном arm64-устройстве не запускалось — проверьте `adb logcat`.
|
||||
- Для эмулятора: `ANDROID_ABI=x86_64 ./android/build.sh`, результат —
|
||||
`target/android/gmod-rs-debug-x86_64.apk`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package org.gmodrs.client;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Активность клиента gmod-rs.
|
||||
*
|
||||
* Наследует штатную SDLActivity и передаёт нативной части путь к ассетам:
|
||||
* внешний каталог приложения (`Android/data/org.gmodrs.client/files`) доступен
|
||||
* без разрешений на хранилище, в отличие от прямого `/sdcard`.
|
||||
*/
|
||||
public class MainActivity extends org.libsdl.app.SDLActivity {
|
||||
/**
|
||||
* SDL при создании окна запрашивает ориентацию из размеров окна и флага
|
||||
* resizable (FULL_USER), перебивая манифест. Для игры фиксируем ландшафт.
|
||||
*/
|
||||
@Override
|
||||
public void setOrientationBis(int w, int h, boolean resizable, String hint) {
|
||||
setRequestedOrientation(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getArguments() {
|
||||
File assets = new File(getExternalFilesDir(null), "gmod_server/server");
|
||||
android.util.Log.i("gmod-rs", "assets=" + assets.getAbsolutePath()
|
||||
+ " exists=" + assets.isDirectory()
|
||||
+ " external=" + getExternalFilesDir(null));
|
||||
return new String[] {
|
||||
"map",
|
||||
"gm_construct",
|
||||
"--assets",
|
||||
assets.getAbsolutePath()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -171,9 +171,14 @@ pub const ANDROID_DEFAULT_ASSETS: &str =
|
|||
/// Java-обвязка SDL (`SDLActivity`) загружает `libSDL3.so` и `libmain.so` и
|
||||
/// вызывает этот символ. По умолчанию открываем `gm_construct` с ассетами из
|
||||
/// внешнего каталога приложения; аргументы можно передать из Java.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Вызывается только из SDL: `argv` — массив из `argc` валидных
|
||||
/// NUL-терминированных строк.
|
||||
#[cfg(target_os = "android")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn SDL_main(
|
||||
pub unsafe extern "C" fn SDL_main(
|
||||
argc: std::os::raw::c_int,
|
||||
argv: *mut *mut std::os::raw::c_char,
|
||||
) -> std::os::raw::c_int {
|
||||
|
|
|
|||
|
|
@ -798,7 +798,7 @@ pub fn map(assets: Option<PathBuf>, args: MapArgs) -> anyhow::Result<()> {
|
|||
if touch.duck() {
|
||||
move_up -= 1.0;
|
||||
}
|
||||
let touch_jump = touch.jump();
|
||||
let touch_jump = touch.take_jump();
|
||||
if player.noclip {
|
||||
let speed = if pressed.contains(&Keycode::LShift) {
|
||||
args.speed * 3.0
|
||||
|
|
@ -901,6 +901,20 @@ pub fn map(assets: Option<PathBuf>, args: MapArgs) -> anyhow::Result<()> {
|
|||
|
||||
// HUD (тень + текст, чтобы читалось на любом фоне).
|
||||
let (width, height) = gfx.size();
|
||||
// Android рисует поверх всего экрана, включая системные панели:
|
||||
// сдвигаем HUD внутрь, иначе строки уезжают под статус-бар.
|
||||
let hud_top = if cfg!(target_os = "android") {
|
||||
96.0
|
||||
} else {
|
||||
8.0
|
||||
};
|
||||
let hud_bottom = if cfg!(target_os = "android") {
|
||||
// Нижняя системная панель на Android выше, чем кажется: держим
|
||||
// строку над ней, иначе подсказка уходит под чёрную полосу.
|
||||
height as f32 - 110.0
|
||||
} else {
|
||||
height as f32 - 28.0
|
||||
};
|
||||
fps = fps * 0.9 + (1.0 / dt.max(1e-4)) * 0.1;
|
||||
renderer.clear_text();
|
||||
let mut hud = |x: f32, y: f32, scale: f32, color: [f32; 4], text: String| {
|
||||
|
|
@ -909,14 +923,14 @@ pub fn map(assets: Option<PathBuf>, args: MapArgs) -> anyhow::Result<()> {
|
|||
};
|
||||
hud(
|
||||
8.0,
|
||||
8.0,
|
||||
hud_top,
|
||||
1.5,
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
format!("FPS {:.0}", fps),
|
||||
);
|
||||
hud(
|
||||
8.0,
|
||||
28.0,
|
||||
hud_top + 20.0,
|
||||
1.5,
|
||||
[0.85, 0.95, 1.0, 1.0],
|
||||
format!(
|
||||
|
|
@ -935,7 +949,7 @@ pub fn map(assets: Option<PathBuf>, args: MapArgs) -> anyhow::Result<()> {
|
|||
);
|
||||
hud(
|
||||
8.0,
|
||||
height as f32 - 28.0,
|
||||
hud_bottom,
|
||||
1.5,
|
||||
[0.95, 0.95, 0.8, 1.0],
|
||||
if touch_enabled {
|
||||
|
|
@ -970,7 +984,7 @@ pub fn map(assets: Option<PathBuf>, args: MapArgs) -> anyhow::Result<()> {
|
|||
);
|
||||
}
|
||||
if show_log {
|
||||
let mut y = height as f32 - 60.0;
|
||||
let mut y = hud_bottom - 32.0;
|
||||
for line in log.iter().rev().take(4) {
|
||||
hud(8.0, y, 1.2, [0.8, 1.0, 0.8, 0.95], line.clone());
|
||||
y -= 16.0;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ pub struct TouchControls {
|
|||
look_finger: Option<u64>,
|
||||
jump_finger: Option<u64>,
|
||||
duck_finger: Option<u64>,
|
||||
/// Короткое касание кнопки прыжка могло начаться и кончиться между
|
||||
/// кадрами — запоминаем его до ближайшего кадра.
|
||||
jump_pending: bool,
|
||||
}
|
||||
|
||||
impl TouchControls {
|
||||
|
|
@ -71,6 +74,7 @@ impl TouchControls {
|
|||
|
||||
if JUMP_BUTTON.contains(x, y) {
|
||||
self.jump_finger = Some(finger_id);
|
||||
self.jump_pending = true;
|
||||
return true;
|
||||
}
|
||||
if DUCK_BUTTON.contains(x, y) {
|
||||
|
|
@ -159,6 +163,7 @@ impl TouchControls {
|
|||
self.look_finger = None;
|
||||
self.jump_finger = None;
|
||||
self.duck_finger = None;
|
||||
self.jump_pending = false;
|
||||
self.move_vector = (0.0, 0.0);
|
||||
}
|
||||
|
||||
|
|
@ -170,8 +175,14 @@ impl TouchControls {
|
|||
self.move_vector
|
||||
}
|
||||
|
||||
pub fn jump(&self) -> bool {
|
||||
self.enabled && self.jump_finger.is_some()
|
||||
/// Прыжок на этот кадр: кнопка удерживается или была коротко нажата.
|
||||
pub fn take_jump(&mut self) -> bool {
|
||||
if !self.enabled {
|
||||
return false;
|
||||
}
|
||||
let jump = self.jump_finger.is_some() || self.jump_pending;
|
||||
self.jump_pending = false;
|
||||
jump
|
||||
}
|
||||
|
||||
pub fn duck(&self) -> bool {
|
||||
|
|
@ -206,10 +217,10 @@ mod tests {
|
|||
assert!(touch.finger_up(2));
|
||||
|
||||
assert!(touch.finger_down(3, 0.8, 0.6));
|
||||
assert!(touch.jump());
|
||||
assert!(touch.take_jump());
|
||||
assert!(!touch.duck());
|
||||
assert!(touch.finger_up(3));
|
||||
assert!(!touch.jump());
|
||||
assert!(!touch.take_jump());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -217,6 +228,6 @@ mod tests {
|
|||
let mut touch = TouchControls::new(false);
|
||||
assert!(!touch.finger_down(1, 0.2, 0.8));
|
||||
assert_eq!(touch.movement(), (0.0, 0.0));
|
||||
assert!(!touch.jump());
|
||||
assert!(!touch.take_jump());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
//! Инициализация логирования (tracing).
|
||||
|
||||
use std::io::IsTerminal;
|
||||
use std::sync::Once;
|
||||
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
|
@ -25,13 +24,16 @@ pub fn init(verbosity: u8) {
|
|||
EnvFilter::new(level)
|
||||
});
|
||||
|
||||
let ansi = std::io::stdout().is_terminal();
|
||||
let builder = tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let _ = builder.with_ansi(ansi).try_init();
|
||||
let _ = {
|
||||
use std::io::IsTerminal;
|
||||
let ansi = std::io::stdout().is_terminal();
|
||||
builder.with_ansi(ansi).try_init()
|
||||
};
|
||||
|
||||
// На Android stdout/stderr не видны: пишем строки в logcat
|
||||
// (`adb logcat -s gmod-rs`), иначе диагностика запуска теряется.
|
||||
|
|
|
|||
61
docs/ANDROID.md
Normal file
61
docs/ANDROID.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Android-порт клиента
|
||||
|
||||
Цель — тот же клиент `gmod-rs` (SDL3 + wgpu), запускаемый как APK на Android
|
||||
12+ (API 31+). Отдельного «мобильного движка» нет: карта, физика, Lua и рендер
|
||||
общие с десктопом.
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
./android/build.sh # arm64-v8a, APK для телефона
|
||||
ANDROID_ABI=x86_64 ./android/build.sh # x86_64, для эмулятора
|
||||
```
|
||||
|
||||
Скрипт использует SDK/NDK, `cargo`, `aapt2`, `d8`, `zipalign`, `apksigner`
|
||||
(Gradle не нужен). Результат — `target/android/gmod-rs-debug.apk`
|
||||
(подписан debug-ключом `android/debug.keystore`). Подробности и команды
|
||||
установки — в `android/README.md`.
|
||||
|
||||
## Крейты и target cfg
|
||||
|
||||
- `crates/client` теперь и библиотека (`src/lib.rs`), и бинарь (`src/main.rs`).
|
||||
Бинарь вызывает `client::run(args)`, Android-сборка — `cargo build --lib`
|
||||
с `crate-type = ["cdylib", "rlib"]`.
|
||||
- Экспорт `SDL_main` (в `lib.rs` под `cfg(target_os = "android")`) — точка
|
||||
входа для штатной Java-обвязки SDL3 (`SDLActivity`).
|
||||
- `steamworks` подключён только для не-Android; команда `net` на Android
|
||||
отсутствует.
|
||||
- `mlua`: на десктопе LuaJIT + `vendored`, на Android — Lua 5.4 + `vendored`
|
||||
(LuaJIT не собирается кросс-компилятором mlua). API GMod-скриптов сохраняется.
|
||||
- `tracing` на Android пишет в logcat (`adb logcat -s gmod-rs`), а не в stdout.
|
||||
|
||||
## APK
|
||||
|
||||
- Java-классы SDL3 (`org.libsdl.app.*`) взяты из `sdl3-src` (zlib-лицензия) и
|
||||
лежат в `android/app/src/main/java`.
|
||||
- `MainActivity` наследует `SDLActivity`, фиксирует ландшафт и передаёт
|
||||
нативной части аргументы `map gm_construct --assets <external files>/...`.
|
||||
- Нативные библиотеки пакуются как `lib/arm64-v8a/libSDL3.so` и
|
||||
`libmain.so` (внутри — `libclient.so` с `SDL_main`).
|
||||
- Ассеты не входят в APK: их кладут во внешний каталог приложения
|
||||
(`Android/data/org.gmodrs.client/files/gmod_server/server`), см. README.
|
||||
|
||||
## Управление
|
||||
|
||||
Обрабатываются `SDL_EVENT_FINGER_*`; синтетические события мыши от касаний
|
||||
отключены (`SDL_TOUCH_MOUSE_EVENTS=0`):
|
||||
|
||||
- левая половина экрана — аналоговый стик (forward/right идут в тот же
|
||||
`Player::update`, что WASD);
|
||||
- правая половина — обзор свайпом (приращение углов камеры);
|
||||
- кнопки `JUMP`/`DUCK` — прыжок (с защёлкой короткого тапа) и присед;
|
||||
- на Android движение сразу бегом (отдельной кнопки Shift нет).
|
||||
|
||||
## Проверка
|
||||
|
||||
- `cargo clippy -p client --lib --target aarch64-linux-android -- -D warnings`.
|
||||
- Эмулятор API 35 x86_64: APK запускается, `gm_construct` грузится, кадры
|
||||
рисуются (SwiftShader, ~11–14 fps), стик/обзор/прыжок проверены через
|
||||
`adb shell input`.
|
||||
- Реальное arm64-устройство не проверялось (нет устройства); при отладке —
|
||||
`adb logcat -s gmod-rs SDL`.
|
||||
|
|
@ -22,7 +22,8 @@ crates/
|
|||
ui — нативный UI: меню, консоль, шрифты, HUD-рамка, загрузочные экраны
|
||||
lua — mlua (vendored LuaJIT), sandbox, рельсы API, мосты к engine/render/studio
|
||||
steam — фичи steamworks-rs (тикет, UGC/workshop), отдельный steam-поток
|
||||
client — бинарь `gmod-rs`: сборка всего, главный цикл, конфиг, CLI
|
||||
client — бинарь `gmod-rs` и библиотека: сборка всего, главный цикл,
|
||||
конфиг, CLI; на Android — cdylib с `SDL_main` (см. `ANDROID.md`)
|
||||
```
|
||||
|
||||
### Правило зависимостей
|
||||
|
|
@ -158,6 +159,19 @@ texture streaming и VRAM-бюджет (с M9).
|
|||
- Авто-детект ассетов: (1) явный путь; (2) `~/gmod_server/server`; (3) Steam common.
|
||||
- Внутриигровые настройки — конвары (как в Source), сохраняются в `cfg/`.
|
||||
|
||||
### 3.10 Android
|
||||
|
||||
- Тот же клиент собирается как `cdylib` и грузится штатной `SDLActivity` из
|
||||
SDL3; экспорт — `SDL_main`. Сборка APK — `android/build.sh` (aapt2 + d8,
|
||||
без Gradle), debug-подпись.
|
||||
- Целевые ABI: `arm64-v8a` (телефон) и `x86_64` (эмулятор). `minSdk 31`,
|
||||
`targetSdk 35`, фиксированный ландшафт.
|
||||
- `target.'cfg(target_os = "android")'`: без `steamworks`, Lua 5.4 вместо
|
||||
LuaJIT; логирование в logcat.
|
||||
- Ассеты — во внешнем каталоге приложения; касания (`FingerDown/Motion/Up`)
|
||||
переведены в те же `wish_forward/right`, прыжок и присед, что клавиатура.
|
||||
- Детали и ограничения — `docs/ANDROID.md`, команды установки — `android/README.md`.
|
||||
|
||||
## 4. Конвенции кода
|
||||
|
||||
1. `unwrap/expect/panic` запрещены в путях обработки внешних данных (сеть/диск/Lua).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue