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 комментария

avatar
Должен заметить, что эта статья — самая сложная для понимания.
Это связано с тем, что здесь описывается такая трудная для понимания вещь как пользовательский интерфейс.

Оказалось, что простой интерфейс как «выбрал — поставил» выглядит настолько дубовым, древним и неюзабельным, что эту часть кода игры я переписывал два раза.
Из-за того, что первоначальный вариант, в котором нужно было сначала ткнуть на место где хочешь поставить башню, а потом выбрать — какую, оказался далеко не таким понятным для игрока как мне казалось, и первые тестирования это показало: из троих игроков двое мне заявили, что интерфейс — не тот. Не привычный, не такой как должен быть.

Надо «drag'n'drop».
Да, ушло много времени на переделку. Но!
В процессе переделки добавилось много того, что оживляет игру — это различные состояния игры, которые позволяют, например, визуально оценить выбор игрока при принятии решений; подсказать когда у него не хватает финансов / каких либо возможностей; были расширено управление — введением дополнительных быстрых клавиш для установки / апгрейда башен, пауза для размышлением над стратегией и т.д., что в целом уже придаёт игре законченный, понятный и привлекательный вид.

По сему я хочу сказать, что UI — это невероятно важно. Это дизайн, и его результат прямо сказывается на восприятии игры, а поиграв даже немного — человек начинает себя чувствовать удобно в игре. А это очень важно.

А то что ААА не вкурил игру — я ни при чём :))
  • VBI
  • +4
avatar
Во первых я не вкурил игру потому что мой уровень это Assain или Farcry, самая трудная Thief, в стратегии я не играю. Во вторых ААА в принципе на спектруме игры использует как источник графики и идей для дем.
avatar
трудная для понимания — потому что интерфейс должен быть понятен интуитивно.
  • VBI
  • +2
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.