前言:
隨便說說:最近閒得蛋疼,然後之前《彙編語言》看了一遍,沒怎樣具體寫過東西,就想着來用匯編寫個貪食蛇吧(話說大一就想寫過,想不到是到了大二才完成,還是用匯編寫的。。。)
代碼可以點這裏下載
P.S.由於CPU差異,初始速度可能有差異,可以通過調節速度(開始頁面按a)
程序:貪食蛇
環境:masm+dosbox+windows(有doxbox基本win的系統都一樣)
功能:(或者說要實現的東西)
1.控制蛇方向
2.蛇撞牆或者撞到自己身體會死
3.蛇吃到食物會長一個身體
4.食物隨機出現,但不能出現在蛇的身體中或者牆裏面
5.開始遊戲前可以調節速度
6.遊戲結束時提示分數以及速度
P.S.最後成品有個bug,就是食物隨機出現那裏(下面說)
下面來根據上面6點逐個解釋:
分析:
1.控制蛇方向
先把其分割成幾個步驟:
1.檢測用戶是否按鍵:有->3 沒有->2
2.保持原方向
3.獲取蓋按鍵,並判斷該按鍵是否合法(合法按鍵:w/s/a/d)合法->5 不合法->4
4.保持原方向
5.修改方向(注意:只是修改方向,蛇身體還沒動)
檢測用戶是否按鍵:採用中斷
mov ax,0b00h
int 21h
這個中斷只能用來檢測是否有輸入而並不能獲取輸入的按鍵
因此還需要另一箇中斷來獲取按鍵:
mov ah,07h
int 21h
注意上面描述中,是等待輸入,因此不判斷是否有輸入而是直接採用這個中斷就會造成整個程序停在那裏等待輸入,因爲是個單線程程序
下面是我貪食蛇中代碼:
;if input the key,then return the key value
;al: the key value
;ah: ff:true 0:false
check_get_input proc
mov ax,0b00h
int 21h
mov ah,0h
cmp al,0ffh
jne finish_cgi
mov ah,07h
int 21h
mov ah,0ffh
finish_cgi:
ret
check_get_input endp
還有個關鍵就是方向的修改,這個算整個程序核心。。。
2、3
把這2個放在一起講,是因爲它們的關鍵都一樣(就是修改方向的那個)
首先,控制蛇方向、蛇撞牆、蛇長身體這些的前提,必須是分清除哪些是蛇身體,哪些是牆,哪些是空白地方,給用戶分辨就是很簡單的顏色不同來區分,但程序如何區分?
由於我這是通過顯示緩衝區來顯示,而且修改後不能從內存緩衝區獲取原來的信息,因此必須用另外的方法來記錄,我一開始想到的是用鏈表來記錄,不過用匯編來實現一個鏈表好像也挺大過程的,那就算了,就用最普通的建一個圖(map)來記錄各種信息:
map db 2000 dup('0')
因爲是25*80所以就是2000
P.S.這個空間可以縮小,因爲一共就3種情況(食物我沒有記錄)所以用2bit就可以,但是這個後面的比較又比較麻煩所以算了。。。
.1地址轉換
採用map的話,還需要一個實際圖和map之間的轉化(由於實際圖,也就是顯示緩衝區中,是用2byte來表示一格,而map使用1byte來表示),又由於8086的地址格式是:
segment:[offset]
最後地址=segment*16+offset
因此要先把地址組合起來,再減去0b800h(顯示緩衝區起始地址),獲得偏移值再除以2,獲得最終的偏移值,最後加上map的起始地址就完成了地址的轉換
;convert the true address(0b800h:0) to the map-address
;temp_seg: source segment
;temp_off: source offset
;ax: result(offset of the <ds>)
convert_address proc
push cx
push temp_seg
push temp_off
sub temp_seg,0b800h ;temp_seg = (temp_seg sub 0b800h) div 2 convert temp_seg
mov ax,temp_seg
mov cx,4
call sal_n
add ax,temp_off
shr ax,1 ;div 2
add ax,offset map
pop temp_off
pop temp_seg
pop cx
ret
convert_address endp
P.S.上面的sal_n是把ax算術左移[cx]位
通過地址轉換把map與實際圖連接起來,這時程序就可以輕鬆分辨出蛇身體、牆壁和空白處,這時判斷是否撞牆(身體)只需要判斷蛇頭是否在牆壁(身體)處便可,爲此蛇頭地址(蛇尾地址)也必須記錄下來
seg_snake_head dw 0 ;store the segment of the head of the snake
off_snake_head dw 0 ;store the offset of the head of the snake
seg_snake_tail dw 0 ;store the segment of the tail of the snake
off_snake_tail dw 0 ;store the offset of the tail of the snake
.2蛇身體
蛇身體和牆不同(map中存儲的除了標識符的作用,應該有更多作用),就是蛇身體是一個接一個的(這就是爲什麼一開始就是想到鏈表),我們又來分析一下蛇移動的過程:
P.S.這邊圖形的顯示都是通過修改顯示緩衝區,所以不用刷新,而是直接改變顯示的顏色
1.蛇頭按照方向往前走一步(新的一格顯色),修改蛇頭地址
2.去掉蛇尾(蛇尾去色),修改蛇尾地址
這時問題來了:
1.蛇頭方向怎麼確定?
2.怎麼修改蛇尾地址?也就是怎麼找到蛇尾的相連的身體?
.21蛇頭
對了,相信大家也是馬上想出來了,蛇頭的map中存儲的就是蛇前進的方向,而修改方向也就是修改相應map位置中的內容便可
;-------------------funtion------------head_toward--------------------
;will change <seg_snake_head> and <off_snake_head>
;ah: new direction
;al: direction before change
head_toward proc
push ax
push bx
push cx
;push dx
push es
push si
push di
mov ax,seg_snake_head
mov temp_seg,ax
mov ax,off_snake_head
mov temp_off,ax
call match_wsad ;temp_seg
mov bx,ax ;bx store the result(cannot be changed)
mov ax,temp_seg
mov seg_snake_head,ax
mov ax,temp_off
mov off_snake_head,ax
mov ah,0
mov ds:[0],ah
mov ah,')'
mov ds:[1],ah
mov es,seg_snake_head
mov si,off_snake_head
mov di,0
mov ah,01110000b
call paint
call is_body
cmp ah,0h
jne lose
mov ax,seg_snake_head
mov temp_seg,ax
mov ax,off_snake_head
mov temp_off,ax
mov ah,bh
call set_map ;set the map
finish_ht:
pop di
pop si
pop es
;pop dx
pop cx
pop bx
pop ax
ret
head_toward endp
;------------------------------------------------------------
上面的set_map就是修改map中的內容
.22蛇身體(除了蛇頭)
蛇頭是特殊的,當它不再是蛇頭的時候,它便承擔了另一個任務,由於蛇的前進,該格遲早會變成蛇尾(它指的是圖中的一格),所以它身上必須存儲可以幫助找到與其連接的身體的信息,對了,就是存儲與它連接的身體的那格的方向(w/s/a/d)
;--------------------funtion---------delete_tail------------------
delete_tail proc
push ax
push bx
push cx
push es
push si
push di
mov ax,seg_snake_tail
mov temp_seg,ax
mov ax,off_snake_tail
mov temp_off,ax
call match_wsad
mov bx,ax ;bx store the result(cannot be changed)
push temp_seg ;push
push temp_off ;push
mov ah,0
mov ds:[0],ah
mov ah,')'
mov ds:[1],ah
mov es,seg_snake_tail
mov si,off_snake_tail
mov di,0
mov ah,0h
call paint
mov ax,seg_snake_tail
mov temp_seg,ax
mov ax,off_snake_tail
mov temp_off,ax
mov ah,'0'
call set_map
pop off_snake_tail ;pop
pop seg_snake_tail ;pop
pop di
pop si
pop es
pop bx
pop cx
pop ax
ret
delete_tail endp
;---------------------------------------------------------------------
有這兩個,蛇的移動和吃食物長身體就很容易了
一個就是需要刪尾巴,一個不用刪尾巴。。。
4.食物隨機出現,但不能出現在蛇的身體中或者牆裏面
4.1產生食物
1.這個我是把 系統時間秒數 * 4 來產生隨機數,這樣產生的隨機數其實並不隨機,隨機效果很差,因爲秒數是遞增的,導致產生的隨機數也是遞增的,表現在遊戲中就是食物一層一層往下出現
2.而每次產生一個隨機數後,還需要判斷該地址是否合法
以上個原因共同導致了一個問題(bug):
當食物被隨機在底牆(頂牆)時,不合法會再次產生食物,但由於隨機數的問題,食物又會被產生在同一行(也就是依然在底牆或頂牆之中),這時意味着程序會不斷產生食物然後判斷,導致整個程序卡在這裏直至最後食物成功誕生。
在遊戲中的表現就是,蛇吃了食物後突然停在那裏,然後又突然動起來。。。通常出現在底層吃了食物之後,而且這個現象應該會隨着蛇越來越長而出現的頻率越來越高,算一個比較大的問題……
create_food proc
push ax
push cx
push dx
cf_l: mov ah,2ch ;get the system time
int 21h
mov ax,0 ;create the random address
mov al,dh
mov cx,2
call sal_n
add ax,0b800h
mov temp_seg,ax
mov temp_off,4
call is_wall
cmp ah,0ffh
call is_body
cmp ah,0ffh
je cf_l
mov ax,temp_seg
mov seg_food,ax
mov ax,temp_off
mov off_food,ax
mov ah,'*'
mov ds:[0],ah
mov ah,')'
mov ds:[1],ah
mov ax,temp_seg
mov es,ax
mov ax,temp_off
mov si,ax
mov di,0
mov ah,00000111b
call paint
pop dx
pop cx
pop ax
ret
create_food endp
4.2吃食物
上面也提過食物並沒有在map中標記,而是記錄食物的地址,再來與蛇頭相比較看是否一致,來判斷是否吃食物,而且判斷必須是把segment和offset合在一起再進行判斷
;temp_seg: segment of the head of snake
;temp_off: offset of the head of snake
;ah: 0ffh: true 0:false
is_food proc
push bx
push cx
mov ax,temp_seg
mov cx,4
call sal_n
add ax,temp_off
mov bx,ax
mov ax,seg_food
mov cx,4
call sal_n
add ax,off_food
cmp ax,bx
mov ah,0ffh
je finish_if
mov ah,0h
finish_if:
pop cx
pop bx
ret
is_food endp
5.開始遊戲前可以調節速度
其實,在每次蛇移動了一格之後,並不是馬上開始下一次移動,因爲計算機的運行速度遠超人類,所以每次移動後都用空循環來延遲,這個也就是調節速度關鍵:循環的次數
;time is greater,speed is littler
delay proc
push cx
mov cx,1000
dl1: push cx
mov cx,time
dl2: push cx
mov cx,10
dl3: loop dl3
pop cx
loop dl2
pop cx
loop dl1
pop cx
ret
delay endp
上面的代碼,只要通過修改time的值便可以調節速度
以上就是比較大的幾個部分
還有個問題就是地圖導致的問題:就是它這個顯示屏是25*80,也就是豎列也就25個格,但是和80格橫行相比,沒差多少,也就是它的格不是正方形,而且高比寬大,導致上下走的時候,會感覺速度快很多。。。
下面來幾張具體截圖吧:
我的是win7,需要用dosbox,不知道怎麼用dosbox可以看這裏
在dosbox轉到程序目錄就可以了
開始頁面:
調節速度:
玩:
結束:
總結:
這次代碼最後是1000行左右,感覺有幾點注意:
1.push和pop必須一一對應好,特別在函數中的push、pop,而且用於暫時存儲的push、pop最好寫上註釋提高注意
2.一定要寫註釋
3.函數,最好把不是用於傳遞結果的寄存器都用push、pop保存好
4.有時用內存傳遞變量可以簡化代碼
5.函數要明確作用
最後,代碼寫得也不怎樣,不過感覺彙編挺好玩的。。。不過也好麻煩,下次寫類似的小程序的時候,感覺用c+彙編也不錯。。。