典型的蛇游戏.如何跟踪蛇? [英] The quintessential Snake Game. How to keep track of the snake?

查看:71
本文介绍了典型的蛇游戏.如何跟踪蛇?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这个棋盘游戏的目的是吃食物并成长.

在最基本的形式中,游戏仅使用三种颜色:一种用于蛇(一系列相互连接的瓷砖),一种用于食物(随机选择的瓷砖)和一种用于背景(无人使用的瓷砖).由于蛇一直在移动,因此随时可以清楚地看到蛇的头部在哪里.无需任何图形标记.玩家可以通过键盘上的箭头键控制蛇,瞄准食物.如果食物被吃掉,蛇将再长一段,然后将下面的食物放在板上.如果蛇撞到边界或撞到自己,游戏就结束了!

为使蛇移动,在头"侧添加了一个新的片段,并在尾巴"侧移除了一个现有的片段.在程序中,我们可以将头部"和尾部"的坐标存储在变量中.更新"head"变量很容易,但是我们如何明确知道新的"tail"将在哪里呢?那么,我们如何跟踪蛇呢?有必要发明一些数据结构吗?

解决方案

要跟踪蛇,我们必须记录其所有段的位置.我们可以选择存储实际(X,Y)坐标或连续坐标之间变化的指示.我们可以将这些信息存储在视频缓冲区矩阵,我们自己的矩阵或循环缓冲区中.

从视频缓冲区矩阵读取游戏信息.

首先让我们假设使用文本视频模式,其中每个字符单元由一个字符字节(ASCII)和一个属性字节(颜色)表示,该属性字节使我们能够在16种前景色和16种背景色之间进行选择.如果前景色和背景色碰巧相等,那么我们存储在其中的字符代码就不再重要了.结果输出将始终形成单色实心矩形.我们可以进行设置,以便当前尾巴所在的图块的字符字节记录要移动的方向以便定位新尾巴.箭头键的扫描代码用于此目的.例如,如果当前尾巴位于(5,8)并且视频存储器中的字符字节保持值48h( up ),则新尾巴将位于(5,7)

我们也可以使用属性字节来代替使用字符字节来存储游戏信息.如果然后选择ASCII 32(空格),则视频硬件仅需要背景颜色,并且我们可以使用为前景色保留的4位空间来记录我们的游戏信息.同样,如果我们选择ASCII 219(全块),则视频硬件仅需要前景色,并且可以使用为背景色保留的4位空间来记录我们的游戏信息.

