화면 — HUD, 미니맵, 키, 인벤토리 창
Bukkit의 커스텀 GUI·스코어보드·보스 바·지도처럼, 플러그인이 플레이어의 화면에 자기 것을 그립니다. 이 쪽의 것은 서버 쪽 코드가 보내는 데이터(요소, 채널의 값)를 게임이 그리고, 누르면 서버에 패킷을 보냅니다. 서버 쪽은 src/plugin_api/ui.rs, 게임 쪽은 PluginHud. 예: plugins/screen.
매 프레임 바뀌는 화면(카운트다운, 따라다니는 표시), 카메라, 화면 페이드·번쩍임은 서버를 오가지 않고 게임 쪽 코드에서 그리세요. 요소 El은 두 쪽이 같습니다(게임 쪽 Hud::add / set / remove / hide / show).
#[on(Join)]
fn came(e: Join) {
let hud = e.player().hud();
hud.add("mana", &El::panel("wood").anchor(Anchor::BottomRight).pos(-130.0, -120.0).size(220.0, 64.0)
.child(El::bar(50.0, 100.0).pos(10.0, 10.0).size(130.0, 20.0).color("4f7bff").label("마나").bind("mana"))
.child(El::button("화염", "cast").anchor(Anchor::TopRight).pos(-8.0, 8.0).size(64.0, 44.0)));
hud.key("H", "clock"); // H를 누르면 채널 "clock"의 패킷
hud.send_text("mana", "50/100"); // 바가 이 값을 보여 줌
}
#[on(Packet)]
fn packet(e: Packet) {
match e.channel().as_str() {
"cast" => { /* 화염 버튼 */ }
"clock" => e.player().hud().hide("clock"),
_ => {}
}
}
Hud
Player::hud()는 그 사람의 화면, World::hud()는 월드 모두의 화면입니다(같은 요소를 모두에게, 채널의 값도 모두에게).
| 메서드 | |
|---|---|
add(id, &El) | 이 플러그인의 요소 (id는 플러그인마다 따로; 같은 id가 있으면 바꿈) |
set(id, &El) | 준 필드만 바꿈 (자식을 주면 자식은 통째로) |
remove(id) | |
clear() | 이 플러그인의 요소와 키를 모두 없애고, 숨긴 게임 HUD를 다시 보임 |
hide(part) / show(part) | 게임 자신의 HUD 부분 숨기기 / 다시 보이기 (아래) |
key(key, on) / key_action(on, label, key) | 단축키: 키("M" "F5" "Tab")를 누르면 채널 on의 패킷 (패널이 열려 있지 않을 때; "" 풀기). 플레이어의 설정 › 조작키에 label로 나와 다른 키로 바꿀 수 있음 (아래 「단축키」) |
replace(window) / restore(window) | 게임의 창을 열면 이 플러그인에 "window" 패킷이 대신 감 / 게임의 창으로 되돌림 (아래 「게임의 창 바꾸기」) |
send(chan, &[u8]) send_text(chan, text) | 이 플러그인의 채널 chan에 값 (256 KB까지). 게임은 마지막 값을 갖고 있고, 그 채널에 묶인 요소가 보여 줌 |
send_fast(chan, text) | 게임 쪽 코드에 빠른 패킷 (데이터그램, 1000바이트까지, 순서 없음, 잃으면 그만; 게임 쪽 Packet::fast()). 꼭 가야 하는 것은 send_text |
send_png(chan, w, h, &rgba) → i32 | RGBA8 픽셀(w × h, 512 × 512까지)을 PNG로 만들어 채널에: 묶인 이미지가 보여 줌. PNG의 바이트 수, 픽셀이 w × h × 4가 아니면 0 |
플러그인 하나의 요소는 자식까지 세어 200개까지입니다. 서버를 떠나면 그 서버의 플러그인이 띄운 것은 모두 사라집니다.
게임 HUD의 부분
hide / show의 이름: clock(날짜, 시각, 날씨, 골드) status(그 아래의 알약과 버튼) nearby(가까운 사람) chat hotbar bars(체력과 기력) quests prompt(E가 하는 일) toasts plugins(plugin.json의 버튼과 바) menu(휴대폰의 메뉴 버튼). 여러 플러그인이 숨기면 모두 show해야 다시 보입니다.
요소 (El)
| 만들기 | |
|---|---|
El::panel(style) | 다른 요소를 담는 상자: "wood"(HUD의 나무 틀) "parch"(양피지) "dark"(어두운 알약) "none" |
El::text(t) | 글 (묶으면 채널의 글) |
El::image(src) | res:// 그림 (게임의 것, 팩·플러그인 assets/의 것). ""에 bind: 채널의 PNG |
El::bar(value, max) | 바 (묶으면 채널의 "30" 또는 "30/100") |
El::button(label, on) | 누르면 채널 on의 패킷 (데이터 "") |
El::input(placeholder, on) | 한 줄 입력: Enter면 채널 on의 패킷 (데이터: 쓴 글) |
| 필드 | |
|---|---|
anchor(Anchor::…) | 화면의 어디: TopLeft Top TopRight Left Center Right BottomLeft Bottom BottomRight (자식은 부모 안에서) |
pos(x, y) size(w, h) | 앵커에서 안쪽으로, HUD의 디자인 픽셀(PC 1280 × 720 기준으로 늘림) |
color(hex) | 바의 채움, 글의 색, 패널의 바탕 |
font_size(n) label(t) set_text(t) value(v, max) visible(on) | |
bind(chan) | 이 플러그인의 채널 값을 보여 줌 |
follow(x0, z0, per_cell) | 이미지를 월드 지도의 창으로: 픽셀 (0, 0)이 칸 (x0, z0), 한 칸에 per_cell 픽셀. 게임이 매 프레임 플레이어 자리를 가운데에 둠 (걸을 때마다 다시 보낼 필요 없음) |
zoom(z) | 따라가는 이미지를 몇 배로 |
rotate(on) | 카메라를 따라 돌림 (위가 카메라가 보는 쪽) |
round(on) | 둥글게 자름 |
marker(on) | 가운데에 플레이어 표시 (바라보는 쪽으로 도는 화살표) |
child(El) | 자식 |
raw(k, json) | 그 밖의 필드를 JSON 그대로 |
미니맵
World::columns_raw(x0, z0, w, d)는 위에서 본 땅을 기둥마다 두 바이트(가장 높은 단단한 블록의 번호(위에 물이면 물), 높이 − Block::LOWEST)로 줍니다(256 × 256까지). 픽셀은 블록 번호로 표를 찾아 만드세요: 이벤트 한 번의 연료(5 000 000)로 128 × 128 지도가 넉넉합니다(칸마다 함수를 부르면 모자람). columns()는 같은 것을 (Block, 높이)로.
const MAP: i64 = 128;
fn draw_map(w: &World, p: &Player) {
let (x, z) = p.position();
let (x0, z0) = (x.round() as i64 - MAP / 2, z.round() as i64 - MAP / 2);
let mut colour = [[60u8, 60, 60]; 16];
for (b, c) in [(Block::WATER, [64, 132, 196]), (Block::SAND, [222, 205, 150]), (Block::GRASS, [104, 168, 76])] {
colour[b.0 as usize] = c;
}
let raw = w.columns_raw(x0, z0, MAP, MAP);
let mut px = vec![255u8; raw.len() * 2];
for (i, c) in raw.chunks_exact(2).enumerate() {
px[i * 4..i * 4 + 3].copy_from_slice(&colour[(c[0] & 15) as usize]);
}
p.hud().set("map", &El::default().follow(x0 as f64, z0 as f64, 1.0));
p.hud().send_png("map", MAP as i32, MAP as i32, &px);
}
// Join에서: 둥글고 카메라를 따라 도는 지도
// hud.add("map", &El::image("").anchor(Anchor::TopLeft).pos(16.0, 16.0).size(176.0, 176.0).bind("map")
// .follow(0.0, 0.0, 1.0).zoom(2.0).round(true).rotate(true).marker(true));
플레이어가 지도 가운데에서 멀리 걸어갔을 때만(Tick에서) 다시 그리면 됩니다.
패킷 (Packet)
버튼·입력 칸의 on, key로 묶은 키, 그리고 게임 쪽 코드의 send는 그 플러그인에게만 Packet 이벤트로 옵니다: channel(), data()(4 KB까지), player(). 플레이어의 행동처럼 판정되므로 여기서 give take add_gold …가 됩니다.
인벤토리 창
플러그인의 칸 창(Bukkit의 커스텀 인벤토리). 칸에 무엇이 있는지는 플러그인이 정하고, 누르면 InventoryClick이 옵니다. 아무것도 저절로 옮겨지지 않습니다.
fn shop() -> Inventory {
Inventory::new("shop", "마나 상점", 1, 5) // id, 제목, 줄, 칸 (각 1~12)
.slot(0, "wood", 5, "나무 다섯", "마나 30") // 칸, 아이템(아이콘), 개수, 이름, 설명 ("": 아이템의 것)
.with_bag() // 옆에 그 사람의 가방
}
#[on(InventoryClick)]
fn click(e: InventoryClick) {
let p = e.player();
match e.side().as_str() {
"top" if e.slot() == 0 => p.give("wood", 5),
"bag" if !e.bag_item().is_empty() => p.take(&e.bag_item(), 1),
_ => return,
}
p.open_inventory(&shop()); // 가방이 바뀐 대로 다시 보임
}
Player::open_inventory(&inv)(이미 열려 있으면 지금 칸으로), Player::close_inventory(id). 서버는 열린 창을 기억해 그 창의 칸만 받습니다.
단축키
Hud::key_action("map", "미니맵 켜기/끄기", "M")은 기본 키가 M인 단축키입니다. 키보드 그림의 키 위에 나올 짧은 이름은 hud.shortcut(&Shortcut::new("map", "미니맵 켜기/끄기", "M").short("지도"))로 줍니다(4자까지). 주지 않으면 이름이, 4자가 넘으면 앞 3자에 “…”를 붙여 나옵니다(“보따리 / 원래 가방” → “보따리…”). 플레이어는 설정 › 조작키에서 보라색 칩으로 보고 다른 키로 옮기거나(Backspace: 키 없음) 기본값으로 되돌릴 수 있습니다. 바꾼 키는 서버마다 플레이어 기기에 남습니다.
- 게임이 쓰는 키(Esc, Enter, V, 단축바 숫자, 조작키의 모든 키)와 겹치면 게임이 먼저입니다: 그 단축키는 키 없이 남고, 먼저 등록된 다른 단축키와 겹쳐도 마찬가지입니다.
- 접속한 뒤 그런 단축키가 있으면 팝업 하나로 알립니다: “이 서버의 플러그인 단축키 N개가 게임의 키와 겹쳐 바인딩이 취소되었어요” 와 그 목록(접속마다 한 번).
환영 창 (그림이 든 창)
접속(Join)에서 HUD 요소로 그립니다: 가운데 panel에 플러그인 그림, 글, 닫는 버튼.
#[on(Join)]
fn came(e: Join) {
let p = e.player();
p.hud().add("welcome", &El::panel("wood").anchor(Anchor::Center).size(400.0, 340.0)
.child(El::image("res://plugins/screen/welcome.png").anchor(Anchor::Top).pos(0.0, 16.0).size(360.0, 150.0))
.child(El::text("어서 오세요!").anchor(Anchor::Top).pos(0.0, 176.0).size(360.0, 30.0).font_size(20))
.child(El::button("시작하기", "welcome").anchor(Anchor::Bottom).pos(0.0, -16.0).size(140.0, 44.0)));
}
// Packet "welcome": p.hud().remove("welcome")
pos는 앵커가 둔 자리에서 더 옮기는 양입니다(가운데 앵커는 이미 가운데). text에 폭(size)을 주면 줄바꿈되고, 가운데 앵커면 가운데 정렬됩니다.
게임의 창 바꾸기
바꾸기 — Hud::replace(window): 플레이어가 그 창을 열면 게임은 열지 않고 이 플러그인에 채널 "window"의 패킷을 보냅니다. 플러그인은 자기 것(인벤토리 창, HUD 요소, gui)을 보여 주면 됩니다. 아무것도 안 하면 그 창은 막힌 셈입니다. restore(window)로 게임의 창이 돌아옵니다.
| 창 | 패킷 데이터 (JSON) |
|---|---|
bag | 가방의 모든 탭: {"window": "bag", "tab": "bag"} (bag skills relations map craft quests) |
shop | 가게: "shop" (general clinic furniture ranch …), "selling" |
smith / enchant | 대장간 / 마법사의 마법 부여대 |
chest | 상자: "x" "z" |
build | 서연의 건축 |
letter | 우편함 편지: "from" "text" "items" |
bundles / library / festival | 꾸러미 / 도서관 / 축제 ("festival") |
dialogue | 주민과의 대화: "who" "text" "choices" (선택지 글). 플러그인이 Hud::choose(n)으로 고름 |
friends | 친구 |
balloon | 열기구 (마을 정거장 ↔ 사막 정거장) |
sleep | 잠자기 물음 (Hud::open("sleep", "{}")이면 눕기) |
summary | 아침 결산: "lines" "total" |
mirror | 거울(모습 바꾸기) |
catch | 잡은 물고기 카드: "id" "q" … |
받는 쪽은 WindowOpen 이벤트로 쓰면 편합니다(window(), get("tab") …; 채널 "window"의 Packet으로도 옴). 게임은 가방 같은 창을 서버에 묻지 않고 바로 열기 때문에(기다림 없이), 열 때마다 묻는 대신 가져갈 창을 미리 정해 두는 방식입니다.
Hud::open(window, data): 바꾼 창이라도 게임의 것을 지금 엶 (플러그인 자기 화면 다음에, 예: 확인을 받은 뒤 sleep).
#[on(Join)]
fn came(e: Join) {
e.player().hud().replace("bag");
}
#[on(WindowOpen)]
fn opened(e: WindowOpen) {
if e.window() == "bag" {
e.player().open_inventory(&Inventory::new("bag", "보따리", 1, 3).with_bag());
}
}
판정은 그대로 서버의 행동입니다: 가방 창을 바꿔도 아이템은 서버의 것이고, 플러그인이 주고 가져갑니다.
설정 창은 바꿀 수 없습니다: 플레이어가 늘 설정(과 로비로 나가기)에 갈 수 있게, settings는 바꾸기 대상이 아니고 설정 버튼은 그 버튼이 든 HUD 부분(status menu)을 숨겨도 남습니다(Esc도 그대로).
SDK 없이
“gh” 함수 hud(json) hud_all(json)의 JSON:
{"op": "add", "id": "mana", "el": {"type": "bar", "anchor": "bottom_right", "pos": [-130, -120], "size": [130, 20], "bind": "mana"}}
{"op": "set", "id": "mana", "el": {"visible": false}}
{"op": "remove", "id": "mana"}
{"op": "clear"}
{"op": "hide", "part": "clock"}
{"op": "show", "part": "clock"}
{"op": "key", "key": "M", "on": "map"}
요소의 키: type(panel text image bar button input) anchor pos size color style text label placeholder value max src bind on follow zoom rotate round marker font_size visible children. 인벤토리는 inv_open({"id", "title", "rows", "cols", "slots": [{"item", "n", "name", "lore"} 또는 null, …], "bag": true}), 닫기 {"id", "close": true}. 함수의 모양은 “gh” 함수.