- 這裏分享一下我的彙編語言課程設計,貪喫蛇遊戲
- 程序使用的資源不超過8086,可以用nasm編譯成.com文件,運行在DOSBox環境中
文章目錄
一、簡介
1. 遊戲規則
- wasd控制轉向
- r重啓遊戲
- esc退出遊戲
- p暫停遊戲
- F1~F10控制蛇的十級變速
注:wasd下文統稱方向鍵;其他鍵統稱控制鍵
2. 段寄存器安排
- 整體上不分段,
ss
、ds
、es
、cs
處於同一個段 - 設置一個128字節的全局變量作爲堆棧區,
ss
指向這個區域(用於push/pop
指令) ds
、es
都初始化爲cs
的值,使它們指向同一個段ds
主要以超越前綴來訪問屏幕顯示緩衝區,也可以缺省前綴形式訪問全局變量es
主要以超越前綴來訪問全局變量,並且以超越前綴形式訪問中斷向量表
3. 流程圖
-
主程序流程,主要包括以下幾部分:
- 初始化:初始化各寄存器、全局變量、顯示初始遊戲界面、修改中斷向量表等
- 控制鍵處理循環:從按鍵緩衝隊列取值,判斷控制鍵、設置全局標誌和變量等
- 死亡界面循環:顯示死亡界面,判斷r和esc鍵
- 收尾:還原中斷向量表、返回操作系統
-
按鍵中斷
-
定時器中斷
3. 關鍵技術
- 屏幕顯示相關
- 鍵盤控制相關
- 食物隨機生成相關
- 蛇移動刷新相關
二、原理說明
1. 屏幕顯示
- 使用直接寫屏的方式來做界面顯示。在內存中有一個顯示緩存區,可以用訪問普通內存的方法訪問它。這是一片4000字節的連續區域,存儲着25行80列顯示區域中每個點的顯示數據,每個點數據長兩個字節,低字節是顯示字符的ASCII碼;高字節是底色和字的顏色,格式如下
- 顯卡會不斷掃描這個區域,並把寫入其中的數據立刻顯示到屏幕上。注意,在這個區域寫入的數據,在刷新到屏幕上後不會消失,也就是說:任意時刻,只要讀取屏幕顯示區,就能得知屏幕上任意一點顯示的字符,這對我們後續遊戲邏輯有着重要作用
- 顯示區域的起始地是0B800H,第m行的初始位置的偏移地址爲
80*m*2
,低m行n列位置對應的存儲單元偏移是(80*m+n)*2
,示意圖如下:
- 爲了統一和方便起見,在程序中讀寫屏上點(x,y)時,統一把ds指向x行起始,用bx指示y對應的偏移,用byte [ds:bx] 和byte [ds:bx+1]取得數據位置
2. 顯示刷新
(1)定時器中斷
- 貪喫蛇是不斷在運動的,如何讓屏幕顯示的畫面動起來呢?我的方法是利用系統定時器來刷新顯示。在系統加電初始化期間,系統定時器被設定爲每55毫秒發出一次中斷請求,cpu的int 8中斷響應它後,會調用一個“接口中斷”1ch。這個中斷每秒被觸發18.2次,但預置的中斷服務函數沒有做任何事,換句話說,1ch中斷就是一個給開發者預留的接口,方面開發者實現某些週期性工作,而我們正好可以利用它。
- 在中斷服務函數中做一個計數器,每次進中斷都減計數,在計數器位爲0時進行計數值重裝載和功能函數調用,這樣我們就得到了一個可以自由控制週期長度的週期性觸發函數。如果要暫停也很簡單,設置一個暫停標誌跳過減計數即可。這也是單片機定時器中斷使用過程中很常用的方法了,可以把一個週期信號變成多個。我們可以在這個函數中進行蛇顯示位置的刷新,這樣就能通過控制計數器裝在值來控制蛇的前進速度。
(2)蛇刷新邏輯
-
先說明一下游戲顯示方式:蛇頭用
亮紫色 *
表示,蛇身用淺紫色 *
表示,食物用淺藍閃爍 #
表示,遊戲邊框用*
表示,如下圖所示 -
蛇每一幀的移動,在視覺上起始只有蛇頭和蛇尾在移動,我們只要存一下當前時刻蛇頭的位置&前進方向 + 蛇尾的位置&前進方向就行了。每輪刷新時,只刷新顯示新的蛇頭,並把舊尾消除,蛇身其他部分不動。先確定頭尾的前進方向,用此結合當前的位置得到更新的位置,判斷更新的位置會不會死,不死就顯示新的蛇頭出來;尾巴也差不多,先在原位置顯示一個空格把舊尾刷掉,再修改蛇尾位置數據即可。如果蛇頭喫到食物的話,這一輪頭動尾不動就行了。蛇頭蛇尾存儲結構示意如下:
%define getXY(x,y) (x<<8) | y ;宏函數,屏幕上座標(x,y)拼成一個字,xy必須是常數
snake_head dw getXY(7,8), 'd' ;存儲蛇頭位置和下一格移動方向(hdir),缺省爲(7 8,'d')
snake_tail dw getXY(5,8), 'd' ;存儲蛇尾位置和下一格移動方向(tdir),缺省爲(5 8,'d')
- 因爲先移動蛇頭,再移動蛇尾,要注意一下蛇頭和蛇尾恰好擦過的情況。 如果蛇頭撞到了非
#
或空格
的點,而且現在蛇頭和蛇尾不重合,那要麼是撞到蛇身,要麼是撞牆,這樣就可以做死亡判斷 - 還有一個比較麻煩的地方在於:顯示刷新頻率、方向鍵操作頻率(wasd)、控制鍵操作頻率(r p esc…)不同,如果在兩次刷新過程中進行了多次方向操作,這些操作不應被忽略,而是應在每次刷新顯示時讀一個出來。但是控制鍵(調速、重啓、暫停等)要求立刻有反應,所以他們不能和方向鍵一起存。還有,方向鍵是直接操作蛇頭方向,間接操作蛇尾的方向的,蛇尾方向變化頻率比蛇頭更慢,要一直走到拐點才能變,所以蛇尾的轉向數據也要單獨存,爲了得到轉彎的位置,蛇尾轉向數據還要結合轉向點座標一起存纔可以。
- 我借鑑了按鍵緩衝區(下文提到)的思路,對於蛇頭蛇尾的轉向數據開了兩個循環隊列,控制鍵數據存在按鍵緩衝隊列、方向鍵數據存入蛇頭方向隊列,在刷新到蛇頭轉向點的時候,轉向方向結合轉向位置一起存入蛇尾方向隊列,這樣就解決了問題。這是整個遊戲裏最麻煩的也最核心的部分了。循環隊列結構如下:
hdir_buff times 20 dw 0 ;蛇頭方向(hdir)按鍵緩衝區(循環隊列),每個字數據:高字節是scan code,低字節是ascII(僅wasd四種)
hdir_head dw hdir_buff
hdir_tail dw hdir_buff
tdir_buff times 80 dw 0 ;蛇尾方向(tdir)按鍵緩衝區(循環隊列),每個雙字數據:高字是轉向位置,低節是ascII(僅wasd四種),因爲蛇尾總是更晚進行轉向,這個要開長一點
tdir_head dw tdir_buff
tdir_tail dw tdir_buff
- 三個循環隊列整理如下
循環隊列名 | 隊列元素 | 入隊時機 | 出隊時機 | 說明 |
---|---|---|---|---|
按鍵緩衝隊列 | 每個元素長一個字,高字節是鍵的掃描碼,低字節是ascII碼(鍵包括:r / p / esc / F1~F10) | int 9中斷服務函數 | 主函數中的while循環 | 改寫了bios的int 9中斷服務函數,但緩衝區仍是bios中原有的 |
蛇頭方向循環隊列 | 同上(鍵包括:w / a / s / d) | int 9中斷服務函數 | 蛇刷新中斷服務函數中調用的蛇頭刷新函數 | 完全模仿按鍵緩衝隊列實現,在數據段自定義緩衝區 |
蛇尾方向循環隊列 | 每個元素長一個雙字,高字是蛇頭轉向位置(高字節x,低字節y);低字是蛇頭轉向的方向鍵(高字節空,低字節ascII碼)(鍵包括:w / a / s / d) | 蛇刷新中斷服務函數中調用的蛇頭刷新函數 | 蛇刷新中斷服務函數中調用的蛇尾刷新函數 | 在數據段自定義緩衝區 |
- 蛇頭刷新函數說明
- 蛇尾刷新函數說明
3. 鍵盤控制
- 鍵盤控制涉及到兩個中斷,一是
int 9h外部中斷
,這是一個可屏蔽的外部中斷,只要鍵盤有輸入,中斷信號就通過8259中斷控制芯片傳入cpu,如果中斷允許標誌IF置1,就會響應中斷信號,執行bios中預置的中斷服務函數。在中斷函數中,觸發鍵盤的按鍵掃描碼被讀入al寄存器中,並被存儲到鍵盤緩衝區。鍵盤緩衝區是一個循環隊列,由一個長16個字的buff和兩個字指針組成。這個數據結構位於bios數據區,buff地址爲0040:001AH
~0040:001CH
,頭指針地址0040:001Ah
,尾指針地址0040:001Ch
。由於循環隊判空的方法是尾指針在頭指針前一個位置,所以這個緩衝最多存15個鍵數據。每個鍵數據佔一個字,高字節是鍵的掃描碼,低字節是ascII碼(掃描碼和按鍵對應關係請看:按鍵掃描碼) - 另一個相關中斷是
int 16h軟中斷
,這是一個不可屏蔽的指令中斷,由用戶調用觸發。它有三個功能,常用的是0號功能,即從按鍵緩衝區出隊一個鍵數據到ax中,如果隊列爲空,就會阻塞等待;我的程序中也用到了1號功能,可以用來查詢鍵盤緩衝區,對鍵盤掃描但不等待,並設置ZF標誌。(關於16h中斷詳見:鍵盤I/O中斷調用(INT 16H))下圖顯示了int 9h、int 16h和按鍵緩衝的關係
- 預置的int9服務函數會處理鍵盤上所有的鍵,把他們一視同仁地放入緩衝隊列,但這不方便我們對按鍵操作進行靈活處理,而且我們也不需要檢查那麼多鍵,所以我重新自定義了9號中斷的服務函數。本質上就是修改了中斷向量表,把9號中斷服務函數的入口改成我自定義的服務函數,從而實現了自定義。
- 按照上面的分析,我在自定義的中斷服務函數中把設置鍵存入按鍵緩衝隊列,把方向控制鍵存入蛇頭隊列
- 在主函數循環中從按鍵隊列取數,在蛇刷新中斷中從蛇頭(蛇尾)隊列取數。(寫到這裏,突然感覺設置鍵也可以不存,而是在int9讀取時直接處理,這樣還能簡化一點)
4. 食物隨機生成
- 每喫到一個食物,都要隨機生成一個新食物。這是一個隨機過程,需要隨機數。但是彙編中沒有自帶的隨機函數,所有我們必須要自己來生成僞隨機數。因爲有一定的單片機開發經驗,我自然地想到了單片機中隨機數的生成方法,最簡單的是利用自然的噪聲,比如做模數轉換時,取幾個超過精度範圍的位作爲隨機數;另一種方法是利用系統時鐘,尤其是小數點後好幾位的那種,只要採樣週期不是特別一致,基本都挺隨機的。
- 受硬件限制,沒法做模數轉換,而在食物隨機生成這個情境中,顯然週期是很不一定的,所以我考慮使用實時鐘來實現這個功能。經查資料,我發現的實時鐘最精確採樣能到百分之一秒,用
int 21h
中斷的2ch
功能,返回的CH:CL=時:分
,DH:DL=秒:1/100秒
,基本滿足要求。 - 只有一個百分之一秒還是不太隨機,需要結合秒的數據一起處理,我的方法是:設百分之一秒值爲y,秒的個位值x,如果x是奇數,僞隨機數=
(x+1)*y
;如果是偶數,僞隨機數=(10-x)*y
,結果看來是挺隨機的。 - 接下來對僞隨機數取兩個餘數就能得到隨機食物的位置了,這裏還要專門判斷一下隨機食物的位置是不是隨機到蛇身上了,我本來的想法是如果隨機到蛇身體就重新取隨機數的,但是操作了幾次發現,如果取得太快,好像返回的實時鐘值就不變了,所以改成了類似哈希的線性散列法:一直往右找直到找到合法位置,當然這樣要注意下換行問題。這個方法的好處在於效率比重新隨機高,尤其是在蛇很長的時候更高,但壞處在於食物相對容易出現在靠牆的位置。
三、相關代碼
1. 全局變量
;--------------------------------------數據區---------------------------------------------------------------
stack times 128 db 0 ;常規操作所用的堆棧
data times 4 dw 0 ;(int 9中斷例程ip)、(int 9中斷例程cs)、(int 1ch中斷例程ip)、(int 1ch中斷例程cs)
frame_color db 00001000B ;邊框顏色
info_color db 0 ;提示信息顏色(由邊框顏色確定)
food_color db 10001011b ;食物顏色(閃爍亮藍色)
score dw 0 ;得分
speed db 25,20,16,14,10,8,5,3,2,1 ;速度對應的計數器週期
spd db '6' ;速度選擇
recount db 5 ;定時器重裝載值
count db 1 ;定時器計數值
;hdir看成結構體
hdir_buff times 20 dw 0 ;蛇頭方向(hdir)按鍵緩衝區(循環隊列),每個字數據:高字節是scan code,低字節是ascII(僅wasd四種)
hdir_head dw hdir_buff
hdir_tail dw hdir_buff
;tdir看成結構體
tdir_buff times 80 dw 0 ;蛇尾方向(hdir)按鍵緩衝區(循環隊列),每個雙字數據:高節是轉向位置,低節是ascII(僅wasd四種),因爲蛇尾總是更晚進行轉向,這個要開長一點
tdir_head dw tdir_buff
tdir_tail dw tdir_buff
;snake_head 和snake_tail 看成結構體
snake_head dw getXY(7,8), 'd' ;存儲蛇頭位置和下一格移動方向(hdir),缺省爲(5 8,'d')
snake_tail dw getXY(5,8), 'd' ;存儲蛇尾位置和下一格移動方向(tdir),缺省爲(5 6,'d')
get_food_flag db 0 ;喫到食物標誌
die_flag db 0 ;死亡標誌
pause_flag db 0 ;暫停標誌
score_str_pos dw getXY(25,10) ;信息位置
replay_str_pos dw getXY(27,11) ;信息位置
exit_str_pos dw getXY(27,12) ;信息位置
score_str db "game over! your score: ", '$' ;遊戲結束顯示信息
replay_str db "press R to replay!",'$' ;遊戲結束顯示信息
exit_str db "press ESC to exit!",'$' ;遊戲結束顯示信息
2. 宏&常數
;----------------------------------------------------------------宏&常數--------------------------------------------------------------------------
%define esc 1bh ;esc按鍵的ascii
snake_color EQU 00000101B ;蛇身顏色
snake_head_color EQU 00001101B ;蛇頭顏色
displayBase EQU 0b800h ;顯示區基地址
PORT_KEY_DAT EQU 0x60 ;鍵盤數據接口
PORT_KEY_STA EQU 0x64 ;鍵盤控制接口
KEYBUFF_DS EQU 0040H ;鍵盤緩衝段地址
KEYBUFF_HEAD EQU 001AH ;鍵盤緩衝頭指針偏移地址(0040:001AH)
KEYBUFF_TAIL EQU 001CH ;鍵盤緩衝尾指針偏移地址(0040:001CH)
KEYBUFF_FIRST EQU 001EH ;鍵盤緩衝首地址 (0040:001EH,左閉)
KEYBUFF_LAST EQU 003EH ;鍵盤緩衝尾地址 (0040:003EH,右閉)
%define HDIRBUFF_DS es ;hdir緩衝段地址
%define HDIRBUFF_HEAD hdir_head ;hdir緩衝頭指針偏移地址
%define HDIRBUFF_TAIL hdir_tail ;hdir緩衝尾指針偏移地址
%define HDIRBUFF_FIRST hdir_buff ;hdir緩衝首地址
%define HDIRBUFF_LAST hdir_buff+2*20 ;hdir緩衝尾地址
%define TDIRBUFF_DS es ;tdir緩衝段地址
%define TDIRBUFF_HEAD tdir_head ;tdir緩衝頭指針偏移地址
%define TDIRBUFF_TAIL tdir_tail ;tdir緩衝尾指針偏移地址
%define TDIRBUFF_FIRST tdir_buff ;tdir緩衝首地址
%define TDIRBUFF_LAST tdir_buff+2*80 ;tdir緩衝尾地址
%define incW(x) inc word [es:x] ;宏函數,字x加1
%define clearW(x) mov [es:x],word 0 ;宏函數,字x清0
%define setW(x,y) mov [es:x],word y ;宏函數,字x設爲y
%define incB(x) inc byte [es:x] ;宏函數,字節x加1
%define decB(x) dec byte [es:x] ;宏函數,字節x加1
%define clearB(x) mov [es:x],byte 0 ;宏函數,字節x清0
%define setB(x,y) mov [es:x],byte y ;宏函數,字節x設爲y
%define notB(x) NOTB x ;宏函數,bool變量x取反(x只能是0或1)
%macro NOTB 1 ;宏函數,bool變量取反
cmp byte [es:%1],0
jnz ISONE
setB(%1,1)
jmp NOTDONE
ISONE:
clearB(%1)
NOTDONE:
%endmacro
%define setDri(x,y) mov [es:x+2],word y ;宏函數,設置x方向爲y(x必須是snake_head或snake_tail)
%define getDir(x) word [es:x+2] ;宏函數,獲取x方向(x必須是snake_head或snake_tail)
%define lineAddr(x) displayBase+x*0ah ;宏函數,獲取屏幕x行首地址,x必須是常數
%define getXY(x,y) (x<<8) | y ;宏函數,屏幕上座標(x,y)拼成一個字,xy必須是常數
%macro showFrames 1 ;宏函數,按參數顏色刷新邊框
mov ax,%1
push ax
call show_all_frames
add sp,2 ;平衡堆棧
%endmacro
%macro setDS 1 ;宏函數,設置ds寄存器爲參數值
push ax
mov ax,%1
mov ds,ax
pop ax
%endmacro
%macro reCount 0 ;宏函數,計數器重裝載
push ax
mov al,[es:recount]
mov [es:count],al
pop ax
%endmacro
%macro setAddrHT 1 ;宏函數,設置ds和bx,使[ds:bx]指向某點,參數必須是snake_head 或 snake_tail 或 某字存儲空間
push ax
push dx
mov bx,[es:%1] ;bh是列號,bl是行號
xor ax,ax ;行號乘0ah找到行首地址偏移
mov al,bl
mov dl,0ah
mul dl
add ax,displayBase ;加上顯示區基地址得到行地址,給ds
mov ds,ax
mov bl,bh
xor bh,bh ;清bh,bx存列號
shl bx,1 ;列號乘2得到點在列中的偏移地址
pop dx
pop ax
%endmacro
%macro printStr 2 ;宏函數,在%2位置打印字符串%1(參數必須是score_str,score_str_pos 或 replay_str,replay_str_pos)
push si ;此函數需要手動保存和恢復ds:bx !
mov si,%1
setAddrHT %2
call show_str
pop si
%endmacro
%macro printDieInfo 0 ;宏函數,打印死亡提示信息
push ds
push bx
printStr score_str, score_str_pos
mov ax,[es:score]
call print_num
printStr replay_str, replay_str_pos
printStr exit_str, exit_str_pos
pop bx
pop ds
%endmacro
3. 主函數
- 對應主程序流程圖
;----------------------------------------------------------------主函數--------------------------------------------------------------------------
segment code
org 100H
;=================設置段寄存器=========================
mov ax,cs ;cs,ds一樣,不分段
mov ds, ax
mov ax,0 ;es指向0,準備更改9號中斷向量表指向自定義的處理函數(如果es值不對,效果上沒啥變化,會調用默認的9號中斷服務程序)
mov es,ax
;=================準備中斷向量表=========================
push word [es:9*4] ;將原來的int 9中斷例程的入口地址保存在data段
pop word [data]
push word [es:9*4+2]
pop word [data+2]
push word [es:1ch*4] ;將原來的int 1ch中斷例程的入口地址保存在data段
pop word [data+4]
push word [es:1ch*4+2]
pop word [data+6]
CLI
mov word [es:9*4], int09h_handler ;在中斷向量表中設置新的int 9中斷例程的入口地址(設置過程關閉中斷,避免被打斷)
mov [es:9*4+2], cs
mov word [es:1ch*4], int1ch_handler ;在中斷向量表中設置新的int 1ch中斷例程的入口地址(設置過程關閉中斷,避免被打斷)
mov [es:1ch*4+2], cs
STI
mov ax,cs
mov es,ax ;程序運行過程中用es來尋址全局變量
mov ax,[es:stack]
mov ss,ax ;ss指向數據區的stack
;================遊戲初始化===============================
game_init:
;初始化全局變量
clearW(score) ;得分
;setB(spd,'6') ;速度選擇(註釋這些,在重啓遊戲時速度會保持原設定)
;setB(recount,5) ;定時器重裝載值
;setB(count,1) ;定時器計數值
setW(snake_head,getXY(7,8)) ;蛇頭信息
setW(snake_head+2,'d')
setW(snake_tail,getXY(5,8)) ;蛇尾信息
setW(snake_tail+2,'d')
clearB(die_flag) ;復位各標誌
clearB(get_food_flag)
clearB(pause_flag)
;每次重啓遊戲,改變邊框和提示信息的顏色
incB(frame_color) ;邊框frame_color前景色+1(00001001~00001111循環)
cmp byte[es:frame_color],00001111b
jbe frame_color_seted
setB(frame_color,00001001b)
frame_color_seted: ;提示信息info_color(00001111~00001001循環)
mov al,24
sub al,[es:frame_color]
mov [es:info_color],al
;初始化窗口
call clear_window
;用frame_color顏色顯示所有邊框
showFrames [es:frame_color]
;初始化蛇
call snake_init
;初始化食物
call draw_food
;初始化循環隊列
call Queue_clear
;=============主循環中進行設置鍵和死亡檢測========================
key_and_death:
cmp byte [es:die_flag],0 ;檢查死亡情況
jnz DIE ;死了,轉移
mov ah, 01h ;查詢按鍵緩衝是否爲空
int 16h
jnz KEYCHEAK ;不空,檢查按鍵
jmp key_and_death ;空,循環檢查死亡情況
KEYCHEAK:
mov ah,0 ;取一個鍵
int 16h
R: ;r鍵,用來重啓遊戲
cmp al,'r'
jnz P
jmp game_init
P:
cmp al,'p'
jnz SPD
notB(pause_flag)
jmp key_and_death
SPD: ;F1~F10,速度設置
cmp al,'0'
jb ESC
cmp al,'9'
ja ESC
;刷新顯示速度
setB(spd,al)
call show_score_and_spd
;更新recount值
sub al,'0'
xor bx,bx
mov bl,al
mov al,[es:speed+bx]
setB(recount,al)
jmp key_and_death
ESC:
cmp al,esc ;esc鍵,用來退出遊戲
jz program_end
;showFrames 02h
jmp key_and_death
;=========================死亡界面=========================
DIE:
call clear_window ;刷新窗口
showFrames [es:frame_color]
printDieInfo ;打印提示信息
restart_or_exit:
mov ah,0 ;取一個鍵
int 16h
restart_cheak: ;r鍵,用來重啓遊戲
cmp al,'r'
jnz exit_cheak
jmp game_init
exit_cheak:
cmp al,esc ;esc鍵,用來退出戶遊戲
jz program_end
jmp restart_cheak
;=========================程序出口=========================
program_end:
;恢復int 9和int 1ch中斷例程
mov ax,cs
mov ds,ax
mov ax,0
mov es,ax
push word [data]
pop word [es:9*4]
push word [data+2]
pop word [es:9*4+2]
push word [data+4]
pop word [es:1ch*4]
push word [data+6]
pop word [es:1ch*4+2]
;返回dos
mov ax,4c00h
int 21h
4. 定時器相關
- 對應定時器中斷流程圖
;----------------------------------------------------------------時鐘相關-------------------------------------------------------------------------
;功能:自定義的1ch號定時器中斷
;參數:無
;返回:無
int1ch_handler:
cmp byte [es:die_flag],0 ;死了,不要刷新蛇(不能把暫停放在前面,否則暫停的時候就不能退出或者重啓了)
jnz RETURN
cmp byte [es:pause_flag],0 ;暫停狀態,不刷新
jnz RETURN
dec byte [es:count]
JZ FLUSH ;計數減到0時刷新蛇位置
jmp RETURN
FLUSH:
reCount ;重裝載計數器
call snake_head_flush ;刷新蛇頭
cmp byte[es:get_food_flag],1 ;喫到食物,這一輪蛇尾不要動
jz RETURN
call snake_tail_flush ;沒喫到,刷新蛇尾
RETURN:
mov al,20h ;通知中斷控制器8259A
out 20h,al ;當前中斷處理已經結束
iret ;中斷返回
5. 蛇顯示相關
;----------------------------------------------------------------蛇顯示相關--------------------------------------------------------------------------
;功能:初始化顯示蛇
;參數:無
;返回:無
snake_init:
push ax
push bx
push dx
push cx
setAddrHT snake_head ;使[ds:bx]指向蛇頭點
mov ah,snake_head_color ;畫蛇頭
mov al,2ah ;*號
mov [ds:bx],ax
sub bx,2
mov ah,snake_color ;從蛇頭開始向左畫2格,代表初始蛇
mov cx,2
PRINTSNAKE:
mov [ds:bx],ax
sub bx,2
loop PRINTSNAKE
pop cx
pop dx
pop bx
pop ax
ret
;功能:刷新顯示蛇頭(根據HDIRBUFF移動蛇頭,畫*)
;參數:無
;返回:設置全局變量get_food_flag和die_flag
snake_head_flush:
push ax
push bx
push ds
mov si,[es:HDIRBUFF_HEAD] ;如果hdir緩衝爲空,不用改snake_head
cmp si,[es:HDIRBUFF_TAIL]
jz DIRSETTED_H
mov si,[es:HDIRBUFF_HEAD] ;如果hdir緩衝非空,取隊首的鍵數據
mov ax,[es:si] ;這個es前綴千萬別忘了(bp/sp缺省ss,其他缺省ds)
;==========判斷當前操作是否合法(不能掉頭)重設snake_head=============
GET_W:
cmp al,'w'
jnz GET_A
cmp getDir(snake_head),'s' ;不能直接調頭
jz POP_hdir
jmp RESET_HEAD
GET_A:
cmp al,'a'
jnz GET_S
cmp getDir(snake_head),'d' ;不能直接調頭
jz POP_hdir
jmp RESET_HEAD
GET_S:
cmp al,'s'
jnz GET_D
cmp getDir(snake_head),'w' ;不能直接調頭
jz POP_hdir
jmp RESET_HEAD
GET_D:
cmp getDir(snake_head),'a' ;不能直接調頭
jz POP_hdir
RESET_HEAD:
xor ah,ah
setDri(snake_head,ax) ;重設蛇頭方向
mov dx,[es:snake_head] ;此刻snake_head高字存儲轉向點,ax存儲將要轉向的方向,在此對TDIRBUFF進行入隊
call Enqueue_tdir
POP_hdir:
call Dequeue_hdir ;隊首元素出隊
;=======至此snake_head中方向數據已經更新或保持,下面找到新的蛇頭位置=================
DIRSETTED_H:
mov dx,[es:snake_head] ;取得蛇頭數據中“點位置”部分數據,根據前進方向進行修改
MOVE_W:
cmp getDir(snake_head),'w'
jnz MOVE_A
dec dl
jmp MOVE_DONE
MOVE_A:
cmp getDir(snake_head),'a'
jnz MOVE_S
dec dh
jmp MOVE_DONE
MOVE_S:
cmp getDir(snake_head),'s'
jnz MOVE_D
inc dl
jmp MOVE_DONE
MOVE_D:
inc dh
;=======至此dx已經存儲了新蛇頭位置,更新snake_head畫新蛇頭,並進行新食物生成=================
MOVE_DONE:
clearB(get_food_flag) ;復位get_food_flag
setAddrHT snake_head ;修改原蛇頭顏色爲蛇身顏色
mov ah,snake_color ;顏色
mov [ds:bx+1],ah
mov [es:snake_head],dx ;更新snake_head
setAddrHT snake_head ;設置ds:bx爲蛇頭點地址
cmp byte[ds:bx],'#' ;如果蛇頭撞到了非#或空格的點,而且現在蛇頭和蛇尾不重合,那要麼是撞到蛇身,要麼是撞牆,設die標誌
jz DIECHEAKED
cmp byte [ds:bx],' '
jz DIECHEAKED
mov ax,[es:snake_head]
cmp ax,[es:snake_tail]
jz DIECHEAKED
setB(die_flag,1)
DIECHEAKED:
cmp byte [ds:bx],'#' ;比較蛇頭位置和當前食物位置
jnz DRAWHEAD ;如果沒喫到,轉DRAWHEAD
setB(get_food_flag,1) ;喫到了,設標誌get_food_flag
call draw_food ;畫新食物
add word[es:score],10 ;得分增加
call show_score_and_spd ;刷新得分顯示
DRAWHEAD:
mov ah,snake_head_color ;顏色
mov al,2ah ;*號
mov [ds:bx],ax ;畫新蛇頭
pop ds
pop bx
pop ax
ret
;功能:刷新顯示蛇尾(先畫空格刷掉蛇身,再根據TDIRBUFF移動蛇尾)
;參數:無
;返回:無
snake_tail_flush:
push ax
push bx
push ds ;清原蛇尾點
setAddrHT snake_tail ;設置ds:bx爲蛇尾點地址
mov ah,1
mov al,' ' ;顯示一個空格
mov [ds:bx],ax
pop ds
mov si,[es:TDIRBUFF_HEAD] ;如果tdir緩衝爲空,不用改snake_tail
cmp si,[es:TDIRBUFF_TAIL]
jz NOTURN
mov si,[es:TDIRBUFF_HEAD] ;如果tdir緩衝非空,取隊首的鍵數據到BX:AX
mov bx,[es:si] ;這個es前綴千萬別忘了(bp/sp缺省ss,其他缺省ds)
mov ax,[es:si+2]
cmp bx,[es:snake_tail] ;判斷snake_tail中位置(高字),如果蛇尾到達轉向點了,就修改snake_tail中的方向參數(低字)
jnz NOTURN
setDri(snake_tail,ax) ;修改蛇尾
call Dequeue_tdir ;出隊
NOTURN:
mov bx,[es:snake_tail] ;取得蛇尾數據中“點位置”部分數據,根據前進方向進行修改
MOVE_W_T:
cmp getDir(snake_tail),'w'
jnz MOVE_A_T
dec bl
jmp MOVE_DONE_T
MOVE_A_T:
cmp getDir(snake_tail),'a'
jnz MOVE_S_T
dec bh
jmp MOVE_DONE_T
MOVE_S_T:
cmp getDir(snake_tail),'s'
jnz MOVE_D_T
inc bl
jmp MOVE_DONE_T
MOVE_D_T:
inc bh
MOVE_DONE_T:
mov [es:snake_tail],bx ;更新snake_tail
pop bx
pop ax
ret
6. 鍵盤輸入相關
;----------------------------------------------------------------鍵盤輸入相關--------------------------------------------------------------------------
;功能:自定義的9號鍵盤中斷
;參數:無
;返回:無
int09h_handler:
pusha ;保護通用reg
mov al,0adh
out PORT_KEY_STA,al ;禁止鍵盤發送數據到接口(準備接受鍵盤發送到接口的數據)
in al, PORT_KEY_DAT ;讀取鍵盤發來的按鍵掃描碼
sti ;開中斷
call int09h_fun ;完成相關功能
cli ;關中斷
mov al,0aeh ;允許發送數據到接口
out PORT_KEY_STA,al
mov al,20h ;通知中斷控制器8259A
out 20h,al ;當前中斷處理已經結束
popa ;恢復通用reg
iret ;中斷返回
;功能:自定義的9號鍵盤中斷功能函數
;參數:無
;返回:無
int09h_fun:
CHEAK_ESC:
cmp al, 81h ;esc鬆開
jnz CHEAK_R
mov ah,al
mov al,esc
jmp Save2keyBuff
CHEAK_R:
cmp al,13h ;r按下
jnz CHEAK_P
mov ah,al
mov al,'r'
jmp Save2keyBuff
CHEAK_P:
;如果是死亡狀態,只檢測 r/esc(避免入隊多餘的鍵)
cmp byte [es:die_flag],0
jnz int9_DONE
cmp al,19h ;p按下
jnz CHEAK_W
mov ah,[es:pause_flag] ;注意這裏,確保每一次按下P後存入keyBuff的ax都不同,否則不能入隊(這樣處理可以避免按住p時重複入隊)
mov al,'p'
jmp Save2keyBuff
CHEAK_W:
;如果是暫停狀態,只檢測 r/esc/p(避免入隊多餘的鍵)
cmp byte [es:pause_flag],0
jnz int9_DONE
cmp al,11h ;w按下
jnz CHEAK_A
mov ah,al
mov al,'w'
jmp Save2dirBuff
CHEAK_A:
cmp al,1eh ;a按下
jnz CHEAK_S
mov ah,al
mov al,'a'
jmp Save2dirBuff
CHEAK_S:
cmp al,1fh ;s按下
jnz CHEAK_D
mov ah,al
mov al,'s'
jmp Save2dirBuff
CHEAK_D:
cmp al,20h ;d按下
jnz CHEAK_SPD
mov ah,al
mov al,'d'
jmp Save2dirBuff
CHEAK_SPD: ;F1~F10 (鍵值'0'~'9')
cmp al,3bh
jb int9_DONE
cmp al,44h
ja int9_DONE
mov ah,al
sub al,3bh ;從scan code轉ascII
add al,'0'
jmp Save2keyBuff
Save2dirBuff:
call Enqueue_hdir ;存入hdir緩衝,每個字數據高字節是掃描碼,低字節是ascII
jmp int9_DONE
Save2keyBuff:
call Enqueue_key
int9_DONE:
ret
;功能:把設置按鍵數據存入環形隊列緩衝區
;參數:ax
;返回:無
Enqueue_key:
push ds
push bx
setDS KEYBUFF_DS ;緩衝區段地址
mov bx,[KEYBUFF_TAIL] ;取隊列的尾指針
mov si,bx ;si=隊列尾指針
add si,2 ;si指向下一個可能的位置
cmp si,KEYBUFF_LAST ;越出緩衝區嗎?
jb EN_OK1 ;沒有越界,轉移
mov si,KEYBUFF_FIRST ;越界了,循環到緩衝區頭部
EN_OK1:
cmp si,[KEYBUFF_HEAD] ;和隊列頭指針比較
jz EnqueueDONE1 ;相等表示緩衝已滿,不再存儲數據
cmp [bx-2],ax
jz EnqueueDONE_h ;如果按鍵和上次一樣,也不存了,這樣可以避免按住不放時重複存入按鍵,操作更靈敏
mov [bx],ax ;按鍵數據存入隊列
mov [KEYBUFF_TAIL],si ;保存隊列尾指針
EnqueueDONE1:
pop bx
pop ds
ret
;功能:把hdir按鍵數據存入環形隊列緩衝區
;參數:ax存儲要保存的數據
;返回:無
Enqueue_hdir:
push ds
push bx
setDS HDIRBUFF_DS ;緩衝區段地址
mov bx,[HDIRBUFF_TAIL] ;取隊列的尾指針
mov si,bx ;si=隊列尾指針
add si,2 ;si指向下一個可能的位置
cmp si,HDIRBUFF_LAST ;越出緩衝區嗎?
jb EN_OK_h ;沒有越界,轉移
mov si,HDIRBUFF_FIRST ;越界了,循環到緩衝區頭部
EN_OK_h:
cmp si,[HDIRBUFF_HEAD] ;和隊列頭指針比較
jz EnqueueDONE_h ;相等表示緩衝已滿,不再存儲數據
cmp [bx-2],ax
jz EnqueueDONE_h ;如果按鍵和上次一樣,也不存了,這樣可以避免按住不放時重複存入按鍵,操作更靈敏
mov [bx],ax ;按鍵數據存入隊列
mov [HDIRBUFF_TAIL],si ;保存隊列尾指針
EnqueueDONE_h:
pop bx
pop ds
ret
;功能:hdir環形隊列出隊一個(隊首指針後移)
;參數:無
;返回:無
Dequeue_hdir:
push ds
setDS HDIRBUFF_DS
mov si,[HDIRBUFF_HEAD]
add si,2
cmp si,HDIRBUFF_LAST
jb .LABh
mov si,HDIRBUFF_FIRST
.LABh:
mov [HDIRBUFF_HEAD],si
pop ds
ret
;功能:把tdir按鍵數據存入環形隊列緩衝區
;參數: DX:AX存儲要保存的數據
;返回:無
Enqueue_tdir:
push ds
push bx
setDS TDIRBUFF_DS ;緩衝區段地址
mov bx,[TDIRBUFF_TAIL] ;取隊列的尾指針
mov si,bx ;si=隊列尾指針
add si,4 ;si指向下一個可能的位置
cmp si,TDIRBUFF_LAST ;越出緩衝區嗎?
jb EN_OK_t ;沒有越界,轉移
mov si,TDIRBUFF_FIRST ;越界了,循環到緩衝區頭部
EN_OK_t:
cmp si,[TDIRBUFF_HEAD] ;和隊列頭指針比較
jz EnqueueDONE_t ;相等表示緩衝已滿,不再存儲數據
mov [bx],dx ;按鍵數據存入隊列
mov [bx+2],ax
mov [TDIRBUFF_TAIL],si ;保存隊列尾指針
EnqueueDONE_t:
pop bx
pop ds
ret
;功能:tdir環形隊列出隊一個(隊首指針後移)
;參數:無
;返回:無
Dequeue_tdir:
push ds
setDS TDIRBUFF_DS
mov si,[TDIRBUFF_HEAD]
add si,4
cmp si,TDIRBUFF_LAST
jb .LABt
mov si,TDIRBUFF_FIRST
.LABt:
mov [TDIRBUFF_HEAD],si
pop ds
ret
;功能:清空所有循環隊列
;參數:無
;返回:無
Queue_clear:
push ax
push es
mov ax,KEYBUFF_DS
mov es,ax
mov ax,KEYBUFF_FIRST
mov [es:KEYBUFF_HEAD],ax
mov [es:KEYBUFF_TAIL],ax
pop es
mov ax,HDIRBUFF_FIRST
mov [es:HDIRBUFF_HEAD],ax
mov [es:HDIRBUFF_TAIL],ax
mov ax,TDIRBUFF_FIRST
mov [es:TDIRBUFF_HEAD],ax
mov [es:TDIRBUFF_TAIL],ax
pop ax
ret
7. 邊框顯示相關
;功能:初始化窗口(清屏)------------------------------------------------------
;說明:雙重循環,在25x80的屏幕緩衝區中全寫空格
;參數:無
;返回:無
clear_window:
push ax
push ds
push cx
push bx
setDS lineAddr(0) ;ds指向第0行起始
mov ah,00000111B ;屏幕所有點清成空格
mov al,' '
mov cx,25 ;0~24行(最後一次循環指針已經指向25行了,但沒賦值)
CLEARWINOW:
push cx ;保護外層循環計數
mov cx,80 ;0~79列
mov bx,0
CLEARLINE:
mov word [ds:bx],ax
add bx,2 ;下一列
loop CLEARLINE
pop cx ;恢復外層循環計數
mov bx,ds ;轉向下一行
add bx,0ah
mov ds,bx
loop CLEARWINOW
pop bx
pop cx
pop ds
pop ax
ret
;功能:顯示所有邊框------------------------------------------------------
;說明:第1行和第24行全顯示*號,其他行只有第0列和第79列顯示*號
;參數: 堆棧一個字低字節存顏色
;返回: 無
show_all_frames:
push bp
mov bp,sp
push ds
push bx
push cx
push ax
;顯示橫向邊框
setDS lineAddr(0) ;ds指向第0行起始
mov bx,0 ;偏移初始化爲0
mov ah,[bp+4] ;顏色
mov al,2ah ;*號
mov cx,80 ;ds開始80個字(正好一行)填入邊框字符
show_up_frame:
mov [ds:bx],ax
add bx,2
loop show_up_frame
setDS lineAddr(24) ;ds指向第24行起始
mov bx,0 ;偏移初始化爲0
mov cx,80 ;ds開始80個字(正好一行)填入邊框字符
show_down_frame:
mov ah,[bp+4] ;顏色
mov al,2ah ;*號
mov [ds:bx],ax
add bx,2
loop show_down_frame
;顯示縱向邊框
setDS lineAddr(1) ;ds指向第1行起始
mov cx,23 ;1~23行(最後一次循環指針已經指向24行了,但沒賦值)
mov ah,[bp+4] ;顏色
mov al,2ah ;*號
show_lengthwise_frame:
mov bx,0 ;最左邊的*
mov [ds:bx],ax
add bx,160-2 ;最右邊的*
mov [ds:bx],ax
mov bx,ds ;轉向下一行
add bx,0ah
mov ds,bx
loop show_lengthwise_frame
call show_score_and_spd ;顯示得分
pop ax
pop cx
pop bx
pop ds
pop bp
ret
;功能:顯示得分和當前設置的速度------------------------------------------------------
;說明:在第0行中間顯示得分,第24行中間顯示速度。先把相關位置清爲'-'符號,再顯示得分和速度
;參數: 數據段score,spd
;返回: 無
show_score_and_spd:
push bp
mov bp,sp
push ds
push ax
push bx
push cx
push dx
;清空score顯示位置
setDS lineAddr(0)
mov bx,68
mov ah,[es:info_color] ;顏色
mov al,'-' ;清成'-'(由於判斷撞牆的機制,不可以清成空格)
mov cx,7 ;清7格
CLEARSCORE:
mov [ds:bx],ax
add bx,2
loop CLEARSCORE
;清空spd顯示位置
setDS lineAddr(24)
mov bx,68
mov cx,7 ;清7格
CLEARSPD:
mov [ds:bx],ax
add bx,2
loop CLEARSPD
;打印提示"spd:"和spd值
mov ah,[es:info_color] ;顏色
mov bx,70
mov al,'s'
mov [ds:bx],ax
add bx,2
mov al,'p'
mov [ds:bx],ax
add bx,2
mov al,'d'
mov [ds:bx],ax
add bx,2
mov al,':'
mov [ds:bx],ax
add bx,2
mov al,[es:spd]
mov [ds:bx],ax
;打印score的十進制值
setDS cs
mov ax,[score]
setDS lineAddr(0) ;設置score的位置
mov bx,72
call print_num
pop dx
pop cx
pop bx
pop ax
pop ds
pop bp
ret
;功能:顯示一個數的十進制值----------------------------------------------------------
;說明:循環除10取餘數分解各位,每一位入棧,打印時依次出棧打印,實現順序顯示
;參數: 被顯示數ax,ds:bx已經指向顯示位置
;返回: 無
print_num:
push cx
push bx
push dx
push ax
push si
mov si,bx
mov cx, -1
mov bx,10
PRINTSCORE1:
xor dx, dx
div bx
push dx
cmp ax, 0
loopne PRINTSCORE1
not cx
PRINTSCORE2:
pop ax
mov ah,[es:info_color] ;顏色
add al, '0'
mov [ds:si],ax
add si,2
LOOP PRINTSCORE2
pop si
pop ax
pop dx
pop bx
pop cx
ret
8. 支持函數
- 包括生成新食物和打印提示信息的函數
;功能: 生成一個新食物
;參數: 無
;返回: 無
draw_food:
push ax
push cx
push dx
push bx
push ds
;讀實時種(CH:CL=時:分 DH:DL=秒:1/100秒,設dh存的秒爲x1,dl存的百分之秒爲x2
mov ah,2ch
int 21h
;算出當前實時秒個位,加1後壓棧(1~10)
xor ax,ax
mov al,dh
mov bl,10
div bl
mov al,ah
xor ah,ah
inc ax
push ax
;判斷實時秒是奇還是偶,生成隨機數ax
xor ax,ax
mov al,dh
mov bl,2
div bl
cmp ah,0
jz EVEN
pop ax
mul dl ;實時秒是奇數:ax=(x1+1)*x2
jmp cheakDone
EVEN:
mov ax,11
pop bx
sub ax,bx
mul dl ;實時秒是偶數:ax=(10-x1)*x2
cheakDone:
;用ax對行數和列數取模,得到隨機食物位置
push ax ;暫存隨機數
xor dx,dx
mov bx,78 ;dx:ax除bx,商在ax,餘數在dx
div bx
add dx,1 ;1~78,x座標(每列2個字節)
pop ax ;恢復隨機數
push dx ;暫存x座標
xor dx,dx
mov bx,23 ;dx:ax除bx,商在ax,餘數在dx
div bx
inc dx ;1~23,y座標
;根據食物位置設置ds和bx,令[ds:bx]指向food所在點
mov ax,displayBase
mov cx,dx
getYAddr:
add ax,0ah
loop getYAddr
mov ds,ax ;至此ds指向food所在行首
pop bx ;恢復x座標
add bx,bx ;至此[ds:bx]指向food所在點
;如果隨機到蛇身或者其他食物點,調整點位置(向右一直移動直到合適爲止,注意換行)
cmp byte[ds:bx],'*'
jz ADJUST
cmp byte[ds:bx],'#'
jnz ADJUSTDONE
ADJUST:
add bx,2
cmp bx,156 ;列>78,重置到1
jb ADJUSTDONE
mov bx,2
mov ds,ax
add ax,0ah
cmp ax,lineAddr(25) ;行>=25,重置到1
jb ADJUSTDONE
mov ax,lineAddr(1)
mov ds,ax
ADJUSTDONE:
;如果移動後還不行,繼續移動直到可以
cmp byte[ds:bx],'*'
jz ADJUST
cmp byte[ds:bx],'#'
jz ADJUST
;畫食物
mov ah,[es:food_color]
mov al,'#' ;#號
mov [ds:bx],ax
pop ds
pop bx
pop dx
pop cx
pop ax
ret
;在窗口指定位置顯示字符串,字符串以'$'結束
;參數:si指向字符串首地址,ds:bx指向屏上顯示起始位置
;返回:無
show_str:
push ax
show_str_start:
mov al,byte [es:si]
cmp al,'$'
jz show_str_end
mov ah,byte [es:info_color]
mov [ds:bx],ax
inc si
add bx,2
jmp show_str_start
show_str_end:
pop ax
ret