在下面的演示程序中,游戏板上的每个图块均由80x25文本视频模式的视频缓冲区中的2个字符单元组成.这将产生正方形图块.生成正方形瓦片的更简单方法是使用40x25文本视频模式,但事实证明,对于Microsoft Windows而言,40x25模式与使用80x25模式的左半部分相同.这不利于获得漂亮的方形瓷砖.
隐藏光标还完全是为了在Microsoft Windows中运行演示程序.

 ;蛇游戏-VRAM(c)2021年9月罗兰组织256模式= 03小时COLS = 80ROWS = 25SLEN = COLS/8;蛇的初始长度MIDP =((ROWS-1)/2)* 256 +(COLS/2);运动场中心BACKCOLOR = 66h;棕色的FOODCOLOR = 55h;品红SNAKECOLOR = 22h;绿色的定时器设备gs:046Ch;BIOS.TimerTickSTRUC Snake a,b,c,d,e{.头dw a尾巴dw b长度dw c.流数据库.速度数据库}cld异或斧mov gs,ax机斧,0B800hmov es,ax;虚拟RAMmov ax,[TIMER];种子mov [Rand],axmov ax,MODE;BIOS.SetVideoMode整数10hmov dx,ROWS * 256 + 0;隐藏光标mov bh,0mov ah,02h;BIOS设置向导整数10h;油漆运动场,画蛇和食物or迪mov cx,COLS *(ROWS-1)mov ax,BACKCOLOR * 256 + 0;0是免费的代表斯托mov di,((((ROWS-1)/2)* COLS +(COLS/2)-SLEN)* 2mov cx,SLEN * 2mov ax,SNAKECOLOR * 256 + 4Dh代表斯托叫NewFood;->(AX..DX);显示"GO"然后等待按键,然后开始mov dword [es:((ROWS-1)* COLS + 4)* 2],0A4F0A47hmov ah,00h;BIOS获取密钥int 16小时;->斧头通话状态;->(AX..DX)主要:mov ax,[TIMER];实时同步@@:cmp斧头,[计时器]je @b.kbd:mov ah,01h;BIOS测试键int 16小时;->AX ZF表演mov ah,00h;BIOS获取密钥int 16小时;->斧头cmp al,27岁;< ESC>吉戒烟cmp al,32;< SPC>jne .arrow速度:mov al,11111111b;快速利用每一刻cmp [S.Speed]等jne @f运动00010001b;慢用四分之一@@:mov [S.Speed],其他jmp .show.arrow:mov al,啊cmp al,4Dh;< RIGHT>je @fcmp al,48小时;< UP>je @fcmp al,4Bh;< LEFT>je @fcmp al,50小时;< DOWN>jne.显示@@:mov [S.Flow]等;AL = {4Dh = X +,48h = Y-,4Bh = X-,50h = Y +}.show:ror [S.Speed],1jnc主运动[S.Flow];{4Dh,48h,4Bh,50h}mov cx,[S.Head]呼叫NextXY;->CX调用ReadPlayfieldCell;->AL = {0,1,4Dh,48h,4Bh,50h}(BX)cmp al,1je.eat;0是免费的,1是食物杰DEAD;其他是蛇.move:致电NewHead;->(AX..CX)呼叫NewTail;->(AX..CX)jmp主.eat:致电NewHead;->(AX..CX)inc [S.Length]通话状态;->(AX..DX)叫NewFood;->(AX..DX)jmp主;----------------------------------;显示游戏结束"然后等待< ESC> ;,然后退出死亡:mov si,Msgmov di,((行-1)* COLS +(COLS/2)-4)* 2罗兹;第一个字符和颜色@@:stosw罗兹cmp al,0jne @b@@:mov ah,00h;BIOS获取密钥int 16小时;->斧头cmp al,27岁;< ESC>jne @b;-------退出:斧头,0003h;BIOS.SetVideoMode整数10hmov ax,4C00h;DOS终止整数21h;----------------------------------;输入(al,cx)输出(cx)NextXY:cmp al,4Dh;AL = {4Dh,48h,4Bh,50h}jne @f加cl,2;每个运动场单元2个字符单元cmp cl,COLS杰·迪德退回@@:cmp al,4Bh;AL = {48h,4Bh,50h}jae @f子频道1死了退回@@:ja @f子分类2死了退回@@:添加ch,1cmp ch,ROWS-1杰·迪德退回;----------------------------------;IN(cx)OUT()MOD(ax..cx)NewHead:mov al,[S.Flow]mov啊,蛇色调用WritePlayfieldCell;->(BX)xchg cx,[S.Head];-------;输入(ax,cx)输出()MOD(bx)WritePlayfieldCell:movzx bx,ch;CH是行imul bx,COLS加bl,cl;CL是列adc bh,0shl bx,1mov [es:bx],axmov [es:bx + 3],啊退回;----------------------------------;IN()OUT()MOD(ax..cx)NewTail:mov cx,[S.Tail]调用ReadPlayfieldCell;->AL = {4Dh,48h,4Bh,50h}(BX)呼叫NextXY;->CXxchg cx,[S.Tail]mov ax,BACKCOLOR * 256 + 0;0是免费的jmp WritePlayfieldCell;----------------------------------;IN()OUT()MOD(ax..dx)NewFood:移动斧头,[兰德]伊穆尔斧头,25173加斧头13849mov [Rand],axmov bx,ROWS-1xor dx,dxdiv bxmov ch,dlmov ax,[兰德]mov bx,COLS/2xor dx,dxdiv bxshl dl,1mov cl,dl调用ReadPlayfieldCell;->AL = {0,1,4Dh,48h,4Bh,50h}(BX)cmp al,0;0是免费的新食品杂志mov ax,FOODCOLOR * 256 + 1;1是食物jmp WritePlayfieldCell;----------------------------------;输入(cx)输出(al)MOD(bx)ReadPlayfieldCell:movzx bx,ch;CH是行imul bx,COLS加bl,cl;CL是列adc bh,0shl bx,1mov al,[es:bx]退回;----------------------------------;IN()OUT()MOD(ax..dx)状态:mov ax,[S.Length]mov bx,((行-1)* COLS + 5)* 2mov cx,10@@:xor dx,dxdiv cx加dx,0F00h +'0'mov [es:bx],dx子bx,2测试斧,斧头jnz @bmov字节[es:bx],''退回;----------------------------------Msg db'G',12,'AME OVER',0对齐2S蛇MIDP + SLEN-2,MIDP-SLEN,SLEN,4Dh,00010001b兰德dw? 

从我们自己的矩阵中读取游戏信息.

此解决方案类似于读取视频缓冲区矩阵,但更快,更灵活.由于从VRAM读取要比从常规RAM读取慢,因此速度更快,并且由于屏幕可以继续显示所有字符和所有颜色组合,因此更加灵活.从某种角度讲更快":"MATRIX"程序运行一个典型的周期为1.1微秒,"VRAM"程序运行一个周期为2.6微秒.这有关系吗?并非如此,这两个程序都在必要的延迟循环中花费了99.99%的时间.

由于不存在内存不足,我们可以浪费一些内存并从中受益.即使游戏板的列数较少,我们也可以将矩阵设置为256列.然后,如果我们将X存储在地址

 ;蛇游戏-MATRIX(c)2021年9月罗兰组织256模式= 03小时COLS = 80ROWS = 25SLEN = COLS/8;蛇的初始长度MIDP =((ROWS-1)/2)* 256 +(COLS/2);运动场中心BACKCOLOR = 66h;棕色的FOODCOLOR = 55h;品红SNAKECOLOR = 22h;绿色的定时器设备gs:046Ch;BIOS.TimerTickSTRUC Snake a,b,c,d,e{.头dw a尾巴dw b长度dw c.流数据库.速度数据库}cld异或斧mov gs,ax机斧,0B800hmov es,ax;虚拟RAMmov ax,[TIMER];种子mov [Rand],axmov ax,MODE;BIOS.SetVideoMode整数10hmov dx,ROWS * 256 + 0;隐藏光标mov bh,0mov ah,02h;BIOS设置向导整数10h;绘制运动场和矩阵,绘制蛇和食物or迪mov cx,COLS *(ROWS-1)mov ax,BACKCOLOR * 256 + 0;0是免费的代表斯托mov bx,256 *(ROWS-1)@@:dec bxmov [Mat + bx]等jnz @bmov bx,MIDP-SLEN;尾XYmov ax,SNAKECOLOR * 256 + 4Dh@@:调用WritePlayfieldCell;->(CX)加bl,2;X +cmp bl,(COLS/2)+ SLEN-2;头Xjbe @b叫NewFood;->(AX..DX);显示"GO"然后等待按键,然后开始mov dword [es:((ROWS-1)* COLS + 4)* 2],0A4F0A47hmov ah,00h;BIOS获取密钥int 16小时;->斧头通话状态;->(AX..DX)主要:mov ax,[TIMER];实时同步@@:cmp斧头,[计时器]je @b.kbd:mov ah,01h;BIOS测试键int 16小时;->AX ZF表演mov ah,00h;BIOS获取密钥int 16小时;->斧头cmp al,27岁;< ESC>吉戒烟cmp al,32;< SPC>jne .arrow速度:mov al,11111111b;快速利用每一刻cmp [S.Speed]等jne @f运动00010001b;慢用四分之一@@:mov [S.Speed],其他jmp .show.arrow:mov al,啊cmp al,4Dh;< RIGHT>je @fcmp al,48小时;< UP>je @fcmp al,4Bh;< LEFT>je @fcmp al,50小时;< DOWN>jne.显示@@:mov [S.Flow]等;AL = {4Dh = X +,48h = Y-,4Bh = X-,50h = Y +}.show:ror [S.Speed],1jnc主运动[S.Flow];{4Dh,48h,4Bh,50h}mov bx,[S.Head]呼叫NextXY;->BXcmp字节[Mat + bx],1je.eat;0是免费的,1是食物杰DEAD;其他是蛇.move:致电NewHead;->(AX..CX)呼叫NewTail;->(AX..CX)jmp主.eat:致电NewHead;->(AX..CX)inc [S.Length]通话状态;->(AX..DX)叫NewFood;->(AX..DX)jmp主;----------------------------------;显示游戏结束"然后等待< ESC> ;,然后退出死亡:mov si,Msgmov di,((行-1)* COLS +(COLS/2)-4)* 2罗兹;第一个字符和颜色@@:stosw罗兹cmp al,0jne @b@@:mov ah,00h;BIOS获取密钥int 16小时;->斧头cmp al,27岁;< ESC>jne @b;-------退出:斧头,0003h;BIOS.SetVideoMode整数10hmov ax,4C00h;DOS终止整数21h;----------------------------------;输入(al,bx)输出(bx)NextXY:cmp al,4Dh;AL = {4Dh,48h,4Bh,50h}jne @f加bl,2;每个运动场单元2个字符单元cmp bl,COLS杰·迪德退回@@:cmp al,4Bh;AL = {48h,4Bh,50h}jae @f低于bh,1死了退回@@:ja @fsub bl,2死了退回@@:添加bh,1cmp bh,ROWS-1杰·迪德退回;----------------------------------;IN(al,bx)OUT()MOD(ax..cx)NewHead:xchg bx,[S.Head]mov [Mat + bx]等mov啊,蛇色mov bx,[S.Head];-------;输入(ax,bx)输出()MOD(cx)WritePlayfieldCell:mov [Mat + bx]等movzx cx,bh;BH是行imul cx,COLS加cl,bl;BL是专栏adc ch,0shl cx,1xchg bx,cxmov [es:bx + 1],啊mov [es:bx + 3],啊mov bx,cx退回;----------------------------------;IN()OUT()MOD(ax..cx)NewTail:mov bx,[S.Tail]运动[Mat + bx];->AL = {4Dh,48h,4Bh,50h}呼叫NextXY;->BXxchg bx,[S.Tail]mov ax,BACKCOLOR * 256 + 0;0是免费的jmp WritePlayfieldCell;----------------------------------;IN()OUT()MOD(ax..dx)NewFood:移动斧头,[兰德]伊穆尔斧头,25173加斧头13849mov [Rand],axmov cx,ROWS-1xor dx,dxdiv cxmov bh,dlmov ax,[兰德]mov cx,COLS/2xor dx,dxdiv cxshl dl,1mov bl,dlcmp字节[Mat + bx],0;0是免费的新食品杂志mov ax,FOODCOLOR * 256 + 1;1是食物jmp WritePlayfieldCell;----------------------------------;IN()OUT()MOD(ax..dx)状态:mov ax,[S.Length]mov bx,((行-1)* COLS + 5)* 2mov cx,10@@:xor dx,dxdiv cx加dx,0F00h +'0'mov [es:bx],dx子bx,2测试斧,斧头jnz @bmov字节[es:bx],''退回;----------------------------------Msg db'G',12,'AME OVER',0对齐2S蛇MIDP + SLEN-2,MIDP-SLEN,SLEN,4Dh,00010001b兰德rw 1垫rb 256 *(ROWS-1) 

从环形缓冲区读取实际坐标.

在此圆形缓冲区中,我们记录了从头到尾的所有蛇段的坐标.缓冲区的大小必须能够容纳可能的最长蛇(或规则所允许的).该程序将指针存储到第一个记录( Head )并指向最后一个记录的指针记录( Tail ).对于新的蛇头,我们降低 Head 指针并插入新的坐标.对于新的蛇尾,我们只需降低 Tail 指针,丢弃最后一条记录.

由于我们需要保留在环形缓冲区内存的范围内,因此需要一种环绕机制.为环形缓冲区的内存选择2的幂很重要,因为这样我们就可以通过一条简单的 AND 指令进行环绕操作.而且,如果我们选择该2的幂为65536,那么我们可以完全放弃此 AND 操作,因为在实地址模式下,CPU已经自动绕回64KB.

搜索环形缓冲区需要花费时间,随着蛇的变长,这个时间将不可避免地增加.但是,在一个出于可玩性原因而设计的程序中,有超过99%的时间都花在延迟循环中,这没关系!

 ;蛇游戏-RINGBUFFER(c)2021年9月罗兰组织256模式= 03小时COLS = 80ROWS = 25SLEN = COLS/8;蛇的初始长度MIDP =((ROWS-1)/2)* 256 +(COLS/2);运动场中心BACKCOLOR = 66h;棕色的FOODCOLOR = 55h;品红SNAKECOLOR = 22h;绿色的定时器设备gs:046Ch;BIOS.TimerTickSTRUC Snake a,b,c,d,e{.头dw a尾巴dw b长度dw c.流数据库.速度数据库}cld异或斧mov gs,axmov斧头,cs加斧(EOF + 15)/16mov ss,ax;512字节堆栈mov sp,512加斧头512/16mov fs,ax;64KB环形缓冲区机斧,0B800hmov es,ax;虚拟RAMmov ax,[TIMER];种子mov [Rand],axmov ax,MODE;BIOS.SetVideoMode整数10hmov dx,ROWS * 256 + 0;隐藏光标mov bh,0mov ah,02h;BIOS设置向导整数10h;油漆运动场,画蛇和食物or迪mov cx,COLS *(ROWS-1)mov ax,BACKCOLOR * 256 + 0代表斯托mov cx,MIDP-SLEN;HeadXY == TailXY@@:致电NewHead;->(AX..BX)加cl,2;X +cmp cl,(COLS/2)+ SLEN-2;头Xjbe @b叫NewFood;->(AX..DX);显示"GO"然后等待按键,然后开始mov dword [es:((ROWS-1)* COLS + 4)* 2],0A4F0A47hmov ah,00h;BIOS获取密钥int 16小时;->斧头通话状态;->(AX..DX)主要:mov ax,[TIMER];实时同步@@:cmp斧头,[计时器]je @b.kbd:mov ah,01h;BIOS测试键int 16小时;->AX ZF表演mov ah,00h;BIOS获取密钥int 16小时;->斧头cmp al,27岁;< ESC>吉戒烟cmp al,32;< SPC>jne .arrow速度:mov al,11111111b;快速利用每一刻cmp [S.Speed]等jne @f运动00010001b;慢用四分之一@@:mov [S.Speed],其他jmp .show.arrow:mov al,啊cmp al,4Dh;< RIGHT>je @fcmp al,48小时;< UP>je @fcmp al,4Bh;< LEFT>je @fcmp al,50小时;< DOWN>jne.显示@@:mov [S.Flow]等;AL = {4Dh = X +,48h = Y-,4Bh = X-,50h = Y +}.show:ror [S.Speed],1jnc主运动[S.Flow];{4Dh,48h,4Bh,50h}mov bx,[S.Head]mov cx,[fs:bx]呼叫NextXY;->CXcmp cx,[FoodXY]je .eat调用ScanSnake;->采埃孚(BX)jnz DEAD;CX是某些蛇形部分的(X,Y).move:致电NewHead;->(AX BX)呼叫NewTail;->(AX..CX)jmp主.eat:致电NewHead;->(AX BX)inc [S.Length]通话状态;->(AX..DX)叫NewFood;->(AX..DX)jmp主;----------------------------------;显示游戏结束"然后等待< ESC> ;,然后退出死亡:mov si,Msgmov di,((行-1)* COLS +(COLS/2)-4)* 2罗兹;第一个字符和颜色@@:stosw罗兹cmp al,0jne @b@@:mov ah,00h;BIOS获取密钥int 16小时;->斧头cmp al,27岁;< ESC>jne @b;-------退出:斧头,0003h;BIOS.SetVideoMode整数10hmov ax,4C00h;DOS终止整数21h;----------------------------------;输入(al,cx)输出(cx)NextXY:cmp al,4Dh;AL = {4Dh,48h,4Bh,50h}jne @f加cl,2;每个运动场单元2个字符单元cmp cl,COLS杰·迪德退回@@:cmp al,4Bh;AL = {48h,4Bh,50h}jae @f子频道1死了退回@@:ja @f子分类2死了退回@@:添加ch,1cmp ch,ROWS-1杰·迪德退回;----------------------------------;输入(cx)输出(ZF)MOD(bx)ScanSnake:mov bx,[S.Tail]mov [fs:bx],cx;哨兵mov bx,[S.Head]子bx,2@@:添加bx,2cmp [fs:bx],cxjne @bcmp bx,[S.Tail]退回;----------------------------------;IN(cx)OUT()MOD(ax,bx)NewHead:mov bx,-2xadd [S.Head],bxmov [fs:bx-2],cxmov啊,蛇色;-------;IN(ah,cx)OUT()MOD(bx)WritePlayfieldCell:movzx bx,ch;CH是行imul bx,COLS加bl,cl;CL是列adc bh,0shl bx,1mov [es:bx + 1],啊mov [es:bx + 3],啊退回;----------------------------------;IN()OUT()MOD(ax..cx)NewTail:mov bx,-2xadd [S.Tail],bxmov cx,[fs:bx-2]mov啊,BACKCOLORjmp WritePlayfieldCell;----------------------------------;IN()OUT()MOD(ax..dx)NewFood:移动斧头,[兰德]伊穆尔斧头,25173加斧头13849mov [Rand],axmov bx,ROWS-1xor dx,dxdiv bxmov ch,dlmov ax,[兰德]mov bx,COLS/2xor dx,dxdiv bxshl dl,1mov cl,dl调用ScanSnake;->采埃孚(BX)jnz NewFood;CX是某些蛇形部分的(X,Y)mov [FoodXY],cxmov啊,FOODCOLORjmp WritePlayfieldCell;----------------------------------;IN()OUT()MOD(ax..dx)状态:mov ax,[S.Length]mov bx,((行-1)* COLS + 5)* 2mov cx,10@@:xor dx,dxdiv cx加dx,0F00h +'0'mov [es:bx],dx子bx,2测试斧,斧头jnz @bmov字节[es:bx],''退回;----------------------------------Msg db'G',12,'AME OVER',0对齐2S蛇SLEN * 2,SLEN * 2,SLEN,4Dh,00010001bFoodXY dw?兰德dw?EOF: 

The objective of this board game is to eat the food and grow.

In its most basic form, the game only uses 3 colors: one for the snake (a series of interconnected tiles), one for the food (a randomly chosen tile), and one for the background (the unoccupied tiles). Because the snake is continuously on the move, it will be obvious enough where the head of the snake is at any one time. There's no need for any graphical markings. The player can control the snake through the arrow keys on the keyboard, aiming for the food. If the food is eaten, the snake will grow an additional one segment and a following food is placed on the board. If the snake crashes into the border or bumps into itself, the game is over!

To make the snake move, a new segment is added at the 'head' side and an existing segment is removed at the 'tail' side. In the program we can store the coordinates for both 'head' and 'tail' in variables. Updating the 'head' variable is easy, but how can we unambiguously know where the new 'tail' will be? So, how can we keep track of the snake? Will it be necessary to invent some data structure?

解决方案

To keep track of the snake we have to record the position of all of its segments. We can choose to store the actual (X,Y) coordinates or an indication of the change between successive coordinates. We can store this information in the video buffer matrix, in a matrix of our own, or in a circular buffer.

Reading game information from the video buffer matrix.

First let us assume using a text video mode where each character cell is represented by a character byte (ASCII), and an attribute byte (color) that enables us to choose between 16 foreground colors and 16 background colors. If the foreground and background colors happen to be equal, it won't matter anymore what character code we have stored there. The resulting output will always form a single color, solid rectangle. We can set things up so that the character byte of the tile where the current tail is located, records the direction to move to in order to position the new tail. The scancodes of the arrow keys are used for this purpose. For example, if the current tail is at (5,8) and the character byte in the video memory holds the value 48h (up), then the new tail will be positioned at (5,7).

Instead of using the character byte to store the game information, we could also use the attribute byte. If we then select ASCII 32 (space), the video hardware only needs the background color and we can use the 4-bit space reserved for the foreground color to record our game information. Similarly if we select ASCII 219 (full block), the video hardware only needs the foreground color and we can use the 4-bit space reserved for the background color to record our game information.

In the demonstration programs that follow, every tile on the game board is made up of 2 character cells in the video buffer of the 80x25 text video mode. This is what will produce square tiles. The simpler method to produce square tiles would have been to use the 40x25 text video mode, but as it turns out, for Microsoft Windows the 40x25 mode is the same as using the left half of the 80x25 mode. That does not help in getting nice square tiles.
Hiding the cursor is also solely for the benefit of running the demo's in Microsoft Windows.

; The Snake Game - VRAM (c) 2021 Sep Roland

        ORG     256

MODE=03h
COLS=80
ROWS=25
SLEN=COLS/8                         ; Initial length of snake
MIDP=((ROWS-1)/2)*256+(COLS/2)      ; Center of playfield
BACKCOLOR=66h                       ; Brown
FOODCOLOR=55h                       ; Magenta
SNAKECOLOR=22h                      ; Green
TIMER equ gs:046Ch                  ; BIOS.TimerTick

STRUC Snake a, b, c, d, e
 {
  .Head         dw      a
  .Tail         dw      b
  .Length       dw      c
  .Flow         db      d
  .Speed        db      e
 }

        cld
        xor     ax, ax
        mov     gs, ax
        mov     ax, 0B800h
        mov     es, ax              ; VRAM

        mov     ax, [TIMER]         ; Seed
        mov     [Rand], ax

        mov     ax, MODE            ; BIOS.SetVideoMode
        int     10h
        mov     dx, ROWS*256+0      ; Hide cursor
        mov     bh, 0
        mov     ah, 02h             ; BIOS.SetCursor
        int     10h

; Paint the playfield, draw the snake and food
        xor     di, di
        mov     cx, COLS*(ROWS-1)
        mov     ax, BACKCOLOR*256+0 ; 0 is free
        rep stosw

        mov     di, (((ROWS-1)/2)*COLS+(COLS/2)-SLEN)*2
        mov     cx, SLEN*2
        mov     ax, SNAKECOLOR*256+4Dh
        rep stosw

        call    NewFood             ; -> (AX..DX)

; Show "GO" and wait for a keypress, then begin
        mov     dword [es:((ROWS-1)*COLS+4)*2], 0A4F0A47h
        mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        call    Status              ; -> (AX..DX)

Main:   mov     ax, [TIMER]         ; Sync with real time
@@:     cmp     ax, [TIMER]
        je      @b

.kbd:   mov     ah, 01h             ; BIOS.TestKey
        int     16h                 ; -> AX ZF
        jz      .show
        mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        cmp     al, 27              ; <ESC>
        je      Quit
        cmp     al, 32              ; <SPC>
        jne     .arrow

.speed: mov     al, 11111111b       ; Fast uses every tick
        cmp     [S.Speed], al
        jne     @f
        mov     al, 00010001b       ; Slow uses one out of four ticks
@@:     mov     [S.Speed], al
        jmp     .show

.arrow: mov     al, ah
        cmp     al, 4Dh             ; <RIGHT>
        je      @f
        cmp     al, 48h             ; <UP>
        je      @f
        cmp     al, 4Bh             ; <LEFT>
        je      @f
        cmp     al, 50h             ; <DOWN>
        jne     .show
@@:     mov     [S.Flow], al        ; AL={4Dh=X+, 48h=Y-, 4Bh=X-, 50h=Y+}

.show:  ror     [S.Speed], 1
        jnc     Main

        mov     al, [S.Flow]        ; {4Dh,48h,4Bh,50h}
        mov     cx, [S.Head]
        call    NextXY              ; -> CX
        call    ReadPlayfieldCell   ; -> AL={0,1,4Dh,48h,4Bh,50h} (BX)
        cmp     al, 1
        je      .eat                ; 0 is free, 1 is food
        ja      DEAD                ; other is snake

.move:  call    NewHead             ; -> (AX..CX)
        call    NewTail             ; -> (AX..CX)
        jmp     Main

.eat:   call    NewHead             ; -> (AX..CX)
        inc     [S.Length]
        call    Status              ; -> (AX..DX)
        call    NewFood             ; -> (AX..DX)
        jmp     Main
; ----------------------------------
; Show "GAME OVER" and wait for <ESC>, then quit
DEAD:   mov     si, Msg
        mov     di, ((ROWS-1)*COLS+(COLS/2)-4)*2
        lodsw                       ; First char and color
@@:     stosw
        lodsb
        cmp     al, 0
        jne     @b

@@:     mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        cmp     al, 27              ; <ESC>
        jne     @b
; ---   ---   ---   ---   ---   ---
Quit:   mov     ax, 0003h           ; BIOS.SetVideoMode
        int     10h
        mov     ax, 4C00h           ; DOS.Terminate
        int     21h
; ----------------------------------
; IN (al,cx) OUT (cx)
NextXY: cmp     al, 4Dh             ; AL={4Dh,48h,4Bh,50h}
        jne     @f
        add     cl, 2               ; 2 character cells per playfield cell
        cmp     cl, COLS
        je      DEAD
        ret
@@:     cmp     al, 4Bh             ; AL={48h,4Bh,50h}
        jae     @f
        sub     ch, 1
        jb      DEAD
        ret
@@:     ja      @f
        sub     cl, 2
        jb      DEAD
        ret
@@:     add     ch, 1
        cmp     ch, ROWS-1
        je      DEAD
        ret
; ----------------------------------
; IN (cx) OUT () MOD (ax..cx)
NewHead:mov     al, [S.Flow]
        mov     ah, SNAKECOLOR
        call    WritePlayfieldCell  ; -> (BX)
        xchg    cx, [S.Head]
; ---   ---   ---   ---   ---   ---
; IN (ax,cx) OUT () MOD (bx)
WritePlayfieldCell:
        movzx   bx, ch              ; CH is Row
        imul    bx, COLS
        add     bl, cl              ; CL is Column
        adc     bh, 0
        shl     bx, 1
        mov     [es:bx], ax
        mov     [es:bx+3], ah
        ret
; ----------------------------------
; IN () OUT () MOD (ax..cx)
NewTail:mov     cx, [S.Tail]
        call    ReadPlayfieldCell   ; -> AL={4Dh,48h,4Bh,50h} (BX)
        call    NextXY              ; -> CX
        xchg    cx, [S.Tail]
        mov     ax, BACKCOLOR*256+0 ; 0 is free
        jmp     WritePlayfieldCell
; ----------------------------------
; IN () OUT () MOD (ax..dx)
NewFood:mov     ax, [Rand]
        imul    ax, 25173
        add     ax, 13849
        mov     [Rand], ax
        mov     bx, ROWS-1
        xor     dx, dx
        div     bx
        mov     ch, dl
        mov     ax, [Rand]
        mov     bx, COLS/2
        xor     dx, dx
        div     bx
        shl     dl, 1
        mov     cl, dl
        call    ReadPlayfieldCell   ; -> AL={0,1,4Dh,48h,4Bh,50h} (BX)
        cmp     al, 0               ; 0 is free
        jne     NewFood
        mov     ax, FOODCOLOR*256+1 ; 1 is food
        jmp     WritePlayfieldCell
; ----------------------------------
; IN (cx) OUT (al) MOD (bx)
ReadPlayfieldCell:
        movzx   bx, ch              ; CH is Row
        imul    bx, COLS
        add     bl, cl              ; CL is Column
        adc     bh, 0
        shl     bx, 1
        mov     al, [es:bx]
        ret
; ----------------------------------
; IN () OUT () MOD (ax..dx)
Status: mov     ax, [S.Length]
        mov     bx, ((ROWS-1)*COLS+5)*2
        mov     cx, 10
@@:     xor     dx, dx
        div     cx
        add     dx, 0F00h+'0'
        mov     [es:bx], dx
        sub     bx, 2
        test    ax, ax
        jnz     @b
        mov     byte [es:bx], ' '
        ret
; ----------------------------------
Msg     db      'G', 12, 'AME OVER', 0

        ALIGN   2
S       Snake   MIDP+SLEN-2, MIDP-SLEN, SLEN, 4Dh, 00010001b
Rand    dw      ?

Reading game information from a matrix of our own.

This solution is similar to reading the video buffer matrix but is both faster and more flexible. Faster because reading from VRAM is slow compared to reading from regular RAM, and more flexible because the screen can keep displaying all of the characters and all of the color combinations. To put 'faster' in some perspective: the 'MATRIX' program runs a typical cycle in 1.1 µsec, and the 'VRAM' program runs a cycle in 2.6 µsec. Does this matter? Not really, both programs spent 99.99% of their time in the necessary delay loop.

Because there is no shortage of memory, we can waste some and benefit from it. Even though the game board has fewer columns, we can setup our matrix for 256 columns. If we then store X in the low byte of an addres register like BX and Y in the high byte of that same address register, the reward will be that no conversion is needed to obtain the offset address BX within the matrix.

; The Snake Game - MATRIX (c) 2021 Sep Roland

        ORG     256

MODE=03h
COLS=80
ROWS=25
SLEN=COLS/8                         ; Initial length of snake
MIDP=((ROWS-1)/2)*256+(COLS/2)      ; Center of playfield
BACKCOLOR=66h                       ; Brown
FOODCOLOR=55h                       ; Magenta
SNAKECOLOR=22h                      ; Green
TIMER equ gs:046Ch                  ; BIOS.TimerTick

STRUC Snake a, b, c, d, e
 {
  .Head         dw      a
  .Tail         dw      b
  .Length       dw      c
  .Flow         db      d
  .Speed        db      e
 }

        cld
        xor     ax, ax
        mov     gs, ax
        mov     ax, 0B800h
        mov     es, ax              ; VRAM

        mov     ax, [TIMER]         ; Seed
        mov     [Rand], ax

        mov     ax, MODE            ; BIOS.SetVideoMode
        int     10h
        mov     dx, ROWS*256+0      ; Hide cursor
        mov     bh, 0
        mov     ah, 02h             ; BIOS.SetCursor
        int     10h

; Paint the playfield and matrix, draw the snake and food
        xor     di, di
        mov     cx, COLS*(ROWS-1)
        mov     ax, BACKCOLOR*256+0 ; 0 is free
        rep stosw

        mov     bx, 256*(ROWS-1)
@@:     dec     bx
        mov     [Mat+bx], al
        jnz     @b

        mov     bx, MIDP-SLEN       ; TailXY
        mov     ax, SNAKECOLOR*256+4Dh
@@:     call    WritePlayfieldCell  ; -> (CX)
        add     bl, 2               ; X+
        cmp     bl, (COLS/2)+SLEN-2 ; HeadX
        jbe     @b

        call    NewFood             ; -> (AX..DX)

; Show "GO" and wait for a keypress, then begin
        mov     dword [es:((ROWS-1)*COLS+4)*2], 0A4F0A47h
        mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        call    Status              ; -> (AX..DX)

Main:   mov     ax, [TIMER]         ; Sync with real time
@@:     cmp     ax, [TIMER]
        je      @b

.kbd:   mov     ah, 01h             ; BIOS.TestKey
        int     16h                 ; -> AX ZF
        jz      .show
        mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        cmp     al, 27              ; <ESC>
        je      Quit
        cmp     al, 32              ; <SPC>
        jne     .arrow

.speed: mov     al, 11111111b       ; Fast uses every tick
        cmp     [S.Speed], al
        jne     @f
        mov     al, 00010001b       ; Slow uses one out of four ticks
@@:     mov     [S.Speed], al
        jmp     .show

.arrow: mov     al, ah
        cmp     al, 4Dh             ; <RIGHT>
        je      @f
        cmp     al, 48h             ; <UP>
        je      @f
        cmp     al, 4Bh             ; <LEFT>
        je      @f
        cmp     al, 50h             ; <DOWN>
        jne     .show
@@:     mov     [S.Flow], al        ; AL={4Dh=X+, 48h=Y-, 4Bh=X-, 50h=Y+}

.show:  ror     [S.Speed], 1
        jnc     Main

        mov     al, [S.Flow]        ; {4Dh,48h,4Bh,50h}
        mov     bx, [S.Head]
        call    NextXY              ; -> BX
        cmp     byte [Mat+bx], 1
        je      .eat                ; 0 is free, 1 is food
        ja      DEAD                ; other is snake

.move:  call    NewHead             ; -> (AX..CX)
        call    NewTail             ; -> (AX..CX)
        jmp     Main

.eat:   call    NewHead             ; -> (AX..CX)
        inc     [S.Length]
        call    Status              ; -> (AX..DX)
        call    NewFood             ; -> (AX..DX)
        jmp     Main
; ----------------------------------
; Show "GAME OVER" and wait for <ESC>, then quit
DEAD:   mov     si, Msg
        mov     di, ((ROWS-1)*COLS+(COLS/2)-4)*2
        lodsw                       ; First char and color
@@:     stosw
        lodsb
        cmp     al, 0
        jne     @b

@@:     mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        cmp     al, 27              ; <ESC>
        jne     @b
; ---   ---   ---   ---   ---   ---
Quit:   mov     ax, 0003h           ; BIOS.SetVideoMode
        int     10h
        mov     ax, 4C00h           ; DOS.Terminate
        int     21h
; ----------------------------------
; IN (al,bx) OUT (bx)
NextXY: cmp     al, 4Dh             ; AL={4Dh,48h,4Bh,50h}
        jne     @f
        add     bl, 2               ; 2 character cells per playfield cell
        cmp     bl, COLS
        je      DEAD
        ret
@@:     cmp     al, 4Bh             ; AL={48h,4Bh,50h}
        jae     @f
        sub     bh, 1
        jb      DEAD
        ret
@@:     ja      @f
        sub     bl, 2
        jb      DEAD
        ret
@@:     add     bh, 1
        cmp     bh, ROWS-1
        je      DEAD
        ret
; ----------------------------------
; IN (al,bx) OUT () MOD (ax..cx)
NewHead:xchg    bx, [S.Head]
        mov     [Mat+bx], al
        mov     ah, SNAKECOLOR
        mov     bx, [S.Head]
; ---   ---   ---   ---   ---   ---
; IN (ax,bx) OUT () MOD (cx)
WritePlayfieldCell:
        mov     [Mat+bx], al
        movzx   cx, bh              ; BH is Row
        imul    cx, COLS
        add     cl, bl              ; BL is Column
        adc     ch, 0
        shl     cx, 1
        xchg    bx, cx
        mov     [es:bx+1], ah
        mov     [es:bx+3], ah
        mov     bx, cx
        ret
; ----------------------------------
; IN () OUT () MOD (ax..cx)
NewTail:mov     bx, [S.Tail]
        mov     al, [Mat+bx]        ; -> AL={4Dh,48h,4Bh,50h}
        call    NextXY              ; -> BX
        xchg    bx, [S.Tail]
        mov     ax, BACKCOLOR*256+0 ; 0 is free
        jmp     WritePlayfieldCell
; ----------------------------------
; IN () OUT () MOD (ax..dx)
NewFood:mov     ax, [Rand]
        imul    ax, 25173
        add     ax, 13849
        mov     [Rand], ax
        mov     cx, ROWS-1
        xor     dx, dx
        div     cx
        mov     bh, dl
        mov     ax, [Rand]
        mov     cx, COLS/2
        xor     dx, dx
        div     cx
        shl     dl, 1
        mov     bl, dl
        cmp     byte [Mat+bx], 0    ; 0 is free
        jne     NewFood
        mov     ax, FOODCOLOR*256+1 ; 1 is food
        jmp     WritePlayfieldCell
; ----------------------------------
; IN () OUT () MOD (ax..dx)
Status: mov     ax, [S.Length]
        mov     bx, ((ROWS-1)*COLS+5)*2
        mov     cx, 10
@@:     xor     dx, dx
        div     cx
        add     dx, 0F00h+'0'
        mov     [es:bx], dx
        sub     bx, 2
        test    ax, ax
        jnz     @b
        mov     byte [es:bx], ' '
        ret
; ----------------------------------
Msg     db      'G', 12, 'AME OVER', 0

        ALIGN   2
S       Snake   MIDP+SLEN-2, MIDP-SLEN, SLEN, 4Dh, 00010001b
Rand    rw      1
Mat     rb      256*(ROWS-1)

Reading the actual coordinates from a ringbuffer.

In this circular buffer we record the coordinates of all the snake's segments going from the head to the tail. The buffer's size must be such that it can accomodate the longest snake possible (or allowed by the rules). The program stores pointers to the first record (Head) and to behind the last record (Tail). For a new snake head, we lower the Head pointer and insert the new coordinates. For a new snake tail, we just lower the Tail pointer, discarding the last record.

Because we need to stay within the confines of the ringbuffer's memory, a wraparounding mechanism is needed. Choosing a power-of-two size for the ringbuffer's memory is important because then we can wraparound via a simple AND instruction. And if we choose this power-of-two size to be 65536, then we can drop this AND operation altogether since the CPU will already automatically wraparound at 64KB in the real address mode.

Searching the ringbuffer takes time, and this time will inevitably increase as the snake gets longer. However, in a program where, for playability reasons, more than 99% of the time is spent in a delay loop, it won't matter a bit!

; The Snake Game - RINGBUFFER (c) 2021 Sep Roland

        ORG     256

MODE=03h
COLS=80
ROWS=25
SLEN=COLS/8                         ; Initial length of snake
MIDP=((ROWS-1)/2)*256+(COLS/2)      ; Center of playfield
BACKCOLOR=66h                       ; Brown
FOODCOLOR=55h                       ; Magenta
SNAKECOLOR=22h                      ; Green
TIMER equ gs:046Ch                  ; BIOS.TimerTick

STRUC Snake a, b, c, d, e
 {
  .Head         dw      a
  .Tail         dw      b
  .Length       dw      c
  .Flow         db      d
  .Speed        db      e
 }

        cld
        xor     ax, ax
        mov     gs, ax
        mov     ax, cs
        add     ax, (EOF+15)/16
        mov     ss, ax              ; 512 bytes stack
        mov     sp, 512
        add     ax, 512/16
        mov     fs, ax              ; 64KB ringbuffer
        mov     ax, 0B800h
        mov     es, ax              ; VRAM

        mov     ax, [TIMER]         ; Seed
        mov     [Rand], ax

        mov     ax, MODE            ; BIOS.SetVideoMode
        int     10h
        mov     dx, ROWS*256+0      ; Hide cursor
        mov     bh, 0
        mov     ah, 02h             ; BIOS.SetCursor
        int     10h

; Paint the playfield, draw the snake and food
        xor     di, di
        mov     cx, COLS*(ROWS-1)
        mov     ax, BACKCOLOR*256+0
        rep stosw

        mov     cx, MIDP-SLEN       ; HeadXY==TailXY
@@:     call    NewHead             ; -> (AX..BX)
        add     cl, 2               ; X+
        cmp     cl, (COLS/2)+SLEN-2 ; HeadX
        jbe     @b

        call    NewFood             ; -> (AX..DX)

; Show "GO" and wait for a keypress, then begin
        mov     dword [es:((ROWS-1)*COLS+4)*2], 0A4F0A47h
        mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        call    Status              ; -> (AX..DX)

Main:   mov     ax, [TIMER]         ; Sync with real time
@@:     cmp     ax, [TIMER]
        je      @b

.kbd:   mov     ah, 01h             ; BIOS.TestKey
        int     16h                 ; -> AX ZF
        jz      .show
        mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        cmp     al, 27              ; <ESC>
        je      Quit
        cmp     al, 32              ; <SPC>
        jne     .arrow

.speed: mov     al, 11111111b       ; Fast uses every tick
        cmp     [S.Speed], al
        jne     @f
        mov     al, 00010001b       ; Slow uses one out of four ticks
@@:     mov     [S.Speed], al
        jmp     .show

.arrow: mov     al, ah
        cmp     al, 4Dh             ; <RIGHT>
        je      @f
        cmp     al, 48h             ; <UP>
        je      @f
        cmp     al, 4Bh             ; <LEFT>
        je      @f
        cmp     al, 50h             ; <DOWN>
        jne     .show
@@:     mov     [S.Flow], al        ; AL={4Dh=X+, 48h=Y-, 4Bh=X-, 50h=Y+}

.show:  ror     [S.Speed], 1
        jnc     Main

        mov     al, [S.Flow]        ; {4Dh,48h,4Bh,50h}
        mov     bx, [S.Head]
        mov     cx, [fs:bx]
        call    NextXY              ; -> CX
        cmp     cx, [FoodXY]
        je      .eat
        call    ScanSnake           ; -> ZF (BX)
        jnz     DEAD                ; CX is (X,Y) of some snake part

.move:  call    NewHead             ; -> (AX BX)
        call    NewTail             ; -> (AX..CX)
        jmp     Main

.eat:   call    NewHead             ; -> (AX BX)
        inc     [S.Length]
        call    Status              ; -> (AX..DX)
        call    NewFood             ; -> (AX..DX)
        jmp     Main
; ----------------------------------
; Show "GAME OVER" and wait for <ESC>, then quit
DEAD:   mov     si, Msg
        mov     di, ((ROWS-1)*COLS+(COLS/2)-4)*2
        lodsw                       ; First char and color
@@:     stosw
        lodsb
        cmp     al, 0
        jne     @b

@@:     mov     ah, 00h             ; BIOS.GetKey
        int     16h                 ; -> AX
        cmp     al, 27              ; <ESC>
        jne     @b
; ---   ---   ---   ---   ---   ---
Quit:   mov     ax, 0003h           ; BIOS.SetVideoMode
        int     10h
        mov     ax, 4C00h           ; DOS.Terminate
        int     21h
; ----------------------------------
; IN (al,cx) OUT (cx)
NextXY: cmp     al, 4Dh             ; AL={4Dh,48h,4Bh,50h}
        jne     @f
        add     cl, 2               ; 2 character cells per playfield cell
        cmp     cl, COLS
        je      DEAD
        ret
@@:     cmp     al, 4Bh             ; AL={48h,4Bh,50h}
        jae     @f
        sub     ch, 1
        jb      DEAD
        ret
@@:     ja      @f
        sub     cl, 2
        jb      DEAD
        ret
@@:     add     ch, 1
        cmp     ch, ROWS-1
        je      DEAD
        ret
; ----------------------------------
; IN (cx) OUT (ZF) MOD (bx)
ScanSnake:
        mov     bx, [S.Tail]
        mov     [fs:bx], cx         ; Sentinel
        mov     bx, [S.Head]
        sub     bx, 2
@@:     add     bx, 2
        cmp     [fs:bx], cx
        jne     @b
        cmp     bx, [S.Tail]
        ret
; ----------------------------------
; IN (cx) OUT () MOD (ax,bx)
NewHead:mov     bx, -2
        xadd    [S.Head], bx
        mov     [fs:bx-2], cx
        mov     ah, SNAKECOLOR
; ---   ---   ---   ---   ---   ---
; IN (ah,cx) OUT () MOD (bx)
WritePlayfieldCell:
        movzx   bx, ch              ; CH is Row
        imul    bx, COLS
        add     bl, cl              ; CL is Column
        adc     bh, 0
        shl     bx, 1
        mov     [es:bx+1], ah
        mov     [es:bx+3], ah
        ret
; ----------------------------------
; IN () OUT () MOD (ax..cx)
NewTail:mov     bx, -2
        xadd    [S.Tail], bx
        mov     cx, [fs:bx-2]
        mov     ah, BACKCOLOR
        jmp     WritePlayfieldCell
; ----------------------------------
; IN () OUT () MOD (ax..dx)
NewFood:mov     ax, [Rand]
        imul    ax, 25173
        add     ax, 13849
        mov     [Rand], ax
        mov     bx, ROWS-1
        xor     dx, dx
        div     bx
        mov     ch, dl
        mov     ax, [Rand]
        mov     bx, COLS/2
        xor     dx, dx
        div     bx
        shl     dl, 1
        mov     cl, dl
        call    ScanSnake           ; -> ZF (BX)
        jnz     NewFood             ; CX is (X,Y) of some snake part
        mov     [FoodXY], cx
        mov     ah, FOODCOLOR
        jmp     WritePlayfieldCell
; ----------------------------------
; IN () OUT () MOD (ax..dx)
Status: mov     ax, [S.Length]
        mov     bx, ((ROWS-1)*COLS+5)*2
        mov     cx, 10
@@:     xor     dx, dx
        div     cx
        add     dx, 0F00h+'0'
        mov     [es:bx], dx
        sub     bx, 2
        test    ax, ax
        jnz     @b
        mov     byte [es:bx], ' '
        ret
; ----------------------------------
Msg     db      'G', 12, 'AME OVER', 0

        ALIGN   2
S       Snake   SLEN*2, SLEN*2, SLEN, 4Dh, 00010001b
FoodXY  dw      ?
Rand    dw      ?

EOF:

这篇关于典型的蛇游戏.如何跟踪蛇?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