随便说说:最近闲得蛋疼,然后之前《汇编语言》看了一遍,没怎样具体写过东西,就想着来用汇编写个贪食蛇吧(话说大一就想写过,想不到是到了大二才完成,还是用汇编写的。。。)
代码可以点这里下载
P.S.由于CPU差异,初始速度可能有差异,可以通过调节速度(开始页面按a)
程序:贪食蛇
环境:masm+dosbox+windows(有doxbox基本win的系统都一样)
功能:(或者说要实现的东西)
1.控制蛇方向
2.蛇撞墙或者撞到自己身体会死
3.蛇吃到食物会长一个身体
4.食物随机出现,但不能出现在蛇的身体中或者墙里面
5.开始游戏前可以调节速度
6.游戏结束时提示分数以及速度
P.S.最后成品有个bug,就是食物随机出现那里(下面说)
下面来根据上面6点逐个解释:
先把其分割成几个步骤:
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个放在一起讲,是因为它们的关键都一样(就是修改方向的那个)
首先,控制蛇方向、蛇撞墙、蛇长身体这些的前提,必须是分清除哪些是蛇身体,哪些是墙,哪些是空白地方,给用户分辨就是很简单的颜色不同来区分,但程序如何区分?
由于我这是通过显示缓冲区来显示,而且修改后不能从内存缓冲区获取原来的信息,因此必须用另外的方法来记录,我一开始想到的是用链表来记录,不过用汇编来实现一个链表好像也挺大过程的,那就算了,就用最普通的建一个图(map)来记录各种信息:
map db 2000 dup('0')
因为是25*80所以就是2000
P.S.这个空间可以缩小,因为一共就3种情况(食物我没有记录)所以用2bit就可以,但是这个后面的比较又比较麻烦所以算了。。。
采用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 )
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
蛇身体和墙不同(map中存储的除了标识符的作用,应该有更多作用),就是蛇身体是一个接一个的(这就是为什么一开始就是想到链表),我们又来分析一下蛇移动的过程:
P.S.这边图形的显示都是通过修改显示缓冲区,所以不用刷新,而是直接改变显示的颜色
1.蛇头按照方向往前走一步(新的一格显色),修改蛇头地址
2.去掉蛇尾(蛇尾去色),修改蛇尾地址
这时问题来了:
1.蛇头方向怎么确定?
2.怎么修改蛇尾地址?也就是怎么找到蛇尾的相连的身体?
对了,相信大家也是马上想出来了,蛇头的map中存储的就是蛇前进的方向,而修改方向也就是修改相应map位置中的内容便可
;-------------------funtion------------head_toward--------------------
;will change and
;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中的内容
蛇头是特殊的,当它不再是蛇头的时候,它便承担了另一个任务,由于蛇的前进,该格迟早会变成蛇尾(它指的是图中的一格),所以它身上必须存储可以帮助找到与其连接的身体的信息,对了,就是存储与它连接的身体的那格的方向(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
;---------------------------------------------------------------------
有这两个,蛇的移动和吃食物长身体就很容易了
一个就是需要删尾巴,一个不用删尾巴。。。
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
上面也提过食物并没有在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
其实,在每次蛇移动了一格之后,并不是马上开始下一次移动,因为计算机的运行速度远超人类,所以每次移动后都用空循环来延迟,这个也就是调节速度关键:循环的次数
;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+汇编也不错。。。