Captain Drexx изнутри. Часть 5. User interface
В этой части я хочу рассказать о управлении игрой.
Содержание цикла «Captain Drexx изнутри»
Вся система управления игрой висит в обработчике прерывания.
Соответственно, здесь мы должны считывать клавиатуру или мышь и обрабатывать ситуации в игре, а так-же показывать все события UI.
Надо сказать, что это один из самых нагруженных логикой блоков игры, так как интерфейс пользователя построен на некоторой событийной системе, основанной на переключателях событий.
Нужно отрабатывать такие ситуации как:
- установка башни
- апгрейд башни
- отображение радиуса поражения выбранной башни
- отработка ситуации отказа или нехватки денег (что делать, если клиент говорит — нет?)
- отработка ситуаций когда апгрейд уже не доступен, или башню пытаются поставить на другую, либо воткнуть на путь крипов
- прочие подобные странные ситуации
Как происходит управление? мы выбираем башню которую хотим установить, возле указателя появляется зона поражения. По клику на место установки башня будет поставлена.
Если-же клик приходится на установленную башню, мы показываем её охват и отображаем в меню данные по апгрейду башни.
Пример работы UI хорошо виден в видео прохождения игры:
А вот пример первого варианта UI:
Данный способ оказался менее понятным и менее удобным и был переписан.
Итак, давайте посмотрим как это происходит. Смотрим на обработчик прерываний:
interrupt di
push af
ex af,af
push af
push hl
push de
push bc
push ix
intro_int ld a,0
or a
jr nz,game_int
отработка прерывания для интро / аутро игры. проигрываем необходимую музыку.
ld a,(current_page)
push af
ld a,3
call page
call vtplayer
pop af
call page
jp rmc_exit
Обработка игровых событий:
проигрываем звуки, подсчитываем количество фреймов, прошедших после обнуления счётчика. Это даст нам фпс игры, который крайне желателен для комфортной игры — 25 кадров.
game_int
; ld a,7
; out (#fe),a
call AFXFRAME
; ld a,(int_calk+1)
; ld de,#50e8
; call view_energy
int_init ld a,0
or a
jp z,rmc_exit
int_calk ld a,0
inc a
ld (int_calk+1),a
md1 LD A,(current_page)
LD (int_ram+1),A
ld a,7
call page
call mouse_restore_screen
call mouse_pos
call mouse_map_adr
Восстановление экрана под указателем, собираем данные о положении указателя и вычисляем его координаты на игровом поле.
tower_upgrade_range_flag
ld a,#ff
cp #ff
call nz,tower_upgrade_range
Начало отработки стандартных ситуаций.
Переключатель отображения радиуса действия башни при доступном для неё апгрейде:
stat_upgrade_flag
ld a,#ff
ld b,a
cp #ff
call nz,stat_upgrade
; ld a,7
; out (#fe),a
stat_new_tower_flag
ld a,0
or a
call z,stat_new_tower
отметка цветом башен в меню при доступном для них апгрейде
; xor a
; out (#fe),a
LD A,(screen_ready)
or a
jr nz,int_1
CALL CHANGE_SCREEN
ld a,1
ld (screen_ready),a
int_1
если основной цикл игры отработал, всё полностью отрисовано — переключаем экран для отрисовки
ld a,(end_wave+1)
or a
jr nz,int_ram
tower_preview_range_flag
ld a,0
or a
call nz,tower_preview_range
отображаем радиус действия башни
; ld a,6
; out (#fe),a
call mouse_pressed
mouse_pressed — основная процедура, которая отрабатывает нажатия клавиш мыши / стрельбы на клавиатуре
; ld a,7
; out (#fe),a
int_ram ld a,7
call page
ld a,(end_wave+1) ; fix
or a
call z, mouse_view
pause_view ld a,(pause_button_view+1)
or a
call nz,pause_view_atr
отображаем состояние паузы
red_lives_counter
ld a,0
or a
jr z,red_money_counter
dec a
ld (red_lives_counter+1),a
or a
jr nz,red_money_counter
call fill_lives_atr
посветка количества жизни при их уменьшении
red_money_counter
ld a,0
or a
jr z,rmc_exit
dec a
ld (red_money_counter+1),a
or a
jr nz,rmc_exit
ld a,#47
call fill_money_atr
rmc_exit
подкрашивание на экране количества денег при их недостатке для покупки/апгрейда
; xor a
; out (#fe),a
pop ix
pop bc
pop de
pop hl
pop af
ex af,af
pop af
ei
ret
Для управления указателем можно использовать как мышь, так и клавиатуру, даже одновременно. Если мыша у нас включена, то читаем её кнопки, потом уже читаем клавиатуру:
mouse_pos
mouse_update ld a,0
or a
jr nz,key_in
LD BC,#FADF
IN A,(C) ;читаем порт кнопок
/*
D0 - левая кнопка
D1 - правая кнопка
D2 - средняя кнопка
Standard Kempston Mouse
#FADF - buttons
#FBDF - X coord
#FFDF - Y coord
*/
key_in ld (mouse_button),a
call key_scan
ki0 LD hl,MOUSE11+1
ld a,d
cp #af
jr nc,ki5
kileft and left
jr z,ki1
inc (hl)
inc (hl)
ki1 ld a,d
kiright and right
jr z,ki2
dec (hl)
dec (hl)
ki2 LD hl,MOUSE12+1
ld a,d
kidown and down
jr z,ki3
inc (hl)
inc (hl)
ki3 ld a,d
kiup and up
jr z,ki4
dec (hl)
dec (hl)
в зависимости от нажатой клавиши меняем координаты указателя
ki4 ld a,d
and fire
or a
jr z,ki5
ld a,#0e
ld (mouse_button),a
ki5 ld a,d
cp rmb_key
правая кнопка мыши или BREAK у нас — кнопка отбоя выбранного действия
jr nz,ki8
ld a,#0d
ld (mouse_button),a
jr ki6
смотрим на нажатие пробела, что есть пауза в игре
ki8 call keys_wait_proc
ld a,d
cp key_pause
jp z,pause_button_view
cp key_tower1
jr c,ki6
ld c,a
keys_wait ld a,0
or a
jr z,ki7
jr ki6
отрабатываем задержку нажатого пробела, дабы клавиша не дребезжала
keys_wait_proc ld a,(keys_wait+1)
or a
ret z
dec a
ld (keys_wait+1),a
ret
ki7 ld a,(tower_upgrade_flag)
or a
jr z,ki9
ld a,c
ld (upgrade_but+1),a
jr ki10
ki9 ld a,c
ld (tower_keys+1),a
ki10 xor a
ld (tpr_old+1),a
ld a,18
ld (keys_wait+1),a
ld a,#0e
ld (mouse_button),a
ki6
читаем данные портов XY мыши
LD HL,(mouse_xy)
LD BC,#FBDF
mz0 IN A,(C)
MOUSE11 LD D,0
LD (MOUSE11+1),A
SUB D
CALL NZ,MOUSE30
LD B,#FF
mz1 IN A,(C)
MOUSE12 LD D,0
LD (MOUSE12+1),A
SUB D
CALL NZ,MOUSE40
ld a,h
cp #f7
jr c,mouse13
ld h,#f7
mouse13 ld a,l
cp #b6
jr c,mouse14
ld l,#b6
mouse14 LD (mouse_xy),HL
RET
координаты указателя получены и сложены в переменную mouse_xy
MOUSE30
JP M,MOUSE35
ADD A,H
LD H,A
RET NC
LD H,#Ff
RET
MOUSE35
XOR #FF
INC A
LD D,A
LD A,H
SUB D
LD H,A
RET NC
LD H,0
RET
MOUSE40
JP M,MOUSE45
LD E,A
LD A,L
SUB E
LD L,A
RET NC
LD L,#0
RET
MOUSE45
XOR #FF
INC A
ADD A,L
LD L,A
RET NC
LD L,#FF
RET
Спасибо alex rider за процедуру для работы с клавиатурой!
; ----- alex rider keys
right: equ #01
left: equ #02
down: equ #04
up: equ #08
fire: equ #10
rmb_key equ #f0
key_pause equ #cf
key_tower1 equ #b0
key_tower2 equ #b2
key_tower3 equ #b4
key_tower4 equ #b6
key_code equ #b8
; in: nothing
; out:
key_scan ; d - pressed directions in kempston format
ld a,#fe ; check for CAPS SHIFT
in a,(#fe)
rra
ld hl,key_table - 1 ; selection of appropriate keyboard table
jr c,.no_cs
ld hl,cs_key_table - 1 ; hl - keyboard table (zero-limited)
.no_cs:
ld d,#00 ; clear key flag
ld c,#0fe ; low address of keyboard port
.loop:
inc hl ; next key
ld b,(hl) ; high byte of port address from table
inc b ; end of table check
dec b
ret z
inc hl ; going to key mask
in a,(c) ; reading half-row state
or (hl) ;
inc hl ; going to key flag
inc a ; a = half-row state or mask. if #ff - current key isn't pressed
ld a,d
jr z,.loop ; key isn't pressed
or (hl) ; result or key flag
ld d,a ; store it
jr .loop
непосредственно данные портов для чтения клавиш
; key table format
; 1st byte - high byte of keyboard half-row address
; 2nd byte - inverted key mask (e.g. outer key - #fe, next key - #0fd etc)
; 3rd byte - direction bit
key_table:
db #0ef, #0fe, fire ;0
db #0ef, #0fd, up ;9
db #0ef, #0fb, down ;8
db #0ef, #0f7, right ;7
db #0ef, #0ef, left ;6
db #0f7, #0fe, key_tower1 ;1
db #0f7, #0fd, key_tower2 ;2
db #0f7, #0fb, key_tower3 ;3
db #0f7, #0f7, key_tower4 ;4
db #0f7, #0ef, key_code ;5
db #0df, #0fe, right ;p
db #0df, #0fd, left ;o
db #0fb, #0fe, up ;q
db #0fd, #0fe, down ;a
db #07f, #0fb, fire ;m
db #07f, #0fe, key_pause ;space
db #000
cs_key_table:
db #0ef, #0fe, fire ;0
db #0ef, #0fb, right ;8
db #0ef, #0f7, up ;7
db #0ef, #0ef, down ;6
db #0f7, #0ef, left ;5
db #07f, #fd , rmb_key ;caps+sym
db #000
Итак, координаты получены. Нужно получить номер клетки на поле, над которым расположен указатель.
Так как поле у нас разбито на блоки 16х16, вычислить положение указателя над определённым квадратом довольно просто — берём положение указателя и сдвигами получаем конкретный квадрат игрового поля:
mouse_map_adr
ld hl,(mouse_xy)
ld a,h
srl a
srl a
srl a
srl a
ld h,a
ld a,l
srl a
srl a
srl a
srl a
ld l,a
ld (mouse_map),hl
ret
mouse_pressed. Глубокое погружение в события игры.
Начинаем самую сложную для понимания ветку логики.
Fire был нажат. на каком обьекте? когда? что делать? кто виноват?
mouse_pressed ld c,0
ld a,(mouse_button)
cpl
and #3
or a
jr nz,mpr0
ld (mouse_pressed+1),a
ld a,c
ret
сохраняем старое состояние нажатий, если правая / левая кнопка (либо fire / break клавиатуры) нажаты, начинаем процесс:
mpr0 cp c
ret z
ld (mouse_pressed+1),a
cp 1
jp nz,rmb2
похоже, бащню собираются апгрейдить! смотрим:
upgrade_but ld a,0
or a
jr z,tk0
ld c,a
xor a
ld (upgrade_but+1),a
ld a,(tower_upgrade_flag)
jp tower_upgrading
да, нажали, нажааали. вычисляем клетку 16х16 на экране:
tk0 ld hl,(mouse_map)
ld a,l ;Y
add a,a
add a,a
add a,a
add a,a
add h ;X
ld c,a
cp #ba
jp z,pause_button_view
могли нажать на поле «пауза». нет?
или, возможно, хотят поставить новую башню?
ld a,(tower_preview_range_flag+1)
or a
jp nz,set_new_tower
похоже, башню собираются апгрейдить!
ld a,(tower_upgrade_flag)
or a
jp nz,tower_upgrading
похоже, башню таки будут апгрейдить, все данные для этого мы уже показали, и место выбрано. и, похоже, для этого даже нажали на цифру на клавиатуре?
tower_keys ld a,0
or a
jr nz,cmp_towers_ex_keys
таки да, но место для башни вообще на игровом поле?
ld a,c
cp #af
jp nc,cmp_towers
ld hl,(map)
ld a,c
add l
ld l,a
ld a,(hl)
cp #ff
jp nc,rmb2 ; #ff - можно ставить башни, else exit
можно ли там вообще башню ставить? если можно, смотрим что там сейчас находится:
cmp_towers ld hl,towers ; поиск башни по позиции
mp0 ld a,(hl)
cp #fe
jp z,cmp_towers_ex
если список башен весь обработан, выходим в установку новой.
если башенка с такими координатами в списке обнаружена, нужно отрабатывать ситуацию апгрейда.
ld b,a ; type
inc hl
ld a,(hl) ; position
cp c
jp z,tower_upgrade_view
mpn0 inc hl
ld a,(hl) ; range propusk
cp #ff
jr nz,mpn0
inc hl
jr mp0
cmp_towers_ex
ld a,c
cp #af
jp c,rmb2
cp #b8
jp nc,rmb2
если мы за пределами карты — выходим
cmp_towers_ex_keys
ld c,a
xor a
ld (tower_keys+1),a
ld a,c
sub #b0
rra
ld hl,tower_list
ld e,a
ld d,0
add hl,de
ld a,(hl)
cp #ff
jp z,rmb2
вычисляем что за башня у нас будет
ld a,c
ld (tower_preview_range_flag+1),a
включаем, какая зона поражения будет показана для этой башни
sub #b0
and #fe
add a,a
add #c0
ld l,a
ld h,#5a
ld e,a
ld d,#da
ld bc,#4202
call block_attr_fill
jp lowbright_atr
затемняем экран, посвечиваем только зону поражения
если всё-таки будем апгрейдить башню, начинаем вычисления — а не на максимум она у нас раскачана?
tower_upgrading ld (tupos+1),a
ld a,c
cp #af
jp c,rmb2
ld a,(tower_upgrade_power)
cp #ff
jp z,rmb2
если да, то выход, выше апгрейда у нас нет.
ld a,(tower_upgrade_price)
ld b,a
ld a,(money)
sub b
jp c,low_money
ld (money),a
ld de,money_adr
push bc
call view_dec_energy
pop bc
башня куплена, деньги потрачены, товар давай!
находим башню в списке и начинаем заносить обновленные данные апгрейда:
ld hl,towers
tu0 inc hl
ld a,(hl) ; position
tupos cp 0
jr z,tower_upgrade1
tun0 inc hl
ld a,(hl) ; range propusk
cp #ff
jr nz,tun0
inc hl
jr tu0
tower_upgrade1 inc hl
inc hl
inc hl
inc hl ; power of this tower
ld a,(tower_upgrade_power)
ld (hl),a
call tower_attr_restore
ld a,(tower_upgrade_flag)
push af
call tower_screen_adr
ld a,h
RRCA
RRCA
RRCA
AND 3
OR #58
LD h,a
ld bc,#21
add hl,bc
or #80
ld d,a
ld e,l
dec (hl)
ex de,hl
dec (hl)
pop af
Так как все башни отображают свой уровень цветом в правом нижнем аттрибуте,
подкрашиваем цвета башни дабы показать её новый уровень.
Сохраняем аттрибуты цвета, проигрываем звук апгрейда, выходим.
call tower_attr_store
xor a
ld (stat_new_tower_flag+1),a
call stat_new_tower
call mouse_store_adr
ld a,create_tower_sound
call AFXPLAY
ld hl,tower_upgraded_count
inc (hl)
jp rmb
При выборе апгрейда башни нужно показать что по чём:
tower_upgrade_view
push bc ; in b:type, c:position, in hl- towers+1
push bc
push hl
ld (tower_upgrade_flag),a
; set on view range and towers
ld (stat_new_tower_flag+1),a
ld (tower_upgrade_range_flag+1),a ; pos
push af
ld a,b
ld (tower_upgrade_range_type+1),a ; type
pop af
push af
call tower_attr_restore
pop af
call tower_attr_store
call clear_menu_tower
ld hl,#5800
ld de,map_color
ld bc,#2c0
ldir
pop hl
pop bc
inc hl
inc hl
inc hl
inc hl ; power of this tower
ld c,(hl)
ld hl,tower_upgrade
tuv0 ld e,(hl) ; price
inc hl
ld a,(hl)
cp b
jr z,tuv2
tuv12 inc hl
ld a,(hl)
cp #ff
jr nz,tuv12
inc hl
jr tuv0
tuv2 inc hl
ld a,(hl)
cp c
jr nz,tuv2
tuv21 ld a,e
ld (tower_upgrade_price),a
inc hl
ld a,(hl) ; in a: new power, e:price
ld (tower_upgrade_power),a
cp #ff
jr nz,tuv24
dec hl ; max upgrade
ld a,(hl)
ld de,upgrade_power_adr
push de
call view_3dec
pop de
dec e
ld a,'F'
call char_print
ld de,upgrade_price_adr
ld a,#2f
call char_print
ld a,#2f
call char_print
ld a,#ff
ld (tower_upgrade_price),a
jr tuv23
отобразили все данные по апгрейду текущей бащни — её новый урон (Force) и цену апгрейда:
tuv24
push de
ld de,upgrade_power_adr
push de
call view_3dec
pop de
dec e
ld a,'F'
call char_print
pop de
ld a,e
ld de,upgrade_price_adr
call view_dec_energy
tuv23 push hl
push de
push bc
call clear_status_attr
ld hl,upgrade_price_adr-1
call money_view
ld a,#44
ld hl,#5ac2
ld de,#dac2
call fill_colors
ld a,#42
ld l,#e2
ld e,l
call fill_colors
pop bc
pop de
pop hl
pop bc
ld a,b
ld (stat_upgrade_flag+1),a
ld a,(tower_upgrade_price)
ld c,a
ld a,(money)
sub c
jr c,$+3
xor a
ld (st_color+1),a
при нехватке финансов отображаем недоступность покупки, выделяя башню в меню серым цветом. либоб всё нормально, включаем отображение её охвата:
tuv30
ld c,b ; вывод типа башни
ld hl,#50c0
call view_tower_instatus
ld (st_clr1+1),hl
ex de,hl
ld (st_clr2+1),hl
jp st_color
stat_upgrade ld a,(st_color+1)
ld e,a
ld a,(tower_upgrade_price)
ld c,a
ld a,(money)
sub c
jr c,$+3
xor a
ld (st_color+1),a
cp e
ret z
xor a
ld (tower_upgrade_range+1),a
jr tuv30
stat_new_towers ld a,(st_color+1)
ld e,a
ld a,(tower_upgrade_price)
ld c,a
ld a,(money)
sub c
jr c,$+3
xor a
ld (st_color+1),a
cp e
ret z
jr tuv30
башню здесь не поставить. отрубаем всё:
tower_not_set ld a,cancel_sound
jp AFXPLAY
Ставим башню на новую позицию. Проверим, есть ли на этом месте башня?
set_new_tower
ld hl,towers ; поиск башни по позиции
snt01 ld a,(hl)
cp #fe
jr z,begin_set_new_tower
ld b,a ; type
inc hl
ld a,(hl) ; position
cp c
jp z,rmb2
snt0 inc hl
ld a,(hl) ; range propusk
cp #ff
jr nz,snt0
inc hl
jr snt01
если есть, выходим, если достигли конца списка башен — монтажники, сюда!
монтажники первым делом смотрят на карту и думают, а не путь ли здесь крипов лежит? если нет, в работу. если да — водку пить дальше.
begin_set_new_tower
ld a,c
ld hl,(map)
add l
ld l,a
ld a,(hl)
cp #ff
jp nz,rmb2 ; #ff - можно ставить башни
push hl
ld a,c
ld (new_tower_pos+1),a
ld a,(tower_preview_range_flag+1)
sub #b0
rra
ld (cmp_towers0+1),a
смотрим на выбранную в меню башню, получаем её номер, и по нему — цену.
вдруг денег не хватит?
; sub #b0
ld c,a
ld b,0
ld hl,tower_price
add hl,bc
ld c,(hl)
pop hl
ld a,(money)
sub c
jr c,low_money
ld (money),a
ld (hl),#fe
ld de,money_adr
call view_dec_energy
call tower_preview_range_restore
cmp_towers0 ld c,0
; sub #b0 ; первая башня
; type
new_tower_pos ld b,0 ; pos
ld hl,towers_fire_rate
ld e,c
ld d,0
add hl,de
ld a,(hl) ; fire rate
; ; create tower, b - pos:#21, c - type: 0
да, башня будет! все её стартовые данные получены. начинаем монтаж:
call tower_create
ld a,(new_tower_pos+1)
call tower_attr_store
ld a,create_tower_sound
call AFXPLAY
jr rmb
монтаж закончен, играем по этому поводу ТУШ. tower_create разбирали в прошлой статье.
если денег таки не хватило, это дело нужно подсказать подсветкой индикатора финансов на экране на некоторое время и блямкнуть звуком — нифига, зарабатывай, копи, потом придёшь :)
Captain Drexx download page
3 комментария
Это связано с тем, что здесь описывается такая трудная для понимания вещь как пользовательский интерфейс.
Оказалось, что простой интерфейс как «выбрал — поставил» выглядит настолько дубовым, древним и неюзабельным, что эту часть кода игры я переписывал два раза.
Из-за того, что первоначальный вариант, в котором нужно было сначала ткнуть на место где хочешь поставить башню, а потом выбрать — какую, оказался далеко не таким понятным для игрока как мне казалось, и первые тестирования это показало: из троих игроков двое мне заявили, что интерфейс — не тот. Не привычный, не такой как должен быть.
Надо «drag'n'drop».
Да, ушло много времени на переделку. Но!
В процессе переделки добавилось много того, что оживляет игру — это различные состояния игры, которые позволяют, например, визуально оценить выбор игрока при принятии решений; подсказать когда у него не хватает финансов / каких либо возможностей; были расширено управление — введением дополнительных быстрых клавиш для установки / апгрейда башен, пауза для размышлением над стратегией и т.д., что в целом уже придаёт игре законченный, понятный и привлекательный вид.
По сему я хочу сказать, что UI — это невероятно важно. Это дизайн, и его результат прямо сказывается на восприятии игры, а поиграв даже немного — человек начинает себя чувствовать удобно в игре. А это очень важно.
А то что ААА не вкурил игру — я ни при чём :))