分类

课内:
不限
类型:
不限 游戏 项目 竞赛 个人研究 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
年份:
不限 2018 2019

资源列表

  • Linux环境下基于GTK的CS聊天系统

    1 项目综述本项目是以Linux C 为主体,使用网络套接字编写,并且具有图形界面(GTK)的可视化聊天室软件。完整实现私人聊天与群组聊天,传文件,日志处理,离线消息,管理员管理功能,信息修改等功能。
    2 开发平台与工具
    编辑器:vim gedit
    编译器:gcc
    调试工具:gdb
    开发平台:fredoa 21 linux4.0.6-200.fc21.x86_64

    3 核心模块思路综述3.1 私聊模块客户端发送信息时,需要发送IP/名称,然后发送信息到服务器端,通过服务器端根据IP/名称,确定其链接套接字,然后发送信息到对应的客户端。就是使用服务器作为中转站。将消息转发到对方的套接字中。
    3.2 群聊模块服务器每接收一条信息就将这条信息,发送给已经为人聊天状态的所有用户。
    3.3 文件传送类似于传送消息,FTP功能只有在处理文件传输功能的时候才会被开启,首先,当需要发送文件的时候,打开文件传输窗口,选择相关文件以及目标对象的名字,发送一个询问语句,等待对方接受。测试经过服务器的转发对方已经接受到了一个文件传送的消息或是显示于聊天窗口,或是显示在FTP 专用窗口,但是只有在FTP主窗口中点击RECV按钮,方可接受,这时服务器首先转发RECV 消息,当消息转发完成后,客户端接受到RECV 信息,确认文件名,开始传输文件,当文件传输完毕后发送一个传送完成的信号,其中如果文件大于800K,那就增加分片标识机制。进一步设计:首先发送端,点击发送按钮,此时立即发送一个基本的数据包,其中包括需要传送的文件,然后有一个读取语句,将后边的发送语句挂起等待一个条件变量,此时对方收发到了先遣数据包,当对方点击接受按钮时,首先会发送一个带有接受标识的数据包用来改变对方的条件变量随后开启接受线程等待最终文件接受完毕后,对方会发送一个文件已经传送完成信号,此时最后一个数据包被接受,跳出循环,线程终止。
    3.4 好友处理模块添加一种新的消息包类型,用新的类型来标记好友相关信息:
    其中好友处理函数又包含三个方面。刷新,添加,删除。这三个字函数,他们根据消息传第的不同内容作出三种响应,最终完成好友相关操作。其中,服务器首先接受一个添加好友的请求,然后将请求转发给目标用户,前提:目标在线,暂不考率目标离线情况(离线默认也可以添加,对方会收到一个被添加的离线消息)。然后由目标给与一个反馈信息,此时需求客户端并不是阻塞模式,可以正常收发信息。才用“发出后不管”的策略。其时每个客户端都在等待一个加它为好友的信息。待客户端成功得到结果后,就将反馈信息反馈给服务器,最终由服务器来完成加好友操作工作。
    3.5 离线消息模块对方发送一个消息,服务器判断目标是否在线,如果是离线就暂时存储在服务器端的目标文件中,对方上线后会首先读取这个文件以达到得到离线消息的目的。
    4 线程池技术应用针对不同的客户端分配给它一个线程,这个线程调用work( )函数,work( ): 首先从服务器的存储用户信息的文件中读取所有用户信息进链表然后便利链表查找相应信息,如果没有发送错误信息并且关闭链接套接字,断开本次链接。如果查找成功,修改标志值,保持链接存在,创建发送,获取数据的两个线程,与客户端进行通信。上线后首先读取离线文件,如果文件里有东西就将文件内的离线消息打印出来接下来读取发送过来的信息确定是要私人聊天还是多人聊天,根据标识与IP等信息确定聊天状态,用户成功登录或者注册后,使用gtk弹出一个窗口,让用户选择当前需要的聊天模式,私人聊天,多人聊天,以及文件发送总共三个模块,用户选择后则进入各自不同的界面,聊天模块都是相类似的两个界面,文件传输为一个单独的模块。
    关于服务器上文件的相应处理与使用,首先服务器存在一个服务器-客户文件(server)这个文件用来存储所有客户的信息其中包括用户名,密码,登录状态,套接字等,每次客户登录时首先刷新读取这个文件一遍,将其中信息读取到一个公共的链表上,其次改变自己的状态,与描述符,将这些信息改变后即刻写入文件中(应当使用读写锁),以便于下一个用户在登录后刷新读取链表时得到的是一个正确的链表信息。所以服务器在使用链表前都应当刷新读取文件进链表一边(或者在下线时在改变相关文件的时候,同时更改链表的信息,这种方法更有可行性)。
    5 运行展示5.1 开始菜单
    5.2 登陆示例
    5.3 功能菜单
    5.4 私人聊天功能
    5.5 群组聊天
    5.6 文件传输


    5.7 修改信息和管理员
    1 评论 17 下载 2018-11-06 17:03:09 下载需要5点积分
  • 基于HTML5实现的一笔画小游戏

    1 游戏介绍一笔画是图论中一个著名的问题,它起源于柯尼斯堡七桥问题。数学家欧拉在他1736年发表的论文《柯尼斯堡的七桥》中不仅解决了七桥问题,也提出了一笔画定理,顺带解决了一笔画问题。用图论的术语来说,对于一个给定的连通图存在一条恰好包含所有线段并且没有重复的路径,这条路径就是「一笔画」。
    寻找连通图这条路径的过程就是「一笔画」的游戏过程,如下:

    2 游戏的实现「一笔画」的实现不复杂,笔者把实现过程分成两步:

    底图绘制
    交互绘制

    「底图绘制」把连通图以「点线」的形式显示在画布上,是游戏最容易实现的部分;「交互绘制」是用户绘制解题路径的过程,这个过程会主要是处理点与点动态成线的逻辑。
    2.1 底图绘制「一笔画」是多关卡的游戏模式,笔者决定把关卡(连通图)的定制以一个配置接口的形式对外暴露。对外暴露关卡接口需要有一套描述连通图形状的规范,而在笔者面前有两个选项:

    点记法
    线记法

    举个连通图 ——— 五角星为例来说一下这两个选项。

    点记法如下:
    levels: [ // 当前关卡 { name: "五角星", coords: [ {x: Ax, y: Ay}, {x: Bx, y: By}, {x: Cx, y: Cy}, {x: Dx, y: Dy}, {x: Ex, y: Ey}, {x: Ax, y: Ay} ] } ...]
    线记法如下:
    levels: [ // 当前关卡 { name: "五角星", lines: [ {x1: Ax, y1: Ay, x2: Bx, y2: By}, {x1: Bx, y1: By, x2: Cx, y2: Cy}, {x1: Cx, y1: Cy, x2: Dx, y2: Dy}, {x1: Dx, y1: Dy, x2: Ex, y2: Ey}, {x1: Ex, y1: Ey, x2: Ax, y2: Ay} ] }]
    「点记法」记录关卡通关的一个答案,即端点要按一定的顺序存放到数组 coords中,它是有序性的记录。「线记法」通过两点描述连通图的线段,它是无序的记录。「点记法」最大的优势是表现更简洁,但它必须记录一个通关答案,笔者只是关卡的搬运工不是关卡创造者,所以笔者最终选择了「线记法」。
    2.2 交互绘制在画布上绘制路径,从视觉上说是「选择或连接连通图端点」的过程,这个过程需要解决2个问题:

    手指下是否有端点
    选中点到待选中点之间能否成线

    收集连通图端点的坐标,再监听手指滑过的坐标可以知道「手指下是否有点」。以下伪代码是收集端点坐标:
    // 端点坐标信息let coords = []; lines.forEach(({x1, y1, x2, y2}) => { // (x1, y1) 在 coords 数组不存在 if(!isExist(x1, y1)) coords.push([x1, y1]); // (x2, y2) 在 coords 数组不存在 if(!isExist(x2, y2)) coords.push([x2, y2]); });
    以下伪代码是监听手指滑动:
    easel.addEventListener("touchmove", e => { let x0 = e.targetTouches[0].pageX, y0 = e.targetTouches[0].pageY; // 端点半径 ------ 取连通图端点半径的2倍,提升移动端体验 let r = radius * 2; for(let [x, y] of coords){ if(Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0), 2) <= r){ // 手指下有端点,判断能否连线 if(canConnect(x, y)) { // todo } break; } } })
    在未绘制任何线段或端点之前,手指滑过的任意端点都会被视作「一笔画」的起始点;在绘制了线段(或有选中点)后,手指滑过的端点能否与选中点串连成线段需要依据现有条件进行判断。

    上图,点A与点B可连接成线段,而点A与点C不能连接。笔者把「可以与指定端点连接成线段的端点称作有效连接点」。连通图端点的有效连接点从连通图的线段中提取:
    coords.forEach(coord => { // 有效连接点(坐标)挂载在端点坐标下 coord.validCoords = []; lines.forEach(({x1, y1, x2, y2}) => { // 坐标是当前线段的起点 if(coord.x === x1 && coord.y === y1) { coord.validCoords.push([x2, y2]); } // 坐标是当前线段的终点 else if(coord.x === x2 && coord.y === y2) { coord.validCoords.push([x1, y1]); } })})
    但是,有效连接点只能判断两个点是否为底图的线段,这只是一个静态的参考,在实际的「交互绘制」中,会遇到以下情况:

    如上图,AB已串连成线段,当前选中点B的有效连接点是 A 与 C。AB 已经连接成线,如果 BA 也串连成线段,那么线段就重复了,所以此时 BA 不能成线,只有 AC 才能成线。
    对选中点而言,它的有效连接点有两种:

    与选中点「成线的有效连接点」
    与选中点「未成线的有效连接点」

    其中「未成线的有效连接点」才能参与「交互绘制」,并且它是动态的。

    回头本节内容开头提的两个问题「手指下是否有端点」 与 「选中点到待选中点之间能否成线」,其实可合并为一个问题:手指下是否存在「未成线的有效连接点」。只须把监听手指滑动遍历的数组由连通图所有的端点坐标 coords 替换为当前选中点的「未成线的有效连接点」即可。
    3 自动识图笔者在录入关卡配置时,发现一个7条边以上的连通图很容易录错或录重线段。笔者在思考能否开发一个自动识别图形的插件,毕竟「一笔画」的图形是有规则的几何图形。

    上面的关卡「底图」,一眼就可以识出三个颜色:

    白底
    端点颜色
    线段颜色

    并且这三种颜色在「底图」的面积大小顺序是:白底 > 线段颜色 > 端点颜色。底图的「采集色值表算法」很简单,如下伪代码:
    let imageData = ctx.getImageData(); let data = imageData.data; // 色值表let clrs = new Map(); for(let i = 0, len = data.length; i < len; i += 4) { let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]]; let key = `rgba(${r}, ${g}, ${b}, ${a})`; let value = clrs.get(key) || {r, g, b, a, count: 0}; clrs.has(key) ? ++value.count : clrs.set(rgba, {r, g, b, a, count});}
    对于连通图来说,只要把端点识别出来,连通图的轮廓也就出来了。
    3.1 端点识别理论上,通过采集的「色值表」可以直接把端点的坐标识别出来。笔者设计的「端点识别算法」分以下2步:

    按像素扫描底图直到遇到「端点颜色」的像素,进入第二步
    从底图上清除端点并记录它的坐标,返回继续第一步

    伪代码如下:
    for(let i = 0, len = data.length; i < len; i += 4) { let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]]; // 当前像素颜色属于端点 if(isBelongVertex(r, g, b, a)) { // 在 data 中清空端点 vertex = clearVertex(i); // 记录端点信息 vertexes.push(vertext); }}
    但是,上面的算法只能跑无损图。笔者在使用了一张手机截屏做测试的时候发现,收集到的「色值表」长度为 5000+ !这直接导致端点和线段的色值无法直接获得。
    经过分析,可以发现「色值表」里绝大多数色值都是相近的,也就是在原来的「采集色值表算法」的基础上添加一个近似颜色过滤即可以找出端点和线段的主色。伪代码实现如下:
    let lineColor = vertexColor = {count: 0}; for(let clr of clrs) { // 与底色相近,跳过 if(isBelongBackground(clr)) continue; // 线段是数量第二多的颜色,端点是第三多的颜色 if(clr.count > lineColor.count) { [vertexColor, lineColor] = [lineColor, clr] }}
    取到端点的主色后,再跑一次「端点识别算法」后居识别出 203 个端点!这是为什么呢?

    上图是放大5倍后的底图局部,蓝色端点的周围和内部充斥着大量噪点(杂色块)。事实上在「端点识别」过程中,由于噪点的存在,把原本的端点被分解成十几个或数十个小端点了,以下是跑过「端点识别算法」后的底图:

    通过上图,可以直观地得出一个结论:识别出来的小端点只在目标(大)端点上集中分布,并且大端点范围内的小端点叠加交错。
    如果把叠加交错的小端点归并成一个大端点,那么这个大端点将十分接近目标端点。小端点的归并伪代码如下:
    for(let i = 0, len = vertexes.length; i < len - 1; ++i) { let vertexA = vertexes[i]; if(vertextA === undefined) continue; // 注意这里 j = 0 而不是 j = i +1 for(let j = 0; j < len; ++j) { let vertexB = vertexes[j]; if(vertextB === undefined) continue; // 点A与点B有叠加,点B合并到点A并删除点B if(isCross(vertexA, vertexB)) { vertexA = merge(vertexA, vertexB); delete vertexA; } }}
    加了小端点归并算法后,「端点识别」的准确度就上去了。经笔者本地测试已经可以 100% 识别有损的连通图了。
    3.2 线段识别笔者分两个步骤完成「线段识别」:

    给定的两个端点连接成线,并采集连线上N个「样本点」;
    遍历样本点像素,如果像素色值不等于线段色值则表示这两个端点之间不存在线段

    如何采集「样式点」是个问题,太密集会影响性能;太疏松精准度不能保证。
    在笔者面前有两个选择:N 是常量;N 是变量。
    假设 N === 5。局部提取「样式点」如下:

    上图,会识别出三条线段:AB, BC 和 AC。而事实上,AC不能成线,它只是因为 AB 和 BC 视觉上共一线的结果。当然把 N 值向上提高可以解决这个问题,不过 N 作为常量的话,这个常量的取量需要靠经验来判断,果然放弃。
    为了避免 AB 与 BC 同处一直线时 AC 被识别成线段,其实很简单 ——— 两个「样本点」的间隔小于或等于端点直径。
    假设 N = S / (2 * R),S 表示两点的距离,R 表示端点半径。局部提取「样式点」如下:

    如上图,成功地绕过了 AC。「线段识别算法」的伪代码实现如下:
    for(let i = 0, len = vertexes.length; i < len - 1; ++i) { let {x: x1, y: y1} = vertexes[i]; for(let j = i + 1; j < len; ++j) { let {x: x2, y: y2} = vertexes[j]; let S = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); let N = S / (R * 2); let stepX = (x1 - x2) / N, stepY = (y1 - y2) / n; while(--N) { // 样本点不是线段色 if(!isBelongLine(x1 + N * stepX, y1 + N * stepY)) break; } // 样本点都合格 ---- 表示两点成线,保存 if(0 === N) lines.push({x1, y1, x2, y2}) }}
    4 性能优化由于「自动识图」需要对图像的的像素点进行扫描,那么性能确实是个需要关注的问题。笔者设计的「自动识图算法」,在识别图像的过程中需要对图像的像素做两次扫描:「采集色值表」 与 「采集端点」。在扫描次数上其实很难降低了,但是对于一张 750 * 1334 的底图来说,「自动识图算法」需要遍历两次长度为 750 * 1334 * 4 = 4,002,000 的数组,压力还是会有的。笔者是从压缩被扫描数组的尺寸来提升性能的。
    被扫描数组的尺寸怎么压缩?
    笔者直接通过缩小画布的尺寸来达到缩小被扫描数组尺寸的。伪代码如下:
    // 要压缩的倍数let resolution = 4; let [width, height] = [img.width / resolution >> 0, img.height / resolution >> 0];ctx.drawImage(img, 0, 0, width, height); let imageData = ctx.getImageData(), data = imageData;
    把源图片缩小 4 倍后,得到的图片像素数组只有原来的 4^2 = 16倍,这在性能上是很大的提升。
    1 评论 24 下载 2018-10-31 20:09:18 下载需要4点积分
  • 基于HTML5实现的贪吃蛇小游戏

    1 游戏介绍贪吃蛇的经典玩法有两种:

    积分闯关
    一吃到底

    第一种是笔者小时候在掌上游戏机最先体验到的(不小心暴露了年龄),具体玩法是蛇吃完一定数量的食物后就通关,通关后速度会加快;第二种是诺基亚在1997年在其自家手机上安装的游戏,它的玩法是吃到没食物为止。笔者要实现的就是第二种玩法。
    2 MVC设计模式基于贪吃蛇的经典,笔者在实现它时也使用一种经典的设计模型:MVC(即:Model - View - Control)。游戏的各种状态与数据结构由 Model 来管理;View 用于显示 Model 的变化;用户与游戏的交互由 Control 完成(Control 提供各种游戏API接口)。
    Model 是游戏的核心也是本文的主要内容;View 会涉及到部分性能问题;Control 负责业务逻辑。 这样设计的好处是: Model完全独立,View 是 Model 的状态机,Model 与 View 都由 Control 来驱动。
    2.1 Model看一张贪吃蛇的经典图片。

    贪吃蛇有四个关键的参与对象:

    蛇(snake)
    食物(food)
    墙(bounds)
    舞台(zone)

    舞台是一个 m * n 的矩阵(二维数组),矩阵的索引边界是舞台的墙,矩阵上的成员用于标记食物和蛇的位置。
    空舞台如下:
    [ [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0],]
    食物(F)和蛇(S)出现在舞台上:
    [ [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,F,0,0,0,0,0,0,0], [0,0,0,S,S,S,S,0,0,0], [0,0,0,0,0,0,S,0,0,0], [0,0,0,0,S,S,S,0,0,0], [0,0,0,0,S,0,0,0,0,0], [0,0,0,0,S,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0],]
    由于操作二维数组不如一维数组方便,所以笔者使用的是一维数组, 如下:
    [ 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,F,0,0,0,0,0,0,0, 0,0,0,S,S,S,S,0,0,0, 0,0,0,0,0,0,S,0,0,0, 0,0,0,0,S,S,S,0,0,0, 0,0,0,0,S,0,0,0,0,0, 0,0,0,0,S,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,]
    舞台矩阵上蛇与食物只是舞台对二者的映射,它们彼此都有独立的数据结构:

    蛇是一串坐标索引链表
    食物是一个指向舞台坐标的索引值

    2.1.1 蛇的活动蛇的活动有三种,如下:

    移动(move)
    吃食(eat)
    碰撞(collision)

    2.1.1.1 移动蛇在移动时,内部发生了什么变化?

    蛇链表在一次移动过程中做了两件事:向表头插入一个新节点,同时剔除表尾一个旧节点。用一个数组来代表蛇链表,那么蛇的移动就是以下的伪代码:
    function move(next) { snake.pop() & snake.unshift(next); }
    数组作为蛇链表合适吗?
    这是笔者最开始思考的问题,毕竟数组的 unshift & pop 可以无缝表示蛇的移动。不过,方便不代表性能好,unshift 向数组插入元素的时间复杂度是 O(n),pop 剔除数组尾元素的时间复杂度是 O(1)。
    蛇的移动是一个高频率的动作,如果一次动作的算法复杂度为 O(n) 并且蛇的长度比较大,那么游戏的性能会有问题。笔者想实现的贪吃蛇理论上讲是一条长蛇,所以笔者在本文章的回复是 ——— 数组不适合作为蛇链表。
    蛇链表必须是真正的链表结构。
    链表删除或插入一个节点的时间复杂度为O(1),用链表作为蛇链表的数据结构能提高游戏的性能。javascript 没有现成的链表结构,笔者写了一个叫 Chain 的链表类,Chain 提供了 unshfit & pop。以下伪代码是创建一条蛇链表:
    let snake = new Chain();
    2.1.1.2 吃食 & 碰撞「吃食」与「碰撞」区别在于吃食撞上了「食物」,碰撞撞上了「墙」。笔者认为「吃食」与「碰撞」属于蛇一次「移动」的三个可能结果的两个分支。蛇移动的三个可能结果是:「前进」、「吃食」和「碰撞」。
    回头看一下蛇移动的伪代码:
    function move(next) { snake.pop() & snake.unshift(next); }
    代码中的 next 表示蛇头即将进入的格子的索引值,只有当这个格子是 0 时蛇才能「前进」,当这个格子是 S 表示「碰撞」自己,当这个格子是 F 表示吃食。
    好像少了撞墙?
    笔者在设计过程中,并没有把墙设计在舞台的矩阵中,而是通过索引出界的方式来表示撞墙。简单地说就是 next === -1 时表示出界和撞墙。
    以下伪代码表示蛇的整上活动过程:
    // B 表示撞墙let cell = -1 === next ? B : zone[next]; switch(cell) { // 吃食 case F: eat(); break; // 撞到自己 case S: collision(S); break; // 撞墙 case B: collision(B): break; // 前进 default: move; }
    2.1.2 随机投食随机投食是指随机挑选舞台的一个索引值用于映射食物的位置。这似乎很简单,可以直接这样写:
    // 伪代码food = Math.random(zone.length) >> 0;
    如果考虑到投食的前提 ——— 不与蛇身重叠,你会发现上面的随机代码并不能保证投食位置不与蛇身重叠。由于这个算法的安全性带有赌博性质,且把它称作「赌博算法」。为了保证投食的安全性,笔者把算法扩展了一下:
    // 伪代码function feed() { let index = Math.random(zone.length) >> 0; // 当前位置是否被占用 return zone[index] === S ? feed() : index; }food = feed();
    上面的代码虽然在理论上可以保证投食的绝对安全,不过笔者把这个算法称作「不要命的赌徒算法」,因为上面的算法有致命的BUG ——— 超长递归 or 死循环。
    为了解决上面的致命问题,笔者设计了下面的算法来做随机投食:
    // 伪代码function feed() { // 未被占用的空格数 let len = zone.length - snake.length; // 无法投食 if(len === 0) return ; // zone的索引 let index = 0, // 空格计数器 count = 0, // 第 rnd 个空格子是最终要投食的位置 rnd = Math.random() * count >> 0 + 1; // 累计空格数 while(count !== rnd) { // 当前格子为空,count总数增一 zone[index++] === 0 && ++count; } return index - 1; }food = feed();
    这个算法的平均复杂度为 O(n/2)。由于投食是一个低频操作,所以 O(n/2)的复杂度并不会带来任何性能问题。不过,笔者觉得这个算法的复杂度还是有点高了。回头看一下最开始的「赌博算法」,虽然「赌博算法」很不靠谱,但是它有一个优势 ——— 时间复杂度为 O(1)。
    「赌博算法」的靠谱概率 = (zone.length - snake.length) / zone.length。snake.length 是一个动态值,它的变化范围是:0 ~ zone.length。推导出「赌博算法」的平均靠谱概率是:

    「赌博算法」平均靠谱概率 = 50%

    看来「赌博算法」还是可以利用一下的。于是笔者重新设计了一个算法:
    // 伪代码function bet() { let rnd = Math.random() * zone.length >> 0; return zone[rnd] === 0 ? rnd : -1; }function feed() { ...}food = bet(); if(food === -1) food = feed();
    新算法的平均复杂度可以有效地降低到 O(n/4),人生有时候需要点运气。
    2.2 View在 View 可以根据喜好选择一款游戏渲染引擎,笔者在 View 层选择了 PIXI 作为游戏游戏渲染引擎。
    View 的任务主要有两个:

    绘制游戏的界面;
    渲染 Model 里的各种数据结构

    也就是说 View 是使用渲染引擎还原设计稿的过程。本文的目的是介绍「贪吃蛇」的实现思路,如何使用一个渲染引擎不是本文讨论的范畴,笔者想介绍的是:「如何提高渲染的效率」。
    在 View 中显示 Model 的蛇可以简单地如以下伪代码:
    // 清空 View 上的蛇view.snake.clean(); model.snake.forEach( (node) => { // 创建 View 上的蛇节点 let viewNode = createViewNode(node); // 并合一条新蛇 view.snake.push(viewNode); });
    上面代码的时间复杂度是 O(n)。上面介绍过蛇的移动是一个高频的活动,我们要尽量避免高频率地运行 O(n) 的代码。来分析蛇的三种活动:「移动」,「吃食」,「碰撞」。
    首先,Model 发生了「碰撞」,View 应该是直接暂停渲染 Model 里的状态,游戏处在死亡状态,接下来的事由 Control 处理。
    Model 中的蛇(链表)在一次「移动」过程中做了两件事:向表头插入一个新节点,同时剔除表尾一个旧节点;蛇(链表)在一次「吃食」过程中只做一件事:向表头插入一个新节点。

    如果在 View 中对 Model 的蛇链表做差异化检查,View 只增量更新差异部分的话,算法的时间复杂度即可降低至 O(1) ~ O(2) 。以下是优化后的伪代码:
    let snakeA = model.snake, snakeB = view.snake; // 增量更新尾部while(snakeB.length <= snakeA.length) { headA = snakeA.next(); // 头节点匹配 if(headA.data === headB.data) break; // 不匹配 else { // 向snakeB插入头节点 if(snakeA.HEAD === headA.index) { snakeB.unshift(headA.data); } // 向snakeB插入第二个节点 else snakeB.insertAfter(0, headA.data); }}// 增量更新头部 let tailA = snakeA.last(), tailB; while(snakeB.length !== 0) { tailB = snakeB.last(); // 尾节点匹配 if(tailA.data === tailB.data) break; // 不匹配 else snakeB.pop(); }
    2.3 ControlControl 主要做 3 件事:

    游戏与用户的互动
    驱动 Model
    同步 View 与 Model

    「游戏与用户的互动」是指向外提供游戏过程需要使用到的 APIs 与 各类事件。笔者规划的 APIs 如下:



    name
    type
    detail




    init
    method
    初始化游戏


    start
    method
    开始游戏


    restart
    method
    重新开始游戏


    pause
    method
    暂停


    resume
    method
    恢复


    turn
    method
    控制蛇的转向。如:turn(“left”)


    destroy
    method
    销毁游戏


    speed
    property
    蛇的移动速度



    事件如下:



    name
    detail




    countdown
    倒时计


    eat
    吃到食物


    before-eat
    吃到食物前触发


    gameover
    游戏结束



    事件统一挂载在游戏实例下的 event 对象下。
    snake.event.on("countdown", (time) => console.log("剩余时间:", time));
    「驱动 Model 」只做一件事 ——— 将 Model 的蛇的方向更新为用户指定的方向。
    「同步 View 与 Model 」也比较简单,检查 Model 是否有更新,如果有更新通知 View 更新游戏界面。
    1 评论 14 下载 2018-10-31 18:11:26 下载需要7点积分
  • 基于HTML5实现的消灭星星小游戏

    1 游戏介绍「消灭星星」是一款很经典的「消除类游戏」,它的玩法很简单:消除相连通的同色砖块。

    「消灭星星」存在多个版本,不过它们的规则除了「关卡分值」有些出入外,其它的规则都是一样的。笔者介绍的版本的游戏规则整理如下:
    1. 色砖分布

    10 x 10 的表格
    5种颜色 ——— 红、绿、蓝,黄,紫
    每类色砖个数在指定区间内随机
    5类色砖在 10 x 10 表格中随机分布

    2. 消除规则
    两个或两个以上同色砖块相连通即是可被消除的砖块。
    3. 分值规则

    消除总分值 = n * n * 5
    奖励总分值 = 2000 - n * n * 20

    「n」表示砖块数量。上面是「总」分值的规则,还有「单」个砖块的分值规则:

    消除砖块得分值 = 10 * i + 5
    剩余砖块扣分值 = 40 * i + 20

    「i」表示砖块的索引值(从 0 开始)。简单地说,单个砖块「得分值」和「扣分值」是一个等差数列。
    4. 关卡分值
    关卡分值 = 1000 + (level - 1) * 2000;「level」即当前关卡数。
    5. 通关条件

    可消除色块不存在
    累计分值 >= 当前关卡分值

    上面两个条件同时成立游戏才可以通关。
    2 MVC 设计模式笔者这次又是使用了 MVC 模式来写「消灭星星」。星星「砖块」的数据结构与各种状态由 Model 实现,游戏的核心在 Model 中完成;View 映射 Model 的变化并做出对应的行为,它的任务主要是展示动画;用户与游戏的交互由 Control 完成。
    从逻辑规划上看,Model 很重而View 与 Control 很轻,不过,从代码量上看,View 很重而 Model 与 Control 相对很轻。
    2.1 Model10 x 10 的表格用长度为 100 的数组可完美映射游戏的星星「砖块」。
    [ R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P, R, R, G, G, B, B, Y, Y, P, P]
    R - 红色,G - 绿色,B - 蓝色,Y - 黄色,P - 紫色。Model 的核心任务是以下四个:

    生成砖墙
    消除砖块 (生成砖块分值)
    夯实砖墙
    清除残砖 (生成奖励分值)

    2.1.1 生成砖墙砖墙分两步生成:

    色砖数量分配
    打散色砖

    理论上,可以将 100 个格子可以均分到 5 类颜色,不过笔者玩过的「消灭星星」都不使用均分策略。通过分析几款「消灭星星」,其实可以发现一个规律 ——— 「色砖之间的数量差在一个固定的区间内」。
    如果把传统意义上的均分称作「完全均分」,那么「消灭星星」的分配是一种在均分线上下波动的「不完全均分」。

    笔者把上面的「不完全均分」称作「波动均分」,算法的具体实现可以参见「波动均分算法」。
    「打散色砖」其实就是将数组乱序的过程,笔者推荐使用「 费雪耶兹乱序算法」。
    以下是伪代码的实现:
    // 波动均分色砖waveaverage(5, 4, 4).forEach( // tiles 即色墙数组 (count, clr) => tiles.concat(generateTiles(count, clr)); ); // 打散色砖shuffle(tiles);
    2.1.2 消除砖块「消除砖块」的规则很简单 ——— 相邻相连通相同色即可以消除。

    前两个组合符合「相邻相连通相同色即可以消除」,所以它们可以被消除;第三个组合虽然「相邻相同色」但是不「相连通」所以它不能被消除。
    「消除砖块」的同时有一个重要的任务:生成砖块对应的分值。在「游戏规则」中,笔者已经提供了对应的数学公式:「消除砖块得分值 = 10 * i + 5」。
    「消除砖块」算法实现如下:
    function clean(tile) { let count = 1; let sameTiles = searchSameTiles(tile); if(sameTiles.length > 0) { deleteTile(tile); while(true) { let nextSameTiles = []; sameTiles.forEach(tile => { nextSameTiles.push(...searchSameTiles(tile)); makeScore(++count * 10 + 5); // 标记当前分值 deleteTile(tile); // 删除砖块 }); // 清除完成,跳出循环 if(nextSameTiles.length === 0) break; else { sameTiles = nextSameTiles; } } }}
    清除的算法使用「递归」逻辑上会清晰一些,不过「递归」在浏览器上容易「栈溢出」,所以笔者没有使用「递归」实现。
    2.1.3 夯实砖墙砖墙在消除了部分砖块后,会出现空洞,此时需要对墙体进行夯实:










    向下夯实
    向左夯实
    向左下夯实(先下后左)



    一种快速的实现方案是,每次「消除砖块」后直接遍历砖墙数组(10x10数组)再把空洞夯实,伪代码表示如下:
    for(let row = 0; row < 10; ++row) { for(let col = 0; col < 10; ++col) { if(isEmpty(row, col)) { // 水平方向(向左)夯实 if(isEmptyCol(col)) { tampRow(col); } // 垂直方向(向下)夯实 else { tampCol(col); } break; } }}
    但是,为了夯实一个空洞对一张大数组进行全量遍历并不是一种高效的算法。在笔者看来影响「墙体夯实」效率的因素有:

    定位空洞
    砖块移动(夯实)

    扫描墙体数组的主要目的是「定位空洞」,但能否不扫描墙体数组直接「定位空洞」?
    墙体的「空洞」是由于「消除砖块」造成的,换种说法 ——— 被消除的砖块留下来的坑位就是墙体的空洞。在「消除砖块」的同时标记空洞的位置,这样就无须全量扫描墙体数组,伪代码如下:
    function deleteTile(tile) { // 标记空洞 markHollow(tile.index); // 删除砖块逻辑 ...}
    在上面的夯实动图,其实可以看到它的夯实过程如下:

    空洞上方的砖块向下移动
    空列右侧的砖块向左移动

    墙体在「夯实」过程中,它的边界是实时在变化,如果「夯实」不按真实边界进行扫描,会产生多余的空白扫描:

    如何记录墙体的边界?
    把墙体拆分成一个个单独的列,那么列最顶部的空白格片段就是墙体的「空白」,而其余非顶部的空白格片段即墙体的「空洞」。

    笔者使用一组「列集合」来描述墙体的边界并记录墙体的空洞,它的模型如下:
    /* @ count - 列砖块数 @ start - 顶部行索引 @ end - 底部行索引 @ pitCount - 坑数 @ topPit - 最顶部的坑 @ bottomPit - 最底部的坑*/ let wall = [ {count, start, end, pitCount, topPit, bottomPit}, {count, start, end, pitCount, topPit, bottomPit}, ...];
    这个模型可以描述墙体的三个细节:

    空列
    列的连续空洞
    列的非连续空洞

    // 空列if(count === 0) { ...}// 连续空洞else if(bottomPit - topPit + 1 === pitCount) { ...}// 非连续空洞else { ...}
    砖块在消除后,映射到单个列上的空洞会有两种分布形态 ——— 连续与非连续。

    「连续空洞」与「非连续空洞」的夯实过程如下:

    其实「空列」放大于墙体上,也会有「空洞」类似的分布形态 ——— 连续与非连续。

    它的夯实过程与空洞类似,这里就不赘述了。
    2.1.4 消除残砖上一小节提到了「描述墙体的边界并记录墙体的空洞」的「列集合」,笔者是直接使用这个「列集合」来消除残砖的,伪代码如下:
    function clearAll() { let count = 0; for(let col = 0, len = this.wall.length; col < len; ++col) { let colInfo = this.wall[col]; for(let row = colInfo.start; row <= colInfo.end; ++row) { let tile = this.grid[row * this.col + col]; tile.score = -20 - 40 * count++; // 标记奖励分数 tile.removed = true; } }}
    2.2 ViewView 主要的功能有两个:

    UI 管理
    映射 Model 的变化(动画)

    UI 管理主要是指「界面绘制」与「资源加载管理」,这两项功能比较常见本文就直接略过了。View 的重头戏是「映射 Model 的变化」并完成对应的动画。动画是复杂的,而映射的原理是简单的,如下伪代码:
    update({originIndex, index, clr, removed, score}) { // 还没有 originIndex 或没有色值,直接不处理 if(originIndex === undefined || clr === undefined) return ; let tile = this.tiles[originIndex]; // tile 存在,判断颜色是否一样 if(tile.clr !== clr) { this.updateTileClr(tile, clr); } // 当前索引变化 ----- 表示位置也有变化 if(tile.index !== index) { this.updateTileIndex(tile, index); } // 设置分数 if(tile.score !== score) { tile.score = score; } if(tile.removed !== removed) { // 移除或添加当前节点 true === removed ? this.bomb(tile) : this.area.addChild(tile.sprite); tile.removed = removed; }}
    Model 的砖块每次数据的更改都会通知到 View 的砖块,View 会根据对应的变化做对应的动作(动画)。
    2.3 ControlControl 要处理的事务比较多,如下:

    绑定 Model & View
    生成通关分值
    判断通关条件
    对外事件
    用户交互

    初始化时,Control 把 Model 的砖块单向绑定到 View 的砖块了。如下:
    Object.defineProperties(model.tile, { originIndex: { get: () => {...}, set: () => { ... view.update({originIndex}); } }, index: { get: () => {...}, set: () => { ... view.update({index}); } }, clr: { get: () => {...}, set: () => { ... view.update({clr}); } }, removed: { get: () => {...}, set: () => { ... view.update({removed}); } }, score: { get: () => {...}, set: () => { ... view.update({score}); } }, });
    「通关分值」与「判断通关条件」这对逻辑在本文的「游戏规则」中有相关介绍,这里不再赘述。
    对外事件规划如下:



    name
    detail




    pass
    通关


    pause
    暂停


    resume
    恢复


    gameover
    游戏结束



    用户交互 APIs 规划如下:



    name
    type
    deltail




    init
    method
    初始化游戏


    next
    method
    进入下一关


    enter
    method
    进入指定关卡


    pause
    method
    暂停


    resume
    method
    恢复


    destroy
    method
    销毁游戏



    3 问题在知乎有一个关于「消灭星星」的话题:popstar关卡是如何设计的?
    这个话题在最后提出了一个问题 ——— 「无法消除和最大得分不满足过关条件的矩阵」。

    「无法消除的矩阵」其实就是最大得分为0的矩阵,本质上是「最大得分不满足过关条件的矩阵」。
    最大得分不满足过关条件的矩阵
    求「矩阵」的最大得分是一个 「背包问题」,求解的算法不难:对当前矩阵用「递归」的形式把所有的消灭分支都执行一次,并取最高分值。但是 javascript 的「递归」极易「栈溢出」导致算法无法执行。
    其实在知乎的话题中提到一个解决方案:

    网上查到有程序提出做个工具随机生成关卡,自动计算,把符合得分条件的关卡筛选出来

    这个解决方案代价是昂贵的!笔者提供有源码并没有解决这个问题,而是用一个比较取巧的方法:进入游戏前检查是事为「无法消除矩阵」,如果是重新生成关卡矩阵。
    注意:笔者使用的取巧方案并没有解决问题。
    1 评论 6 下载 2018-10-31 18:07:28 下载需要6点积分
  • 基于VS2012和SQL SERVER的餐厅点餐系统设计与实现

    一、需求分析1. 面向对象点典点菜系统是一款面向顾客和管理人员的全方面系统,其中管理人员又包括服务员、采购员和厨师。本组认真分析了不同对象的需求,为不同的对象都设计了独特的系统功能。简化了传统点菜、烧菜和采购方式繁琐的步骤,为顾客提供便捷操作的同时提高酒店管理的效率。
    2. 市场背景目前传统的点菜、烧菜和采购方式繁琐复杂。在人才配置方面,任何的餐厅或酒店都要配置大量的基层服务人员,这些基层服务人员大多从事简单的任务。例如:誊写顾客点菜的名称或代号、沟通顾客与厨房的材料状况、沟通采购员和厨房的材料状况。而这些工作恰恰是能由计算机代替人力来劳动的。在工作效率方面,传统的方式有着极大的时延缺陷。例如:服务员在为顾客点菜时并不能及时了解材料的剩余情况。如果材料已近用完了服务员又不知道,在顾客点菜后又要去询问顾客是否要取消或更改点菜情况。这样不仅极大地降低工作效率,还有可能引起顾客的不满。又或者材料用完了不能及时通知采购员,会降低销售额,不利于经营。
    3. 运行环境
    操作系统及版本:Windows XP、Win7开发环境:VS 2012数据库环境:SQL Server 2005
    二、数据库设计1. 数据字典
    餐桌信息表:餐桌号、座位号、餐桌位置、餐桌使用情况顾客表:顾客号、餐桌号、顾客信息菜单表:菜号、菜名、价格、类别账单表:账单号、顾客号、菜号、数量、价格部门表:部门号、部门名员工表:员工号、员工密码、员工名、性别、部门号、工种进货表:商品号、员工号、商品名、进货数量、进货日期商品表:商品号、商品名、商品价格
    2. 概念模型点餐子系统E-R图

    餐厅内部管理子系统E-R图

    3. 表格合并化简过程
    Dinner_Table:Table_ID,Table_Seat_quantity,Table_Position,Table_SituationTable_Use:Table_ID,Client_IDClient:Client_ID,Client_MessageDishes_List:Dishes_ID,Dishes_Name,Dishes_Price,Dishes_ClassificationBill:Bill_ID,Dishes_ID,QuantityOrders:Bill_ID,Client_IDEmployee:Employee_ID,Employee_Password,Employee_Name,SexDepartment:Department_ID,Department_NameDept_Belonging:Employee_ID,Department_IDGoods:Good_ID,Good_Name,Goods_PricePurchase:Goods_ID,Employee_ID,Good_Quantity,Purchase_Date
    (加粗标注为关系集合)
    合并表格后得:

    Dinner_Table:Table_ID,Table_Seat_quantity,Table_Position,Table_SituationClient:Client_ID,Table_ID,Client_Message(将餐桌使用表和顾客表合并)Dishes_List:Dishes_ID,Dishes_Name,Dishes_Price,Dishes_ClassificationBill:Bill_ID,Client_ID,Dishes_ID,Quantity(将账单表和点菜表合并)Employee:Employee_ID,Employee_Password,Department_ID,Employee_Name,Sex(将员工表和部门隶属表合并)Department:Department_ID,Department_NameGoods:Good_ID,Good_Name,Goods_PricePurchase:Goods_ID,Employee_ID,Good_Quantity,Purchase_Date
    (加粗标注为合并表格后新增属性)
    4. 建表代码Create table Dinner_Table( Table_ID char(3) not null , Seat_Num int not null, Table_Position varchar(20), Table_Situation varchar(10) Primary key(Table_ID));
    Create table Client( Client_ID char(6) not null,--从000001开始 Table_ID char(3), Client_Message varchar(10) Primary key(Client_ID), Foreign key(Table_ID) references Dinner_Table On delete cascade On update cascade );
    Create table Dishes_List( Dishes_ID char(6) not null , Dishes_Name varchar(20) not null, Dishes_Price int not null, Dishes_Classification varchar(10) not null, Primary key (Dishes_ID ));
    Create table Bill ( Bill_ID char(6) not null , Client_ID char(6) not null, Dishes_ID char(6) not null, Quantity int not null, Price int, Foreign key(Client_ID ) references Client On delete cascade On update cascade, Foreign key(Dishes_ID) references Dishes_List On delete cascade On update cascade, Primary key(Bill_ID,Dishes_ID));
    Create table Department( Department_ID char(6), Department_Name varchar(10), Primary key(Department_ID));
    Create table Employee( Employee_ID char(11), Employee_Password varchar(6) not null, Employee_Name varchar(20), Employee_Sex char(2) check(Employee_Sex='男' or Employee_Sex='女'), Department_ID char(6), Employee_Style char(10), Primary key(Employee_ID), Foreign key(Department_ID) references Department On delete cascade On update cascade );
    Create table Purchase( Goods_ID char(6), Employee_ID char(11), Goods_Name char(20), Goods_Quantity int, Purchase_Date char(10), Foreign key(Employee_ID) references Employee On delete cascade On update cascade, Primary key(Goods_ID));
    Create table Goods( Goods_ID char(6), Goods_Name varchar(20), Goods_Price int, Foreign key(Goods_ID) references Goods On delete cascade On update cascade Primary key(Goods_ID));
    三、系统功能模块说明本系统主要分为点餐和餐馆内部管理两个大的功能模块。
    在点餐模块,程序分为管理终端和顾客终端。设想的是在每台餐桌上都有一台用以点餐的pc终端,顾客可以通过此终端浏览菜品进行点菜,同时可以和餐馆管理端进行通信,实时获得所需的服务。在管理终端,可以直观地看到各台餐桌的使用情况以及各个顾客的点餐信息,同时可以和各台顾客进行通信。程序截图如下:
    管理端登录界面

    输入错误时输出提示信息

    管理端工作界面

    使用不同图标表示餐桌的使用情况

    和客户端的通信

    客户端开始界面

    客户端功能菜单窗口

    客户点餐

    客户查看菜单

    客户修改菜单

    客户与管理端的通信界面

    客户点击结账时看到的账单界面

    餐馆内部管理模块,在此模块我设置了不同的接口,对不同的登录员工显示不同的界面。程序截图如下:
    采购员登录后显示的界面(为方便管理,设置了查找功能)

    餐馆管理员登录后显示的界面

    管理员对本系统所有表格均能进行更新修改

    四、项目总结1. 问题项目开发中遇到的主要问题是多台客户端和管理终端的通信。比如客户在对管理端发起通话时,若管理端未打开对话窗口,要如何将收到的信息暂存起来,并且如何在管理端打开对话框后正确的显示;其次还有管理端与多个客户端通信时要如何做到信息的正确发送给对应的客户。然后因为是网络软件,和平时简单的写个程序调用数据库不同,这里采用的是各个终端连接远程的数据库,因为没有接触过,费了一番周折。其次还有一些小的问题,比如列表控件显示图标,子、父窗口之间数据参数的传递。同时程序中还需处理大量事先约定的带特定意义的字符串,以此辨别此次通信的目的,否则程序中在通信方面涉及多种功能,如果为每一种功能都维系一个通信套接字对资源的利用将非常低。
    2. 解决方法对于第一个问题,没有采用进程间通信,而是在通信对话框未打开时由其父窗口接收信息并写入临时文件夹,当打开通信对话框时由初始化函数将信息从临时文件夹读出显示在编辑框上,这样也实现了聊天记录的保存。
    对于第二个问题,采用iocp框架的服务器,维护各个客户端与管理端通信的套接字,以便准确转发各种信息。同时完成端口框架的服务器也非常适合处理多用户并发通信的问题。
    五、参考资料
    《VC++ 深入详解》
    《Visual C++ 网络通信开发入门与编程实践》
    《Visual C++ 网络编程经典案例详解》
    《C++ Primer》
    http://blog.sina.com.cn/s/blog_625ef6610101g4qj.html
    http://blog.163.com/notepad_2008/blog/static/48206602200802353810418/
    http://blog.csdn.net/ristal/article/details/6652020
    http://blog.sina.com.cn/s/blog_6d0730c70100thvi.html
    http://bbs.csdn.net/topics/390620910?page=1
    http://blog.sina.com.cn/s/blog_5f30147a0100dzgx.html
    http://www.cnblogs.com/mgtwei/archive/2012/08/27/2659365.html
    1 评论 42 下载 2018-10-06 00:06:12 下载需要9点积分
  • Anti-Rootkit(ARK)内核级系统防护软件KsBinSword的设计与实现

    KsBinSword是一斩断黑手的利刃,它适用于Windows 2000/XP/2003操作系统,用于查探系统中的幕后黑手(木马后门)并作出处理,当然使用它需要用户有一些操作系统的知识。
    KsBinSword内部功能是十分强大的。它有着自己的独创核心态进程管理方案、简洁而不失效率的网络防火墙、强大而稳定的文件过滤驱动、深入磁盘底层甚至穿透还原软件的磁盘微端口驱动。可能您也用过很多类似功能的软件,比如一些进程工具、端口工具,但是现在的系统级后门功能越来越强,一般都可轻而易举地隐藏进程、端口、注册表、文件信息,一般的工具根本无法发现这些“幕后黑手”。KsBinSword使用大量新颖的内核技术,使得这些后门躲无所躲。
    本程序分为应用态与核心态两部分。
    1 应用层程序采用VS2005编写,负责与核心态交互通信,将核心态的信息处理后反馈至用户。应用层使用标准的MFC框架,分为:

    CAboutDlg:About对话框所在类
    CEditEx:重载了的编辑框类
    CHexEdit:重载了的十六进制编辑框类
    CKsBinSwordApp:程序主应用框架类
    CKsBinSwordDlg:程序主界面类
    CMyList:重载了的列表框类
    CMySystem:静态系统函数类
    CPage1:进程管理页的类
    CPage2:监控管理页的类
    CPage3:驱动模块枚举页的类
    CPage4:列举LSP页的类
    CPage5:SSDT页的类
    CPage6:文件管理页的类
    CPage7:磁盘编辑器页的类
    CPage8:防火墙页的类
    CPage9:PE文件查看页的类
    CRuleDlg:防火墙规则对话框类
    CTrayIcon:系统托盘类

    其中CMysystem类比较重要,它封装了对驱动操作、各个系统函数调用等操作,各个类都要用到。程序主界面如下图所示:

    主界面上,又划分为九个子界面。分别为:进程管理、系统监控、LSP管理、SSDT检测部分、文件管理、磁盘编辑、防火墙部分、PE信息查看和系统模块列举。
    1.1 进程管理
    内核级进程、线程检测,顺利查找各种病毒隐藏的进程、线程
    细致的内核级模块检测,深刻扫描系统潜在危险模块
    强大的内核级进程、线程结束,尚无病毒能够抵挡

    1.2 系统监控
    使用当前杀毒软件最新HIPS技术(主动防御),防范病毒于未然
    U盘扫描、脚本木马查杀,确保用户中毒后第一时间清理系统

    1.3 LSP管理
    列举系统socket所依赖的动态链接库,揭发病毒隐藏之处
    1.4 SSDT检测部分
    完全彻底扫描系统所有SSDT(系统服务派发表),病毒无遁形之处
    从系统原生文件还原SSDT,确保系统未修改,阻断病毒自我防护

    1.5 文件管理
    强大的文件过滤驱动技术,彻底检测所有隐藏文件,安全可靠。独创的强制删除文件功能,绕过FSD(文件系统驱动),底层删除文件。
    1.6 磁盘编辑
    方便的十六进制编辑器,完美模拟WinHex功能
    强大的底层磁盘编辑,甚至穿透还原卡,读取写入任何被保护扇区

    1.7 防火墙部分
    自定义安全规则,拦截一切未知数据包,更灵活的保护系统
    实时反馈系统网络数据流,提供网络详细信息

    1.8 PE信息查看
    详细列举PE文件信息,如导入表,区块等,方便了解未知文件结构。
    1.9 系统模块列举
    列举系统所有模块,查杀潜在内核级病毒威胁
    2 内核层驱动层采用DDK 2003编写,共四个NT驱动程序,分别为:

    KsBinSword.sys:负责处理进线程相关检测、查杀
    SIoctl.sys:负责处理硬盘编程
    DrvFltIp.sys:负责处理防火墙相关部分
    Explore.sys:负责处理文件编辑相关部分

    3 应用层各个功能实现原理下面结合代码详细介绍应用层各个功能及其实现。
    3.1 进程管理3.1.1 普通列举进程本功能封装在消息响应函数CPage1::OnBnClickedListProcess()中。采用系统PSAPI.LIB库导出的函数CreateToolhelp32Snapshot()、Process32First()、Process32Next()等函数工作。属于应用态列举进程。取得进程PID后,将使用我们独创的My_OpenProcess()打开进程句柄。
    My_OpenProcess()的功能类似于普通的OpenProcess()作用,但功能更为强大。我们知道一些病毒等软件为了防止自己被杀毒软件结束,会采用一定的防御手段。为了关闭进程,进程句柄是必不可少的。所以很多病毒会在OpenProcess()上采用API HOOK技术保护自己不被结束。而我们的CMySystem::My_OpenProcess()将完美绕过,并且采用了一定的新颖的微软未公开技术。
    My_OpenProcess()实现原理简介
    Windows在子系统进程CSRSS.EXE里维护了一张整个应用层句柄表。通过系统未文档函数ZwQuerySystemInformation()将得到这张表。然后遍历所有句柄,如果是进程句柄则通过ZwDuplicateObject()将句柄复制到本进程,并调用ZwQueryInformationProcess()查询是否为我们需要打开的进程句柄。如果是则返回,否则继续遍历。
    通过以上手段,应用层很少病毒能躲过KsBinSword的扫描。但不排除一些内核级的ROOTKIT(既一些底层核心态病毒)使用篡改系统内核技术逃过杀毒软件的检测。这时我们可以采用驱动列举进程功能。
    3.1.2 驱动列举进程本功能封装在函数CMySystem::ListProcessByDrive()中。
    实现原理简介
    在Windows下,所有资源都是以对象方式进行管理。我们要访问一个对象时,系统就会创建一个对象句柄。句柄和对象之间是通过句柄表来完成的。准确来说,一个句柄是它所对应的对象在句柄表中的索引。PspCidTable是Windows系统上一个特殊的句柄表。它不链接在系统句柄表上,也不属于任何进程。通过它可以访问系统任何对象。
    在Windows XP中,为了节省系统空间,采用了动态扩展的表结构。当句柄表数目少的时候仅采用下层表。数目增大后才采用更多的层。最多有三层句柄表。当我们获得三层句柄表后,我们就可以通过句柄来访问对象了。
    利用PspCidTable来检测隐藏进程的基本原理正是如此,系统内所有进程对象的对象类型是一样的,先取得任一进程对象的对象类型,然后访问所有句柄值,是进程对象则记录下来。下面是实现代码:
    VOID IsValidProcess(){ //判断是否是进程对象,是则记录,不是则放弃 ULONG PspCidTable; ULONG TableCode; ULONG table1,table2; ULONG object,objectheader; ULONG NextFreeTableEntry; ULONG processtype,type; ULONG flags; ULONG i; PspCidTable=GetCidAddr(); processtype=GetProcessType(); if(PspCidTable==0) { return ; } else { //TableCode的最后两位在XP中决定了句柄表的层数 TableCode=*(PULONG)(*(PULONG)PspCidTable); if((TableCode&0x3)==0x0) { table1=TableCode; table2=0x0; } if((TableCode&0x3)==0x1) { TableCode=TableCode&0xfffffffc; table1=*(PULONG)TableCode; table2=*(PULONG)(TableCode+0x4); } //对cid从0x0到0x4e1c进行遍历 for(i=0x0;i<0x4e1c;i++) { if(i<=0x800) { if(MmIsAddressValid((PULONG)(table1+i*2))) { object=*(PULONG)(table1+i*2); if(MmIsAddressValid((PULONG)(table1+i*2+NEXTFREETABLEENTRY))) { NextFreeTableEntry=*(PULONG)(table1+i*2+NEXTFREETABLEENTRY); if(NextFreeTableEntry==0x0)//正常的_HANDLE_TABLE_ENTRY中NextFreeTableEntry为0x0 { object=((object | 0x80000000)& 0xfffffff8);//转换为对象(体)指针 objectheader=(ULONG)OBJECT_TO_OBJECT_HEADER(object);//获取对象(头)指针 if(MmIsAddressValid((PULONG)(objectheader+TYPE))) { type=*(PULONG)(objectheader+TYPE); if(type==processtype) { flags=*(PULONG)((ULONG)object+FLAGS); if((flags&0xc)!=0xc) RecordInfo(object);//flags显示进程没有退出 } } } } } } else { if(table2!=0) { if(MmIsAddressValid((PULONG)(table2+(i-0x800)*2))) { object=*(PULONG)(table2+(i-0x800)*2); if(MmIsAddressValid((PULONG)((table2+(i-0x800)*2)+NEXTFREETABLEENTRY))) { NextFreeTableEntry=*(PULONG)((table2+(i-0x800)*2)+NEXTFREETABLEENTRY); if(NextFreeTableEntry==0x0) { object=((object | 0x80000000)& 0xfffffff8); objectheader=(ULONG)OBJECT_TO_OBJECT_HEADER(object); if(MmIsAddressValid((PULONG)(objectheader+TYPE))) { type=*(PULONG)(objectheader+TYPE); if(type==processtype) { flags=*(PULONG)((ULONG)object+FLAGS); if((flags&0xc)!=0xc) RecordInfo(object); } } } } } } } } }}
    上面解决了检测进程功能。但PspCidTable是未被Windows导出的,属于未文档结构。下面的代码负责查找PspCidTable:
    //通过搜索PsLookupProcessByProcessId函数,获取PspCidTable的地址ULONG GetCidAddr(){ PUCHAR addr; PUCHAR p; UNICODE_STRING pslookup; ULONG cid; RtlInitUnicodeString (&pslookup, L"PsLookupProcessByProcessId"); addr = (PUCHAR) MmGetSystemRoutineAddress(&pslookup);//MmGetSystemRoutineAddress可以通过函数名获得函数地址 for (p=addr;p<addr+PAGE_SIZE;p++) { if((*(PUSHORT)p==0x35ff)&&(*(p+6)==0xe8)) { cid=*(PULONG)(p+2); return cid; break; } } return 0;}
    具体细节的补充说明:

    本程序所使用的结构都是在Windows xp sp2下实现的,所以移植性比较差
    这种检测方式是针对系统句柄 ,所以可以从结果看出不存在系统句柄的System IDIE Process 进程无法列举
    因为进程的退出也是基于句柄的,所以存在进程已经退出而进程对象仍然存在的情况。这种情况可以通过EPROCESS结构中的ProcessExiting等标志位来判断是否退出

    3.1.3 结束进程结束进程我们提供了三种方式:

    普通TerminateProcess()法结束进程,封装在CPage1::OnMenuKillProcessByTer()
    强制清零法结束进程封装在CMySystem::KillProcess()
    驱动调用PspTerminateProcess()结束进程,封装在CMySystem::ForceKillProcess()中

    清零法
    程序调用ZwProtectVirtualMemory()和ZwWriteVirtualMemory() 等函数,强制将目标进程的ring3层的地址空间清除为零。由于连异常处理等Windows特定结构都被清除,故目标进程甚至连异常对话框都不会出现便自动被Windows内存进程管理器消除进程执行体等进程标志,此为目前ring3层最强的结束进程法。
    驱动层结束进程将在下面的驱动部分再行介绍。
    3.1.4 模块列举本功能封装在CPage1::ListProDllByQueryVirMem()中。
    实现方法
    通过ZwQueryVirtualMemory()函数暴力搜索目标进程应用层任何一处位置,并得到响应的地址信息,如果是模块的话列举出来。目前绝大多数工具查找模块也是通过Toolhlp32、psapi,前者会调用RtlDebug***函数向目标注入远线程,后者会用调试api读取目标进程内存,本质上都是对PEB的枚举,通过修改PEB就轻易让这些工具找不到北了。而KsBinSword的核心态方案原原本本地将模块展示,病毒无所逃匿。
    3.1.5 线程列举本功能封装在CMySystem::ListThread(void) 中。
    线程列举完全使用了内核态方案,在驱动中遍历线程结构体ETHREAD,通过ETHREAD中的双向链表完成线程列举。完全杜绝了病毒的一些常规拦截操作。
    3.1.6 线程结束线程结束我们提供了两种方案:

    基于应用层的TerminateThread()的结束进程。封装在CMySystem::KillProcess()中。原理是创建远程线程,注入目标进程中,再调用TerminateThread()的结束进
    基于核心态的PspTerminateThread()结束进程。原理是内核态搜索未导出函数PspTerminateThread()结束进程。

    3.2 监控配置3.2.1 进程监控本功能封装在CPage2::OnBnClickedOk()中。应用层传递消息控制字给内核层,内核层SSDT挂钩了内核函数ZwCreateProcess(),对每个新创建进程进行用户询问。
    3.2.2 注册表监控本功能封装在CPage2::OnBnClickedCancel()中。应用层传递消息控制字给内核层,内核层SSDT挂钩了内核函数ZwSetValueKey(),对每个注册表访问进行用户询问。
    3.2.3 模块监控本功能封装在CPage2:: OnBnClickedButton1 ()中。应用层传递消息控制字给内核层,内核层SSDT挂钩了内核函数ZwLoadDriver(),对每个内核模块加载进行用户询问。
    3.2.4 U盘辅助插件本功能对U盘可疑文件(如AUTORUNS.INF)进行彻底查杀,在源头上封堵了U盘病毒的来源
    3.2.5 脚本木马查杀本功能采用特征码杀毒方式,能全盘扫描脚本木马,速度快,稳定性高,可靠性好。
    3.3 驱动模块检测本功能采用两种不同的方式列举系统驱动:ZwQuerySystemInformation()和ZwQueryDirectoryObject()方式。前者属于常规法,容易遭到病毒拦截,而后者列举了系统的对象目录,极少数病毒会注意到这个地方,所以采用这种方式查找病毒安全又可靠。本来我们打算移进内核态。但由于这两个函数能在应用态调用,为了增强稳定性,就在应用层实现了。
    3.4 LSP枚举LSP枚举我们采用了API: WSCEnumProtocols()、WSCGetProviderPath()
    遍历每个socket协议链得到相应模块路径。病毒有可能更改这个协议链表,所以这里列举出来给用户自行判断。
    3.5 SSDT操作3.5.1 SSDT枚举本部分操作比较多。分别封装两个函数:CPage5::ShowSSDT(void), OnReShowSSDT()中。CPage5::ShowSSDT(void) 调用BOOL CMySystem::EnumSSDT(IN HANDLE hDriver ) 枚举SSDT。
    实现原理
    从系统内核读取出SSDT表,然后使用PE文件操作,从系统内核文件ntoskrnl.exe中分别读取PE头部->数据目录->导出表->导出目录表->函数名数组指针。再通过内核中得到ntoskrnl.exe在内存中的基址,根据上述各数据得到相应的SSDT函数在内存中的正确地址,通过与前述得到的数据相对比,判断是否SSDT被更改。关键函数代码:
    //枚举SSDTBOOL CMySystem::EnumSSDT( IN HANDLE hDriver ){ HINSTANCE hNtDllInst = NULL; ULONG ulNtDllOffset; ULONG ulFuncNameCount = 0; PIMAGE_EXPORT_DIRECTORY pImgExpDir = NULL; PULONG pFuncNameArray = NULL; ULONG i; BOOL bOK = TRUE; do { RealCount = 0; //个数清 if( pList ) //还有存没有释放 { DestroyModList( pList ); //释放它 pList = NULL; } pList = CreateModList( &NTBase ); //创建模块信息链表,顺便得到NT基址 if( pList == NULL ) //创建失败 { bOK = FALSE; break; } if( !( hNtDllInst = LoadLibrary( L"ntdll" ) ) ) { bOK = FALSE; break; } ///////////////////////////////////////////////////////// //分配SSDT保存缓冲表 //得到SSDT个数 SSDT ssdt; if( !GetSSDT( hDriver, &ssdt ) ) { bOK = FALSE; break; } if( TotalSSDTCount == -1 ) //得到SSDT个数失败 { bOK = FALSE; break; } if( pSSDTST ) //pSSDTST已有值,先释放它 { free( pSSDTST ); pSSDTST = NULL; } pSSDTST = (pSSDTSaveTable)malloc( TotalSSDTCount * sizeof( SSDTSaveTable ) ); if( pSSDTST == NULL ) { bOK = FALSE; break; } for( i = 0; i < TotalSSDTCount; i ++ ) //初始化它 { ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulServiceNumber = -1; ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress = 0L; ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulOriginalFunctionAddress = 0L; memset( ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ServiceFunctionName, \ 0, \ sizeof(((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ServiceFunctionName)); memset( ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ModuleName, \ 0, \ sizeof(((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ModuleName)); } ///////////////////////////////////////////////////////// //枚举 ulNtDllOffset = (ULONG)hNtDllInst; //PE头部 ulNtDllOffset += ((PIMAGE_DOS_HEADER)hNtDllInst)->e_lfanew + sizeof( DWORD ); //数据目录 ulNtDllOffset += sizeof( IMAGE_FILE_HEADER ) + sizeof( IMAGE_OPTIONAL_HEADER ) - IMAGE_NUMBEROF_DIRECTORY_ENTRIES * sizeof( IMAGE_DATA_DIRECTORY ); //导出表 ulNtDllOffset = (DWORD)hNtDllInst + ((PIMAGE_DATA_DIRECTORY)ulNtDllOffset)->VirtualAddress; //导出目录表 pImgExpDir = (PIMAGE_EXPORT_DIRECTORY)ulNtDllOffset; //得到函数个数 ulFuncNameCount = pImgExpDir->NumberOfNames; //函数名数组指针 pFuncNameArray = (PULONG)( (ULONG)hNtDllInst + pImgExpDir->AddressOfNames ); ///////////////////// //循环找函数名 for( i = 0; i < ulFuncNameCount; i ++ ) { //函数名 PCSTR pszName = (PCSTR)( pFuncNameArray[i] + (ULONG)hNtDllInst ); if( pszName[0] == 'N' && pszName[1] == 't' ) //Nt 开头的函数 { //查找表 LPWORD pOrdNameArray = (LPWORD)( (ULONG)hNtDllInst + pImgExpDir->AddressOfNameOrdinals ); //函数地址 LPDWORD pFuncArray = (LPDWORD)( (ULONG)hNtDllInst + pImgExpDir->AddressOfFunctions ); //函数代码 LPCVOID pFuncCode = (LPCVOID)( (ULONG)hNtDllInst + pFuncArray[pOrdNameArray[i]] ); //获取服务号 SSDTEntry EntryCode; memcpy( &EntryCode, pFuncCode, sizeof( SSDTEntry ) ); if( EntryCode.byMov == 0xB8 ) // MOV EAX, XXXX { ULONG ulAddr = 0; if( !GetHook( hDriver, EntryCode.ulIndex, &ulAddr ) ) { bOK = FALSE; break; } //////////////////////// //通过地址得到模块名 char ModNameBuf[MAX_PATH+1]; memset( ModNameBuf, 0, sizeof( ModNameBuf ) ); if( GetModuleNameByAddr( ulAddr, pList, ModNameBuf, sizeof( ModNameBuf )-1 ) ) { memcpy( \ ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ModuleName, \ ModNameBuf, \ sizeof( ModNameBuf ) \ ); } //////////////////////////////////////////////////// //保存SSDT信息到缓冲表中 ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ulServiceNumber = \ EntryCode.ulIndex; //服务号 ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress = \ ulAddr; //当前函数地址 memcpy( \ ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ServiceFunctionName, \ pszName, \ sizeof( ((pSSDTSaveTable)((ULONG)pSSDTST + RealCount * sizeof(SSDTSaveTable)))->ServiceFunctionName ) ); RealCount ++; } } } } while( FALSE ); ::FreeLibrary( hNtDllInst ); if( bOK ) //成功 { //获取剩下的服务号 for( i = RealCount; i < TotalSSDTCount; i ++ ) { if( !GetHook( hDriver, i, &((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress ) ) { bOK = FALSE; break; } //////////////////////// //通过地址得到模块名 char ModNameBuf[MAX_PATH+1]; memset( ModNameBuf, 0, sizeof( ModNameBuf ) ); if( GetModuleNameByAddr( \ ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ulCurrentFunctionAddress, \ pList, ModNameBuf, sizeof( ModNameBuf )-1 ) ) { memcpy( \ ((pSSDTSaveTable)((ULONG)pSSDTST + i * sizeof(SSDTSaveTable)))->ModuleName, \ ModNameBuf, \ sizeof( ModNameBuf ) \ ); } } //按服务号进行排序 SSDTSTOrderByServiceNum( pSSDTST ); //获取原始函数地址 GetOldSSDTAddress(); } if( pList ) { DestroyModList( pList ); //释放模块链表 pList = NULL; } return bOK;}
    3.5.2 SSDT恢复该功能封装在CMySystem::SetSSDT()中。这个函数只是一个用户层通信函数,真正恢复函数在内核态中。内核态将用户层传来的SSDT函数正确地址,写入到内存中。其中为了防止被中断打乱,关闭了一次中断,操作完成后恢复中断。
    3.6 文件管理器本文件管理采用了独创的文件过滤驱动技术,未使用windows API,能查探、删除目前已知的绝大多数病毒、Rootkit等。
    文件管理应用层负责与核心态通信。下面分析应用层框架:
    3.6.1 列目录、文件CPage6::OnItemexpanding(NMHDR *pNMHDR, LRESULT *pResult) 函数负责响应文件管理器中树型控件的节点展开消息。然后将点击的路径传送至内核,由文件过滤驱动构造IRP包发送至相应卷驱动。根据卷驱动返回的数据包传递给应用层。应用层通过CPage6::IsMediaValid()、CPage6::IsPathValid()、CPage6::AddDirectoryNodes()判断路径下是否有文件,是何种文件,并将信息添加至树型控件节点。相应的消息控制字为:IOCTL_MT_GETDIRINF、IOCTL_MT_GETDIRNUMINF。其中最重要的函数代码如下:
    UINT CPage6::AddDirectoryNodes(HTREEITEM hItem, CString &strPathName){ WCHAR wBuf[60]; DWORD bytesReturned=0; ULONG num=0; PDIRECTORY_INFO temp={0}; DIRECTORY_INFO_EX DIRECTORY_INFO_b; CString str,str1,strFileSpec = strPathName; if (strFileSpec.Right (1) != "\\") strFileSpec += "\\"; char a[MAX_PATH]; str1=strFileSpec; CMySystem::WCHARToChar(a,strFileSpec.GetBuffer(strFileSpec.GetLength())); strFileSpec += "*.*"; DeviceIoControl(hDevice,(DWORD)IOCTL_MT_GETDIRNUMINF,a,sizeof(a),&num,sizeof(ULONG),&bytesReturned,NULL); if(num==0) { AfxMessageBox(L"驱动未加载,列举出错!"); return 0; } temp=(PDIRECTORY_INFO)calloc(num,sizeof(DIRECTORY_INFO)); if(temp==NULL) { return 0; } DeviceIoControl(hDevice,(DWORD)IOCTL_MT_GETDIRINF,a,sizeof(a),temp,num*sizeof(DIRECTORY_INFO),&bytesReturned,NULL); CWaitCursor wait; WCHAR wTemp[MAX_PATH]={'\0'}; m_FileList.DeleteAllItems(); index=0; SetPath(str1,hDevice); for(ULONG i=0;i<num;i++) { TRACE("AddDirectoryNode:%d\n",i); CMySystem::CharToWCHAR(wTemp,temp[i].FileName); str.Format(L"%s",wTemp); str=str1+str; CString strFileName = (LPCTSTR) &temp[i].FileName; if(PathIsDirectory(str)) { if(strcmp(temp[i].FileName,".")) { if(strcmp(temp[i].FileName,"..")) { CMySystem::CharToWCHAR(wTemp,temp[i].FileName); HTREEITEM hChild = m_FileTree.InsertItem ((LPCTSTR) wTemp,//&fd.cFileName, ILI_CLSDFLD , ILI_OPENFLD , hItem , TVI_SORT); CString strNewPathName = strPathName; if (strNewPathName.Right (1) != "\\") {strNewPathName += "\\";} CMySystem::CharToWCHAR(wBuf,temp[i].FileName); strNewPathName += wBuf;//fd.cFileName; SetButtonState (hChild, strNewPathName); } } } else { DIRECTORY_INFO_b.DirectoryInfo=temp[i]; DIRECTORY_INFO_b.path=str1; AddToListView(&DIRECTORY_INFO_b); } } delete temp; return num;}
    3.6.2 文件删除文件删除分为普通删除与驱动删除。

    普通删除:调用Win32 API DeleteFile()删除文件。对付普通病毒这种方式有效。但某些病毒会采用文件占用式保护本体不被删除,甚至采用驱动形式保护,此时普通删除无效
    驱动删除:传递控制字IOCTL_MT_KILLFILE至驱动Explorer.sys,驱动删除病毒,此方式有一定危险性,但对病毒有奇效。极少数病毒能逃离此法删除

    3.7 磁盘编辑器3.7.1 十六进制编辑器界面处理我们的磁盘编辑器界面采用了重载后的CEdit类。新类CHexEdit响应下列消息:
    WM_CHARWM_KILLFOCUSWM_PAINTWM_SETFOCUSWM_SIZEWM_VSCROLLWM_HSCROLLWM_GETDLGCODEWM_ERASEBKGNDWM_LBUTTONDOWNWM_LBUTTONDBLCLKWM_MOUSEMOVEWM_LBUTTONUPWM_KEYDOWN
    并在WM_CHAR的响应函数CHexEdit::OnPaint()中绘制了三大部分:地址栏、十六进制栏、字符显示栏。其中的细节比较多,这里就不全部描述了。
    3.7.2 硬盘编辑功能硬盘编辑有两种选择, 一种是普通的通过应用层API CreateFile()打开物理对象\\.\PhysicalDrive0,实现函数为CMySystem::ReadSector(__int64Sect, BYTE *OutBuf).另一种在核心态自己构造IRP发送至磁盘驱动ATAPI.SYS(这是Windows处理磁盘请求的最后一站,再往下就是硬盘IO指令了),直接绕过文件系统FSD.在我们的测试中,意外的发现这种极为底层的技术甚至连知名的影子系统, RVS,冰点……等还原软件被穿透。
    3.8 网络防火墙网络防火墙有两种用途,一种是建立规则,阻止或通过指定网络包。防火墙的驱动实现将在后面讲解。
    3.8.1 建立防火墙规则通过CPage8:: OnButtonAdd()打开规则对话框,对规则进行相关配置后,调用CPage8::OnButtoninStall再调用CPage8::AddFilterToFw发送规则至驱动。
    3.8.2 监视网络数据包在驱动中我们自己实现了一个类似DbgPrint的函数。应用层中申请了一个定时器,反复读取内核传来的网络数据包。并分析数据包中的源IP、目标IP和数据包协议类型。相关函数为CPage8::OnBnClickedMonitor()
    3.9 PE文件分析文件分析部分没有什么内核技术,纯粹是个辅助性功能。相应部分看函数便可知。
    CPage9::LoadFile(); CPage9::IsPEFile() CPage9::PrintFileHeader(); CPage9::PrintOptionAlHeader(); CPage9::PrintSectionInfo(); CPage9::printET(); CPage9::printIAT(); CPage9::UnLoadFile()
    至此,应用层分析完毕。
    4 内核层各个功能实现原理下面结合代码详细介绍内核层各个功能及其实现。
    4.1 进程管理见应用层分析部分
    4.2 自动防御见应用层分析部分
    4.3 驱动模块列举见应用层分析部分
    4.4 列举LSP见应用层分析部分
    4.5 SSDT见应用层分析部分
    4.6 文件管理文件管理器通过自己构造IRP数据包下发至卷驱动。相应的函数位于Explorer.sys中的GetDirectory(char *lpDirName, PULONG dwRetSize)里。函数调用ZwOpenFile打开设备链接\\DosDevices\\C:\\卷驱动,调用IoAllocateIrp分配一个IRP,然后填充IRP:
    KeInitializeEvent(&event,SynchronizationEvent,FALSE); lpInformation = ExAllocatePool(PagedPool,655350); lpSystemBuffer = ExAllocatePool(PagedPool,655350); RtlZeroMemory(lpSystemBuffer,655350); RtlZeroMemory(lpInformation,655350); lpirp->UserEvent = &event; lpirp->UserBuffer = lpInformation; lpirp->AssociatedIrp.SystemBuffer = lpInformation; lpirp->MdlAddress = NULL; lpirp->Flags = 0; lpirp->UserIosb = &ios; lpirp->Tail.Overlay.OriginalFileObject = lpFileObject; lpirp->Tail.Overlay.Thread = PsGetCurrentThread(); lpirp->RequestorMode = KernelMode; lpsp = IoGetNextIrpStackLocation(lpirp); lpsp->MajorFunction = IRP_MJ_DIRECTORY_CONTROL; lpsp->MinorFunction = IRP_MN_QUERY_DIRECTORY; lpsp->FileObject = lpFileObject; lpsp->DeviceObject = lpDeviceObject; lpsp->Flags = SL_RESTART_SCAN; lpsp->Control = 0; lpsp->Parameters.QueryDirectory.FileIndex = 0; lpsp->Parameters.QueryDirectory.FileInformationClass = FileDirectoryInformation; lpsp->Parameters.QueryDirectory.FileName = NULL; lpsp->Parameters.QueryDirectory.Length = 655350;
    填这样当IRP返回时便携带了我们所需要的文件信息。
    强制删除文件同样采用构造IRP下发方式。不同的是下发的IRP参数不同:
    FileInformation.DeleteFile = TRUE; Irp->AssociatedIrp.SystemBuffer = &FileInformation; Irp->UserEvent = &event; Irp->UserIosb = &ioStatus; Irp->Tail.Overlay.OriginalFileObject = fileObject; Irp->Tail.Overlay.Thread = (PETHREAD)KeGetCurrentThread(); Irp->RequestorMode = KernelMode; irpSp = IoGetNextIrpStackLocation(Irp); irpSp->MajorFunction = IRP_MJ_SET_INFORMATION; irpSp->DeviceObject = DeviceObject; irpSp->FileObject = fileObject; irpSp->Parameters.SetFile.Length = sizeof(FILE_DISPOSITION_INFORMATION); irpSp->Parameters.SetFile.FileInformationClass = FileDispositionInformation; irpSp->Parameters.SetFile.FileObject = fileObject;
    4.7 硬盘编辑硬盘编辑功能的实现位于驱动SIoctl.sys中。为了实现底层的硬盘编辑,我们选择PhysicalDrive0设备对象。根据《深入解析windows》(第5版),这个对象其实是硬盘驱动atapi.sys的一个驱动。在这层驱动中,文件系统所需要的文件路径等已经不存在了,我们面对是直接是硬盘。下面是填充IRP和下发过程:
    irpSp = IoGetNextIrpStackLocation(irp); irp->UserEvent = &event; irp->IoStatus.Status = 0; irp->IoStatus.Information = 0; irp->UserBuffer = NULL; irp->Flags = (irp->Type << 16) | 5; irp->Tail.Overlay.Thread = PsGetCurrentThread(); irp->Cancel = FALSE; IoSetCancelRoutine(irp,NULL); irp->RequestorMode =KernelMode; irp->AssociatedIrp.SystemBuffer = NULL; irpSp->DeviceObject = DeviceObject; irpSp->MajorFunction = (UCHAR)ReadOrWrite; irpSp->Parameters.DeviceIoControl.InputBufferLength = 0;
    4.8 网络防火墙在WINDOWS 2000 DDK中,微软包含了称为Filter-HookDriver的新型网络驱动。可以使用它来过滤所有进出接口的数据。实际上,Filter-Hook Driver并不是网络驱动,它是一种内核模式驱动(Kernel Mode Driver)。大致上是这样的:在Filter-Hook Driver中我们提供回调函数(callback),然后使用IP Filter Driver注册回调函数。这样当数据包发送和接收时,IP Filter Driver会调用回调函数。那么我们到底该如何实现这些步骤呢?总结如下:

    建立Filter-HookDriver.我们必须建立内核模式驱动,你可以选择名称,DOS名称和其它驱动特性,这些不是必须的,但我建议使用描述名称
    如果我们要安装过滤函数,首先我们必须得到指向IP Filter Driver的指针,这是第二步
    我们已经取得了指针,现在我们可以通过发送特殊的IRP来安装过滤函数,该”消息”传递的数据包含了过滤函数的指针
    过滤数据包
    当我们想结束过滤,我们必须撤销过滤函数。这通过传递NULL指针作为过滤函数指针来实现

    下面是我们的防火墙构架:

    一个创建设备的驱动程序入口,为通讯创建符号连接和处理IRPs(分派,加载,卸载,创建…)的标准例程
    在标准例程里管理IRPs。在我们的代码中,我们实现了四个IOCTL代码:START_IP_HOOK(注册过滤函数),STOP_IP_HOOK(注销过滤函数),ADD_FILTER(安装新的过滤规则),CLEAR_FILTER(清除所有规则)
    对于我们的驱动,我们实现多个用于过滤的函数

    我们在IP Filter Driver中执行一个函数来注册过滤函数,步骤如下:

    首先,得到IP Filter Driver的指针,这要求驱动已经安装并执行。为了保证IP Filter Driver已经安装并执行,在前述用户程序中,在加载本驱动前加载并启动IP Filter Driver
    第二步,建立用IOCTL_PF_SET_EXTENSION_POINTER作为控制代码的IRP。传递PF_SET_EXTENSION_HOOK_INFO 参数,该参数结构中包含了指向过滤函数的指针。如果要卸载该函数,必须在同样的步骤里传递NULL作为过滤函数指针
    向设备驱动发送:创建IRP, 这里有一个大的问题,只有一个过滤函数可以安装,因此如果另外的应用程序已经安装了一个过滤函数,你就不能再安装了

    设置过滤函数的代码如下:
    NTSTATUS SetFilterFunction(PacketFilterExtensionPtr filterFunction){ NTSTATUS status = STATUS_SUCCESS, waitStatus=STATUS_SUCCESS; UNICODE_STRING filterName; PDEVICE_OBJECT ipDeviceObject=NULL; PFILE_OBJECT ipFileObject=NULL; PF_SET_EXTENSION_HOOK_INFO filterData; KEVENT event; IO_STATUS_BLOCK ioStatus; PIRP irp; dprintf("Getting pointer to IpFilterDriver\n"); RtlInitUnicodeString(&filterName, DD_IPFLTRDRVR_DEVICE_NAME); status = IoGetDeviceObjectPointer(&filterName,STANDARD_RIGHTS_ALL, &ipFileObject, &ipDeviceObject); dprintf("OK:%x",status); if(NT_SUCCESS(status)) { filterData.ExtensionPointer = filterFunction; KeInitializeEvent(&event, NotificationEvent, FALSE); irp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER, ipDeviceObject, (PVOID) &filterData, sizeof(PF_SET_EXTENSION_HOOK_INFO), NULL, 0, FALSE, &event, &ioStatus); if(irp != NULL) { status = IoCallDriver(ipDeviceObject, irp); if (status == STATUS_PENDING) { waitStatus = KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL); if (waitStatus != STATUS_SUCCESS ) dprintf("Error waiting for IpFilterDriver response."); } status = ioStatus.Status; if(!NT_SUCCESS(status)) dprintf("Error, IO error with ipFilterDriver\n"); } else { status = STATUS_INSUFFICIENT_RESOURCES; dprintf("Error building IpFilterDriver IRP\n"); } if(ipFileObject != NULL) { dprintf("ObDereferenceObject"); ObDereferenceObject(ipFileObject); } ipFileObject = NULL; ipDeviceObject = NULL; } else dprintf("Error while getting the pointer\n"); return status;}
    这时已经完成了建立过滤函数的工作,当取得设备驱动的指针后必须释放文件对象。我们使用事件来通知IpFilterDriver 已经完成了IRP处理。
    下面是过滤函数代码:
    PF_FORWARD_ACTION cbFilterFunction(IN unsigned char *PacketHeader, IN unsigned char *Packet, IN unsigned int PacketLength, IN unsigned int RecvInterfaceIndex, IN unsigned int SendInterfaceIndex, IN unsigned long RecvLinkNextHop, IN unsigned long SendLinkNextHop){ IPPacket *ipp; TCPHeader *tcph; UDPHeader *udph; int countRule = 0; struct filterList *aux = first; WCHAR wcMessage[MAXSTR]; ipp = (IPPacket*)PacketHeader; MyPrint(SEPARATOR); dprintf("PacketInfo %x, %d\r\n", PacketLength, RecvInterfaceIndex); dprintf("Source: %x Destination: %x Protocol: %d\r\n", ipp->ipSource, ipp ->ipDestination, ipp->ipProtocol); swprintf(wcMessage, L "PacketLength: %x, RecvInterfaceIndex:%d\r\n", PacketLength, RecvInterfaceIndex); MyPrint(wcMessage); swprintf(wcMessage, L "NetInfomation:@%x@@%x@@@%d\r\n", ipp->ipSource, ipp ->ipDestination, ipp->ipProtocol); MyPrint(wcMessage); if (ipp->ipProtocol == 6) { tcph = (TCPHeader*)Packet; dprintf("FLAGS: %x\r\n", tcph->flags); swprintf(wcMessage, L "FLAGS: %x\r\n", tcph->flags); if (!(tcph->flags &0x02)) return PF_FORWARD; } while (aux != NULL) { dprintf("Comparing with Rule %d", countRule); if (aux->ipf.protocol == 0 || ipp->ipProtocol == aux->ipf.protocol) { if (aux->ipf.sourceIp != 0 && (ipp->ipSource &aux->ipf.sourceMask) != aux->ipf.sourceIp) { aux = aux->next; countRule++; continue; }if (aux->ipf.destinationIp != 0 && (ipp->ipDestination &aux->ipf.destinationMask) != aux->ipf.destinationIp) { aux = aux->next; countRule++; continue; } //tcp, protocol = 6 if (ipp->ipProtocol == 6) { if (aux->ipf.sourcePort == 0 || tcph->sourcePort == aux->ipf.sourcePort) { if (aux->ipf.destinationPort == 0 || tcph->destinationPort == aux->ipf.destinationPort) //puerto tcp destino { if (aux->ipf.drop) return PF_DROP; else return PF_FORWARD; } } } //udp, protocol = 17 else if (ipp->ipProtocol == 17) { udph = (UDPHeader*)Packet; if (aux->ipf.sourcePort == 0 || udph->sourcePort == aux->ipf.sourcePort) { if (aux->ipf.destinationPort == 0 || udph->destinationPort == aux->ipf.destinationPort) { if (aux->ipf.drop) return PF_DROP; else return PF_FORWARD; } } } else { if (aux->ipf.drop) return PF_DROP; else return PF_FORWARD; } } countRule++; aux = aux->next; } return PF_FORWARD;}
    4.9 PE文件分析见应用层分析部分
    至此,整个KsBinSword分析完毕。
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++资源转载自:https://download.csdn.net/download/anzyky/3329962+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    1 评论 6 下载 2018-10-05 22:02:18 下载需要13点积分
  • 基于WIN32 API实现的超级玛丽游戏

    游戏截图



    游戏中用到的类结构介绍图像层
    图像基类MYBITMAP
    游戏背景MYBKSKY—>MYBITMAP
    游戏图片MYANIOBJ—>MYBITMAP
    魔法攻击MYANIMAGIC—>MYBITMAP

    逻辑层
    游戏逻辑GAMEMAP
    时钟处理MYCLOCK
    字体处理MYFONT
    跟踪打印FILEREPORT
    玩家控制MYROLE—>MYBITMAP

    结构和表
    精灵结构ROLE
    物品结构MapObject
    地图信息表MAPINFO

    一、工程开始介绍下准备工作,也就是所需要的开发工具。代码编写调试:VC 6.0,美术工具:Windows自带的画图(开始-程序-附件-画图)。这是最简陋的开发工具,但已足够。最好再有Photoshop,记事本或UltraEdit等等你喜欢的文本编辑工具。
    游戏代码分两部分,图像部分和逻辑部分。
    先说图像部分:图像分两种,矩形图片和不规则图片。工程中的PIC文件夹下,可以看到所有图像资源。
    矩形图片有:地面、砖块、水管、血条、血条背景。
    不规则图片有:蘑菇(玩家,敌人1,敌人2),子弹、旋风、爆炸效果、金币、撞击金币后的得分、攻击武器(那个从魂斗罗里抠来的东东)、火圈1、火圈2、箭头(用于开始菜单选择)、树木、河流、WIN标志、背景图片(游戏背景和菜单背景)。
    所有图片都分成几个位图BMP文件存储。一个文件中,每种图片,都纵向排列。每种图片可能有多帧。比如,金币需要4帧图像,才能构成一个旋转的动画效果,那么,各帧图像横向排列。
    图像层的结构就这样简单,逻辑层只需要确定“哪个图像,哪一帧”这两个参数,就能在屏幕上绘制出所有图片。
    图像层的基类是:
    class MYBITMAP{public: MYBITMAP(); ~MYBITMAP(); // 初始化 void Init(HINSTANCE hInstance,int iResource,int row,int col); void SetDevice(HDC hdest,HDC hsrc,int wwin,int hwin); void SetPos(int istyle,int x,int y); // 显示 void Draw(DWORD dwRop); void Stretch(int x,int y); void Stretch(int x,int y,int id); void Show(int x,int y); void ShowCenter(int y); void ShowLoop(int left,int top,int right,int bottom,int iframe); void ShowNoBack(int x,int y,int iFrame); void ShowNoBackLoop(int x,int y,int iFrame,int iNum); // 动画播放 void ShowAni(); void SetAni(int x,int y); HBITMAP hBm;public: // 按照行列平均分成几个 int inum; int jnum; int width; int height; int screenwidth; int screenheight; HDC hdcdest; HDC hdcsrc; // 当前位置 int xpos; int ypos; int iStartAni;};
    这只是一个基类,上面是几个重要的数据成员和函数。它所描述的图片,是一个m行n列构成的m*n个图片,每个图片大小一致,都是矩形。显然,这并不能满足上面的设计要求,怎么解决呢?派生,提供更多的功能。但是,这个基类封装了足够的物理层信息:设备上下文HDC,和位图句柄HBITMAP。矩形图片的显示、不规则图片的显示、图片组织排列信息,这些功能交给它的派生类MYANIOBJ。
    还有,我们最关心的问题是图片坐标,比如,不同位置的砖块、精灵、金币,这些由逻辑层处理。
    二、图片基类MYBITMAP先说一下代码风格,大家都说看不懂,这就对了。整套代码约有3000行,并不都是针对这个游戏写的。我想把代码写成一个容易扩展、容易维护、功能全面的“框架”,需要什么功能,就从这个框架中取出相应功能,如果是一个新的功能,比如新的图像显示、新的运动控制,我也能方便地实现。所以,这个游戏的代码,是在前几个游戏的基础上扩充起来的。部分函数,部分变量在这款游戏中,根本不用,但要保留,要为下一款游戏作准备。只要理解了各个类,就理解了整个框架。
    今天先讲最基础的图像类MYBITMAP,先说一下代码风格,大家都说看不懂,这就对了。整套代码约有3000行,并不都是针对这个游戏写的。我想把代码写成一个容易扩展、容易维护、功能全面的“框架”,需要什么功能,就从这个框架中取出相应功能,如果是一个新的功能,比如新的图像显示、新的运动控制,我也能方便地实现。所以,这个游戏的代码,是在前几个游戏的基础上扩充起来的。部分函数,部分变量在这款游戏中,根本不用,但要保留,要为下一款游戏作准备。只要理解了各个类,就理解了整个框架。
    今天先讲最基础的图像类MYBITMAP,成员函数功能列表:
    // 功能 根据一个位图文件,初始化图像// 入参 应用程序实例句柄 资源ID 横向位图个数 纵向位图个数void Init(HINSTANCE hInstance,int iResource,int row,int col);// 功能 设置环境信息// 入参 目的DC(要绘制图像的DC),临时DC,要绘制区域的宽 高void SetDevice(HDC hdest,HDC hsrc,int wwin,int hwin);// 功能 设置图片位置// 入参 设置方法 横纵坐标void SetPos(int istyle,int x,int y);// 功能 图片显示// 入参 图片显示方式void Draw(DWORD dwRop);// 功能 图片缩放显示// 入参 横纵方向缩放比例void Stretch(int x,int y);// 功能 图片缩放显示// 入参 横纵方向缩放比例 缩放图像ID(纵向第几个)void Stretch(int x,int y,int id);// 功能 在指定位置显示图片// 入参 横纵坐标void Show(int x,int y);// 功能 横向居中显示图片// 入参 纵坐标void ShowCenter(int y);// 功能 将某个图片平铺在一个区域内// 入参 左上右下边界的坐标 图片ID(横向第几个)void ShowLoop(int left,int top,int right,int bottom,int iframe);// 功能 不规则图片显示// 入参 横纵坐标 图片ID(横向第几个)void ShowNoBack(int x,int y,int iFrame);// 功能 不规则图片横向平铺// 入参 横纵坐标 图片ID(横向第几个) 平铺个数void ShowNoBackLoop(int x,int y,int iFrame,int iNum);// 动画播放// 功能 自动播放该图片的所有帧,函数没有实现,但以后肯定要用:)// 入参 无void ShowAni();// 功能 设置动画坐标// 入参 横纵坐标void SetAni(int x,int y);
    成员数据:
    // 图像句柄HBITMAP hBm;// 按照行列平均分成几个int inum;int jnum;// 按行列分割后,每个图片的宽高(显然各个图片大小一致,派生后,这里的宽高已没有使用意义)int width;int height;// 屏幕宽高int screenwidth;int screenheight;// 要绘制图片的dcHDC hdcdest;// 用来选择图片的临时dcHDC hdcsrc; // 当前位置int xpos;int ypos;// 是否处于动画播放中(功能没有实现)int iStartAni;
    这个基类的部分函数和变量,在这个游戏中没有使用,是从前几个游戏中保留下来的,所以看起来有些零乱。这个游戏的主要图像功能,由它的派生类完成。由于基类封装了物理层信息(dc和句柄),派生类的编写就容易一些,可以让我专注于逻辑含义。
    基类的函数实现上,很简单,主要是以下几点:
    1.图片初始化
    // 根据程序实例句柄,位图文件的资源ID,导入该位图,得到位图句柄 hBm=LoadBitmap(hInstance,MAKEINTRESOURCE(iResource)); // 获取该位图文件的相关信息 GetObject(hBm,sizeof(BITMAP),&bm); // 根据横纵方向的图片个数,计算出每个图片的宽高(对于超级玛丽,宽高信息由派生类处理) width=bm.bmWidth/inum; height=bm.bmHeight/jnum;
    2.图片显示
    各个图片的显示函数,大同小异,都要先选入一个临时DC,再bitblt到要绘制的dc上。矩形图片,可以直接用SRCCOPY的方式绘制;不规则图片,需要先用黑白图与目的区域相”与”(SRCAND),再用”或”的方法显示图像(SRCPAINT),这是一种简单的”绘制透明位图”的方法。
    void MYBITMAP::ShowNoBack(int x,int y,int iFrame){ xpos=x; ypos=y; SelectObject(hdcsrc,hBm); BitBlt(hdcdest,xpos,ypos,width,height/2,hdcsrc,iFrame*width,height/2,SRCAND); BitBlt(hdcdest,xpos,ypos,width,height/2,hdcsrc,iFrame*width,0,SRCPAINT); }
    3.图片缩放
    用StretchBlt的方法实现。
    void MYBITMAP::Stretch(int x,int y,int id){ SelectObject(hdcsrc,hBm); StretchBlt(hdcdest,xpos,ypos,width*x,height*y, hdcsrc,0,id*height, width,height, SRCCOPY); }
    在超级玛丽这个游戏中,哪些图像的处理是通关这个基类呢?只有一个:MYBITMAP bmPre;由于这个基类只能处理几个大小均等的图片,只有这些图片大小一致,且都是矩形:游戏开始前的菜单背景,操作信息的背景,每一关开始前的背景(此时显示LIFE x WORLD x),通关或游戏结束时显示的图片,共5个,将这5个图片,放在一个位图文件中,于是,这些图片的操作就做完了,代码如下:
    // 初始设置,在InitInstance函数中bmPre.Init(hInstance,IDB_BITMAP_PRE1,1,5);bmPre.SetDevice(hscreen,hmem,GAMEW*32,GAMEH*32);bmPre.SetPos(BM_USER,0,0);// 图片绘制,在WndProc中,前两个参数指横纵方向扩大2倍显示.bmPre.Stretch(2,2,0);bmPre.Stretch(2,2,4);bmPre.Stretch(2,2,2); bmPre.Stretch(2,2,1); bmPre.Stretch(2,2,3);
    三、游戏背景类MYBKSKY类说明
    这是一个专门处理游戏背景的类。在横版游戏或射击游戏中,都有一个背景画面,如山、天空、云、星空等等。这些图片一般只有1到2倍屏幕宽度,然后像一个卷轴一样循环移动,连成一片,感觉上像一张很长的图片。这个类就是专门处理这个背景的。在超级玛丽增强版中,主要关卡是3关,各有一张背景图片;从水管进去,有两关,都用一张全黑图片。共四张图。这四张图大小一致,纵向排列在一个位图文件中。MYBKSKY这个类,派生于MYBITMAP。由于背景图片只需要完成循环移动的效果,只需要实现一个功能,而无需关心其他任何问题(例如句柄、dc)。编码起来很简单,再次反映出面向对象的好处。
    技术原理
    怎样让一张图片像卷轴一样不停移动呢?很简单,假设有一条垂直分割线,把图片分成左右两部分。先显示右边部分,再把左边部分接到图片末尾。不停移动向右移动分割线,图片就会循环地显示。
    MYBKSKY类定义如下所示:
    class MYBKSKY:public MYBITMAP{public: MYBKSKY(); ~MYBKSKY(); // show // 功能 显示一个背景. // 入参 无 void DrawRoll(); // 循环补空 // 功能 显示一个背景,并缩放图片 // 入参 横纵方向缩放比例 void DrawRollStretch(int x,int y); // 功能 指定显示某一个背景,并缩放图片,游戏中用的就是这个函数 // 入参 横纵方向缩放比例 背景图片ID(纵向第几个) void DrawRollStretch(int x,int y,int id); // 功能 设置图片位置 // 入参 新的横纵坐标 void MoveTo(int x,int y); // 功能 循环移动分割线 // 入参 分割线移动的距离 void MoveRoll(int x); // data // 分割线横坐标 int xseparate;};
    函数具体实现都很简单,例如:
    void MYBKSKY::DrawRollStretch(int x,int y, int id){ // 选入句柄 SelectObject(hdcsrc,hBm); // 将分割线右边部分显示在当前位置 StretchBlt(hdcdest, xpos,ypos, // 当前位置 (width-xseparate)*x,height*y, // 缩放比例 hdcsrc, xseparate,id*height, // 右边部分的坐标 width-xseparate,height, // 右边部分的宽高 SRCCOPY); // 将分割线左边部分接在图片末尾 StretchBlt(hdcdest,xpos+(width-xseparate)*x,ypos, xseparate*x,height*y, hdcsrc,0,id*height, xseparate,height, SRCCOPY); }
    使用举例:
    // 定义 MYBKSKY bmSky;// 初始化bmSky.Init(hInstance,IDB_BITMAP_MAP_SKY,1,4);bmSky.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);bmSky.SetPos(BM_USER,0,0);// 游戏过程中显示bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);// 每隔一定时间,移动分割线bmSky.MoveRoll(SKY_SPEED);//云彩移动// 以下两处与玩家角色有关:// 当玩家切换到一张新地图时,刷新背景图片的坐标bmSky.SetPos(BM_USER,viewx,0);// 当玩家向右移动时,刷新背景图片的坐标bmSky.SetPos(BM_USER,viewx,0);
    至此,游戏背景图片的功能就做完了。
    四、图片显示类MYANIOBJ类说明
    这个类负责游戏中的图片显示。菜单背景、通关和游戏结束的提示图片,由MYBITMAP处理(大小一致的静态图片)。游戏背景由MYBKSKY处理。其余图片,也就是游戏过程中的所有图片,都是MYANIOBJ处理。
    技术原理
    游戏中的图片大小不一致,具体在超级玛丽中,可以分成两类:矩形图片和不规则图片。在位图文件中,都是纵向排列各个图片,横向排列各帧。用两个数组存储各个图片的宽和高。为了方便显示某一个图片,用一个数组存储各个图片的纵坐标(即位图文件中左上角的位置)。使用时,由逻辑层指定“哪个图片”的“哪一帧”,显示在“什么位置”。这样图片的显示功能就实现了。
    MYANIOBJ类定义如下所示:
    class MYANIOBJ:public MYBITMAP{public: MYANIOBJ(); ~MYANIOBJ(); // init list // 功能 初始化宽度数组 高度数组 纵坐标数组 是否有黑白图 // 入参 宽度数组地址 高度数组地址 图片数量 是否有黑白图(0 没有, 1 有) // (图片纵坐标信息由函数计算得出) void InitAniList(int *pw,int *ph,int inum,int ismask); // 功能 初始化一些特殊的位图,例如各图片大小一致,或者有其他规律 // 入参 初始化方式 参数1 参数2 // (留作以后扩展, 目的是为了省去宽高数组的麻烦) void InitAniList(int style,int a,int b); // show // 功能 显示图片(不规则图片) // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 图片帧(横向第几个) void DrawItem(int x,int y,int id,int iframe); // 功能 显示图片(矩形图片) // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 图片帧(横向第几个) void DrawItemNoMask(int x,int y,int id,int iframe); // 功能 指定宽度, 显示图片的一部分(矩形图片) // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个), 显示宽度 图片帧(横向第几个) void DrawItemNoMaskWidth(int x,int y,int id,int w,int iframe); // 功能 播放一个动画 即循环显示各帧 // 入参 横纵坐标(要显示的位置) 图片id(纵向第几个) void PlayItem(int x,int y,int id); // 宽度数组 最多支持20个图片 int wlist[20]; // 高度数组 最多支持20个图片 int hlist[20]; // 纵坐标数组 最多支持20个图片 int ylist[20]; // 动画播放时的当前帧 int iframeplay;};
    函数实现上也很简单。构造函数中,所有成员数据清零;初始化时,将各图片的高度累加,即得到各图片的纵坐标。显示图片的方法如前所述。
    使用举例:
    游戏图片分成三类:地图物品、地图背景物体、精灵(即所有不规则图片)。
    MYANIOBJ bmMap;MYANIOBJ bmMapBkObj;MYANIOBJ bmAniObj;
    初始化宽高信息,程序中定义一个二维数组,例如:
    int mapani[2][10]={ {32,32,64,32,32,52,64,32,64,32}, {32,32,64,32,32,25,64,32,64,32},};
    第一维mapani[0]存储10个图片的宽度,第二维mapani[1]存储10个图片的高度,初始化时,将mapani[0],mapani[1]传给初始化函数即可。
    1.地图物品的显示
    // 定义MYANIOBJ bmMap;// 初始化// 这一步加载位图bmMap.Init(hInstance,IDB_BITMAP_MAP,1,1);// 这一步初始化DCbmMap.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);// 这一步设置宽高信息, 图片为矩形bmMap.InitAniList(mapsolid[0],mapsolid[1], sizeof(mapsolid[0])/sizeof(int),0);// 对象作为参数传给逻辑层, 显示地图物品gamemap.Show(bmMap);
    2.血条的显示
    打怪时,屏幕上方要显示血条。由于同样是矩形图片,也一并放在了地图物品的位图中。
    // 变量声明extern MYANIOBJ bmMap;// 显示血条背景,指定图片宽度:最大生命值*单位生命值对应血条宽度bmMap.DrawItemNoMaskWidth(xstart-1, ATTACK_TO_Y-1, ID_MAP_HEALTH_BK, iAttackMaxLife*BMP_WIDTH_HEALTH, 0);// 显示怪物血条,指定图片宽度:当前生命值*单位生命值对应血条宽度 bmMap.DrawItemNoMaskWidth(xstart, ATTACK_TO_Y, ID_MAP_HEALTH, iAttackLife*BMP_WIDTH_HEALTH, 0);
    3.地图背景物体的显示
    背景物体包括草、河流、树木、目的地标志。这些物体都不参与任何逻辑处理,只需要显示到屏幕上。图片放在一个位图文件中,都是不规则形状。
    // 定义MYANIOBJ bmMapBkObj;// 初始化并加载位图bmMapBkObj.Init(hInstance,IDB_BITMAP_MAP_BK,1,1);// 设置dcbmMapBkObj.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);// 设置各图片宽高信息bmMapBkObj.InitAniList(mapanibk[0],mapanibk[1],sizeof(mapanibk[0])/sizeof(int),1);// 对象作为参数传给逻辑层, 显示地图背景物体gamemap.ShowBkObj(bmMapBkObj);
    4.精灵的显示
    精灵包括:蘑菇(玩家,敌人1,敌人2),子弹、旋风、爆炸效果、金币、撞击金币后的得分、攻击武器(那个从魂斗罗里抠来的东东)、火圈1、火圈2、箭头(用于开始菜单选择)。
    // 定义MYANIOBJ bmAniObj;// 初始化加载位图bmAniObj.Init(hInstance,IDB_BITMAP_ANI,1,1);// 设置dcbmAniObj.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32);// 设置宽高信息bmAniObj.InitAniList(mapani[0],mapani[1],sizeof(mapani[0])/sizeof(int),1);// 菜单显示(即菜单文字左边的箭头)gamemap.ShowMenu(bmAniObj);// 对象作为参数传给逻辑层, 显示各个精灵gamemap.ShowAniObj(bmAniObj);
    五、魔法攻击类MYANIMAGIC类说明:玩家有两种攻击方式:普通攻击(子弹),魔法攻击(旋风)。这个类是专门处理旋风的。我最初的想法是用一些特殊的bitblt方法制造特效,例如或、与、异或。试了几次,都失败了。最后只能用“先与后或”的老方法。这个类可看成MYANIOBJ的一个简化版,只支持不规则图片的显示。
    MYANIMAGIC类定义如下所示:
    class MYANIMAGIC:public MYBITMAP{public: MYANIMAGIC(); ~MYANIMAGIC(); // init list // 功能 初始化宽度数组 高度数组 纵坐标数组(必须有黑白图) // 入参 宽度数组地址 高度数组地址 图片数量 // (图片纵坐标信息由函数计算得出) void InitAniList(int *pw,int *ph,int inum); // 功能 设置dc // 入参 显示dc 临时dc(用于图片句柄选择) 临时dc(用于特效实现) void SetDevice(HDC hdest,HDC hsrc,HDC htemp); // show // 功能 显示某个图片的某帧 // 入参 横纵坐标(显示位置) 图片id(纵向第几个) 帧(横向第几个) void DrawItem(int x,int y,int id,int iframe); // 宽度数组 int wlist[20]; // 高度数组 int hlist[20]; // 纵坐标数组 int ylist[20]; // 用于特效的临时dc, 功能没有实现 HDC hdctemp;};
    函数具体实现很简单,可参照MYANIOBJ类。
    使用举例
    // 定义MYANIMAGIC bmMagic;// 初始化加载位图bmMagic.Init(hInstance,IDB_BITMAP_MAGIC,1,1);// 设置dcbmMagic.SetDevice(hscreen,hmem, hmem2);// 初始化宽高信息bmMagic.InitAniList(mapanimagic[0],mapanimagic[1],sizeof(mapanimagic[0])/sizeof(int));// 变量声明extern MYANIMAGIC bmMagic;// 在逻辑层中, 显示旋风图片bmMagic.DrawItem(xstart,ystart, 0, FireArray[i].iframe);
    六、时钟控制类MYCLOCK类说明
    时间就是生命。这对于游戏来说,最为准确。游戏程序只做两件事:显示图片、处理逻辑。更准确的说法是:每隔一段时间显示图片并处理逻辑。程序中,要设置一个定时器。这个定时器会每隔一段时间发出一个WM_TIMER消息。在该消息的处理中,先逻辑处理。逻辑处理完毕,通过InvalidateRect函数发出WM_PAINT消息,显示各种图片。游戏就不停地运行下去,直至程序结束。
    时间表示
    用一个整数iNum表示当前时间,游戏中的时间是1,2,3, … , n, 1,2,3, …,n 不停循环.假设1秒内需要25个WM_TIMER消息(每40毫秒1次),则n=25。也可以用一个变量,统计过了几秒。
    控制事件频率的方法

    一秒内发生多次
    以游戏背景图片为例, 每秒移动5下, 可以在iNum为5,10,15,20,25这5个时间点上移动.即iNum可以被5整除时,修改背景图片的坐标.
    一秒内发生一次
    例如火圈, 每秒产生一个新的蘑菇兵. 可以随便指定一个时间点,如20. 当iNum等于20时,生成一个蘑菇兵。
    多秒内发生一次
    需要一个辅助变量iNumShow,统计时间过了几秒。每隔一秒iNumShow减1,当iNumShow等于0时处理逻辑。

    MYCLOCK类定义如下所示:(所有函数都是内联函数)
    class MYCLOCK{public: // 构造函数 初始化所有变量 MYCLOCK() { iNum=0; // 时间点 iIsActive=0; // 是否已经开始计时 iNumShow=0; // 计时秒数 iElapse=100; // 默认每100ms发一个WM_TIMER消息 ishow=0; // 是否显示时间 } // 析构函数 销毁计时器 ~MYCLOCK() { Destroy(); } // 功能 开始计时, 产生WM_TIEMR消息的时间间隔为elapse. // 设置计时秒数(timetotal). // 入参 窗口句柄 时间间隔 计时秒数 void Begin(HWND hw,int elapse,int timetotal) { if(iIsActive) return;//已经启动了,直接返回 hWnd=hw; iElapse=elapse; SetTimer(hWnd,1,iElapse,NULL); iNum=1000/iElapse;//一秒钟的时间消息数量 iNumShow=timetotal; iIsActive=1; } // 功能 销毁计时器. // 入参 无 void Destroy() { if(iIsActive) { iIsActive=0; KillTimer(hWnd,1); } } // 功能 重置计时秒数 // 入参 秒数 void ReStart(int timetotal) { iNumShow=timetotal; iNum=1000/iElapse; ishow=1; } //////////////////////////// 显示部分 // 功能 设置显示dc (在超级玛丽增强版中不显示时间) // 入参 显示dc void SetDevice(HDC h) { hDC=h; } // 功能 显示时间, TIME 秒数 // 入参 显示坐标 void Show(int x,int y) { char temp[20]={0}; if(!ishow) return; // 设置显示文本 sprintf(temp,"TIME: %d ",iNumShow); TextOut(hDC,x, y, temp,strlen(temp)); } // 功能 时间点减一 // 如果到了计时秒数, 函数返回1, 否则返回0. // 入参 无 int DecCount() { iNum--; if(iNum==0) { // 过了一秒 iNum=1000/iElapse; iNumShow--; if(iNumShow<=0) { // 不销毁计时器 return 1; } } return 0; } // 功能 时间点减一 // 如果到了计时秒数, 函数返回1并销毁计时器, 否则返回0. // 入参 无 int Dec() { iNum--; if(iNum<=0) { //过了一秒 iNum=1000/iElapse; iNumShow--; if(iNumShow<=0) { iNumShow=0; Destroy(); return 1; } } return 0; } // 功能 设置是否显示 // 入参 1,显示; 0, 不显示 void SetShow(int i) { ishow=i; }public: // 窗口句柄 HWND hWnd; // 显示dc HDC hDC; // 时间点 int iNum; // 计时秒数 int iNumShow; // 消息时间间隔 int iElapse; // 是否开始计时 int iIsActive; // 是否显示 int ishow;};
    具体函数实现很简单,如上所述。
    使用举例
    // 定义MYCLOCK c1;// 设置显示dcc1.SetDevice(hscreen);// 开始计时(计时秒数无效)c1.Begin(hWnd, GAME_TIME_CLIP ,-1);// 选择游戏菜单,每隔一定时间,重绘屏幕,实现箭头闪烁c1.DecCount();if(0 == c1.iNum%MENU_ARROW_TIME)// 屏幕提示LIFE,WORLD,如果达到计时秒数,进入游戏。if(c1.DecCount())// 进入游戏,计时300秒(无意义,在超级玛丽增强版中取消时间限制)c1.ReStart(TIME_GAME_IN); // 在游戏过程中,每隔一定时间,处理游戏逻辑c1.DecCount();if(0 == c1.iNum%SKY_TIME)gamemap.ChangeFrame(c1.iNum); // 帧控制gamemap.CheckAni(c1.iNum); // 逻辑数据检测// 玩家过关后,等待一定时间。if(c1.DecCount())// 玩家进入水管,等待一定时间。if(c1.DecCount())c1.ReStart(TIME_GAME_IN); // 玩家失败后,等待一定时间。if(c1.DecCount())// 玩家通关后,等待一定时间。if(c1.DecCount())// 玩家生命值为0,游戏结束,等待一定时间。if(c1.DecCount())// 程序结束(窗口关闭),销毁计时器c1.Destroy();// 变量声明extern MYCLOCK c1;// 游戏菜单中,选择“开始游戏”,显示LIFE,WORLD提示,计时两秒c1.ReStart(TIME_GAME_IN_PRE); // 停顿两秒// 进入水管,等待,计时两秒c1.ReStart(TIME_GAME_PUMP_WAIT);// 玩家过关,等待,计时两秒c1.ReStart(TIME_GAME_WIN_WAIT);// 生命值为0,游戏结束,等待,计时三秒c1.ReStart(TIME_GAME_END); // 玩家失败,显示LIFE,WORLD提示,计时两秒c1.ReStart(TIME_GAME_IN_PRE); // 玩家失败,等待,计时两秒c1.ReStart(TIME_GAME_FAIL_WAIT);
    至此,所有的时间消息控制、时间计时都已处理完毕。
    七、字体管理类MYFONT类说明
    游戏当然少不了文字。在超级玛丽中,文字内容是比较少的,分两类:游戏菜单中的文字,游戏过程中的文字。菜单中的文字包括:

    “操作: Z:子弹 X:跳 方向键移动 W:默认窗口大小”,“地图文件错误,请修正错误后重新启动程序。”,“(上下键选择菜单,回车键确认)”,“开始游戏”,“操作说明”,“(回车键返回主菜单)”
    这几个字符串存储在一个指针数组中(全局变量),通关数组下标使用各个字符串。
    游戏中的文字只有两个:’LIFE’,’WORLD’。
    其他的文字其实都是位图,例如“通关”、“gameover”以及碰到金币后的“+10”。这些都是位图图片,在pic文件夹里一看便知。
    技术原理
    要在屏幕上显示一个字符串,分以下几步:将字体句柄选入dc,设置文字背景色,设置文字颜色,最后用TextOut完成显示。这个类就是将整个过程封装了一下。显示dc,背景色,文字颜色,字体句柄都对应各个成员数据。函数具体实现很简单,一看便知。
    MYFONT类定义如下所示:
    class MYFONT{public: // 构造函数,初始化”字体表”,即5个字体句柄构成的数组,字体大小依次递增. MYFONT(); ~MYFONT(); // 功能 设置显示文字的dc // 入参 显示文字的dc句柄 void SetDevice(HDC h); // 功能 设置当前显示的字体 // 入参 字体表下标 void SelectFont(int i); // 功能 设置当前字体为默认字体 // 入参 无 void SelectOldFont(); // 功能 在指定坐标显示字符串 // 入参 横纵坐标 字符串指针 void ShowText(int x,int y,char *p); // 功能 设置文字背景颜色,文字颜色 // 入参 文字背景颜色 文字颜色 void SetColor(COLORREF cbk, COLORREF ctext); // 功能 设置文字背景颜色,文字颜色 // 入参 文字背景颜色 文字颜色 void SelectColor(COLORREF cbk, COLORREF ctext); // 显示文字的dc HDC hdc; // 字体表,包含5个字体句柄,字体大小依次是0,10,20,30,40 HFONT hf[5]; // 默认字体 HFONT oldhf; // color COLORREF c1; // 字体背景色 COLORREF c2; // 字体颜色};
    使用举例
    // 定义MYFONT myfont;// 初始化设置显示dcmyfont.SetDevice(hscreen);// 地图文件错误:设置颜色,设置字体,显示提示文字myfont.SelectColor(TC_WHITE,TC_BLACK);myfont.SelectFont(0);myfont.ShowText(150,290,pPreText[3]);// 游戏开始菜单:设置字体,设置颜色,显示三行菜单文字myfont.SelectFont(0);myfont.SelectColor(TC_BLACK, TC_YELLOW_0);myfont.ShowText(150,260,pPreText[4]);myfont.ShowText(150,290,pPreText[5]);myfont.ShowText(150,320,pPreText[6]);// 游戏操作说明菜单:设置字体,设置颜色,显示四行说明文字myfont.SelectFont(0);myfont.SelectColor(TC_BLACK, TC_YELLOW_0);myfont.ShowText(150,230,pPreText[8]);myfont.ShowText(50,260,pPreText[1]);myfont.ShowText(50,290,pPreText[0]);myfont.ShowText(50,320,pPreText[7]);
    这个类的使用就这些。这个类只是负责菜单文字的显示,那么,游戏中的LIFE,WORLD的提示,是在哪里完成的呢?函数如下:
    void GAMEMAP::ShowInfo(HDC h){ char temp[50]={0}; SetTextColor(h, TC_WHITE); SetBkColor(h, TC_BLACK); sprintf(temp, "LIFE : %d",iLife); TextOut(h, 220,100,temp,strlen(temp)); sprintf(temp, "WORLD : %d",iMatch+1); TextOut(h, 220,130,temp,strlen(temp));}
    这个函数很简单。要说明的是,它并没有设置字体,因为在显示菜单的时候已经设置过了。
    至此,所有文字的处理全部实现。
    八、跟踪打印类FILEREPORT前面介绍了图片显示、时钟控制、字体管理几项基本技术。这是所有游戏都通用的基本技术。剩下的问题就是游戏逻辑,例如益智类、运动类、射击类、格斗类等等。当然,不同的游戏需要针对自身做一些优化,比如益智类游戏的时钟控制、画面刷新都更简单,而格斗游戏,画面的质量要更酷、更炫。下面要介绍整个游戏的核心层:逻辑控制。地图怎样绘制的?物品的坐标怎么存储?人物怎样移动?游戏流程是什么样的?
    在介绍这些内容前,先打断一下思路,说程序是怎样写出来的,即“调试”。
    程序就是一堆代码,了无秘密。初学时,dos下一个猜数字的程序,只需要十几行。一个纸牌游戏,一千多行,而超级玛丽增强版,近三千行。怎样让这么一堆程序从无到有而且运行正确?开发不是靠设计的巧妙或者笨拙,而是靠反复调试。在三千行的代码中,增加一千行,仍然运行正确,这是编程的基本要求。这个最基本的要求,靠设计做不到,只能靠调试。正如公司里的测试部,人力规模,工作压力,丝毫不比开发部差。即使如此,还是能让一些简单bug流入最终产品。老板只能先问测试部:“这么简单的bug,怎么没测出来?”再问开发部:“这么明显的错误,你怎么写出来的?”总之,程序是调出来的。
    怎么调?vc提供了很全面的调试方法,打断点、单步跟踪、看变量。这些方法对游戏不适用。一个bug,通常发生在某种情况下,比如超级玛丽,玩家在水管上,按方向键“下”,新的地图显示不出来,屏幕上乱七八糟。请问,bug在哪里?玩家坐标出问题、按键响应出问题、地图加载出问题、图片显示出问题?打断点,无处下手。
    解决方法是:程序中,创建一个文本文件,在“可能有问题”的地方,添加代码,向这个文件写入提示信息或变量内容(称为跟踪打印)。这个文本文件,就成了代码运行的日志。看日志,就知道代码中发生了什么事情。最终,找到bug。
    FILEREPORT,就是对日志文件创建、写入等操作的封装。
    FILEREPORT类定义如下所示:
    class FILEREPORT{public: // 功能 默认构造函数,创建日志trace.txt // 入参 无 FILEREPORT(); // 功能 指定日志文件名称 // 入参 日志文件名称 FILEREPORT(char *p); // 功能 析构函数,关闭文件 // 入参 无 ~FILEREPORT(); // 功能 向日志文件写入字符串 // 入参 要写入的字符串 void put(char *p); // 功能 向日志文件写入一个字符串,两个整数 // 入参 字符串 整数a 整数b void put(char *p,int a,int b); // 功能 计数器计数, 并写入一个提示字符串 // 入参 计时器id 字符串 void putnum(int i,char *p); // 功能 判断一个dc是否为null, 如果是,写入提示信息 // 入参 dc句柄 字符串 void CheckDC(HDC h,char *p); // 功能 设置显示跟踪信息的dc和文本坐标 // 入参 显示dc 横纵坐标 void SetDevice(HDC h,int x,int y); // 功能 设置要显示的跟踪信息 // 功能 提示字符串 整数a 整数b void Output(char *p,int a,int b); // 功能 在屏幕上显示当前的跟踪信息 void Show();private: // 跟踪文件指针 FILE *fp; // 计数器组 int num[5]; // 显示dc HDC hshow; // 跟踪文本显示坐标 int xpos; int ypos; // 当前跟踪信息 char info[50];};
    函数具体实现很简单,只是简单的文件写入。要说明的是两部分,

    一:计数功能,有时要统计某个事情发生多少次,所以用一个整数数组,通过putnum让指定数字累加。二:显示功能,让跟踪信息,立刻显示在屏幕上。
    使用举例:没有使用。程序最终完成,所有的跟踪打印都已删除。
    九、精灵结构struct ROLE这个结构用来存储两种精灵:敌人(各种小怪)和子弹(攻击方式)。敌人包括两种蘑菇兵和两种火圈。子弹包括火球和旋风。游戏中,精灵的结构很简单:
    struct ROLE{ int x; // 横坐标 int y; // 纵坐标 int w; // 图片宽度 int h; // 图片高度 int id; // 精灵id int iframe; // 图片当前帧 int iframemax; // 图片最大帧数 // 移动部分 int xleft; // 水平运动的左界限 int xright; // 水平运动的右界限 int movex; // 水平运动的速度 // 人物属性 int health; // 精灵的生命值 int show; // 精灵是否显示};
    游戏中的子弹处理非常简单,包括存储、生成、销毁。子弹的存储:所有的子弹存储在一个数组中,如下:
    struct ROLE FireArray[MAX_MAP_OBJECT];
    其实,所有的动态元素都有从生成到销毁的过程。看一下子弹是怎样产生的。
    首先,玩家按下z键:发出子弹,调用函数:
    int GAMEMAP::KeyProc(int iKey) case KEY_Z: // FIRE if(iBeginFire) break; iTimeFire=0; iBeginFire=1; break;
    这段代码的意思是:如果正在发子弹,代码结束。否则,设置iBeginFire为1,表示开始发子弹。
    子弹是在哪里发出的呢?
    思路:用一个函数不停地检测iBeginFire,如果它为1,则生成一个子弹。函数如下:
    int GAMEMAP::CheckAni(int itimeclip)
    发子弹的部分:
    // 发子弹 if(iBeginFire) { // 发子弹的时间到了(连续两个子弹要间隔一定时间) if(0 == iTimeFire ) { // 设置子弹属性: 可见, 动画起始帧:第0帧 FireArray[iFireNum].show=1; FireArray[iFireNum].iframe = 0; // 子弹方向 // 如果人物朝右 if(0==rmain.idirec) { // 子弹向右 FireArray[iFireNum].movex=1; } else { // 子弹向左 FireArray[iFireNum].movex=-1; } // 区分攻击种类: 子弹,旋风 switch(iAttack) { // 普通攻击: 子弹 case ATTACK_NORMAL: // 精灵ID: 子弹 FireArray[iFireNum].id=ID_ANI_FIRE; // 设置子弹坐标 FireArray[iFireNum].x=rmain.xpos; FireArray[iFireNum].y=rmain.ypos; // 设置子弹宽高 FireArray[iFireNum].w=FIREW; FireArray[iFireNum].h=FIREH; // 设置子弹速度: 方向向量乘以移动速度 FireArray[iFireNum].movex*=FIRE_SPEED; break;
    最后,移动数组的游标iFireNum.这个名字没起好, 应该写成cursor.游标表示当前往数组中存储元素的位置。
    // 移动数组游标iFireNum=(iFireNum+1)%MAX_MAP_OBJECT;
    至此,游戏中已经生成了一个子弹。 由图像层,通过子弹的id,坐标在屏幕上绘制出来。
    子弹已经显示在屏幕上,接下来,就是让它移动、碰撞、销毁。
    十、子弹的显示和帧的刷新继续介绍子弹的显示和动画帧的刷新,这个思路,可以应用的其他精灵上。
    上次讲所有的子弹存储到一个数组里,用一个游标(数组下标)表示新生产的子弹存储的位置。设数组为a,长度为n。游戏开始,一个子弹存储在a0,然后是a1,a2,…,a(n-1)。然后游标又回到0,继续从a0位置存储。数组长度30,保存屏幕上所有的子弹足够了。
    子弹的显示功能由图像层完成,如同图像处理中讲的,显示一个子弹(所有图片都是如此),只需要子弹坐标,子弹图片id,图片帧。函数如下:
    void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj)
    代码部分:
    // 显示子弹,魔法攻击 for(i=0;i<MAX_MAP_OBJECT;i++) { if (FireArray[i].show) { ystart=FireArray[i].y; xstart=FireArray[i].x; switch(FireArray[i].id) { case ID_ANI_FIRE: bmobj.DrawItem(xstart,ystart,FireArray[i].id,FireArray[i].iframe); break;
    子弹图片显示完成。游戏中,子弹是两帧图片构成的动画。动画帧是哪里改变的呢?
    刷新帧的函数是:
    voidGAMEMAP::ChangeFrame(int itimeclip)
    游戏中,不停地调用这个函数,刷新各种动画的当前帧。其中子弹部分的代码:
    // 子弹,攻击控制 for(i=0;i<MAX_MAP_OBJECT;i++) { if(FireArray[i].show) { switch(FireArray[i].id) { default: FireArray[i].iframe=1-FireArray[i].iframe; break; } } }
    子弹的动画只有两帧,所以iframe只是0,1交替变化。至此,子弹在屏幕上显示,并且两帧图片不停播放。
    子弹和小怪碰撞,是游戏中的关键逻辑。网游里也是主要日常工作,打怪。消灭小怪,也是这个游戏的全部乐趣。那么, 这个关键的碰撞检测,以及碰撞检测后的逻辑处理,是怎样的呢?
    十一、子弹运动和打怪玩家按攻击键,生成子弹,存储在数组中,显示,接下来:子弹运动,打怪。先说子弹是怎样运动的。思路:用一个函数不停地检测子弹数组,如果子弹可见,刷新子弹的坐标。
    实现如下:
    int GAMEMAP::CheckAni(int itimeclip){ // 子弹移动 for(i=0;i<MAX_MAP_OBJECT;i++) { // 判断子弹是否可见 if (FireArray[i].show) { // 根据子弹的移动速度movex,修改子弹坐标. // (movex为正,向右移动;为负,向左移动,). FireArray[i].x+=FireArray[i].movex; // 判断子弹是否超出了屏幕范围,如果超出,子弹消失(设置为不可见) if( FireArray[i].x > viewx+VIEWW || FireArray[i].x<viewx-FIRE_MAGIC_MAX_W) { FireArray[i].show = 0; } } }}
    至此,子弹在屏幕上不停地运动。
    打怪是怎样实现的呢:碰撞检测的思路:用一个函数不停地检测所有子弹,如果某个子弹碰到了小怪,小怪消失,子弹消失。
    实现如下:
    int GAMEMAP::CheckAni(int itimeclip){ // 检测子弹和敌人的碰撞(包括魔法攻击) for(i=0;i<MAX_MAP_OBJECT;i++) { // 判断小怪是否可见 if(MapEnemyArray[i].show) { // 检测所有子弹 for(j=0;j<MAX_MAP_OBJECT;j++) { // 判断子弹是否可见 if (FireArray[j].show) { // 判断子弹和小怪是否"碰撞" if(RECT_HIT_RECT(FireArray[j].x+FIRE_XOFF, FireArray[j].y, FireArray[j].w, FireArray[j].h, MapEnemyArray[i].x, MapEnemyArray[i].y, MapEnemyArray[i].w, MapEnemyArray[i].h) ) { // 如果碰撞,小怪消灭 ClearEnemy(i); switch(iAttack) { case ATTACK_NORMAL: // 子弹消失 FireArray[j].show=0;
    如果是旋风,在旋风动画帧结束后消失。
    碰撞检测说明
    子弹和小怪,都被看作是矩形,检测碰撞就是判断两个矩形是否相交。以前,有网友说,碰撞检测有很多优化算法。我还是想不出来,只写成了这样:
    // 矩形与矩形碰撞#define RECT_HIT_RECT(x,y,w,h,x1,y1,w1,h1) ( (y)+(h)>(y1) && (y)<(y1)+(h1) && (x)+(w)>(x1) && (x)<(x1)+(w1) )
    小怪的消失,代码如下所示:
    void GAMEMAP::ClearEnemy(int i){ // 小怪的生命值减一 MapEnemyArray[i].health--; // 如果小怪的生命值减到0, 小怪消失(设置为不可见) if(MapEnemyArray[i].health<=0) { MapEnemyArray[i].show=0; }}
    至此,玩家按下攻击键,子弹生成、显示、运动,碰到小怪,子弹消失,小怪消失。这些功能全部完成。如果只做成这样,不算本事。
    攻击方式分两种:子弹和旋风。小怪包括:两种蘑菇兵和两种火圈。同时,火圈能产生两种蘑菇兵,而旋风的攻击效果明显高于普通子弹。这是不是很复杂?怎样做到的呢?
    十二、旋风攻击、小怪运动、火圈前面介绍了子弹的生成、显示、运动、碰撞、消失的过程。这个过程可以推广到其他精灵上。继续介绍旋风、蘑菇兵、火圈。
    作为魔法攻击方式的旋风,和子弹大同小异。旋风的存储与子弹同存储在一个数组中,如下:
    struct ROLE FireArray[MAX_MAP_OBJECT];
    使用时,用id区分。旋风生成函数如下所示:
    int GAMEMAP::CheckAni(int itimeclip){ // 发子弹 if(iBeginFire) { if(0 == iTimeFire ) { FireArray[iFireNum].show=1; FireArray[iFireNum].iframe = 0; // 子弹方向 if(0==rmain.idirec) { FireArray[iFireNum].movex=1; } else { FireArray[iFireNum].movex=-1; } switch(iAttack) { case ATTACK_MAGIC: FireArray[iFireNum].id=ID_ANI_FIRE_MAGIC; FireArray[iFireNum].x=rmain.xpos-ID_ANI_FIRE_MAGIC_XOFF; FireArray[iFireNum].y=rmain.ypos-ID_ANI_FIRE_MAGIC_YOFF; FireArray[iFireNum].w=FIRE_MAGIC_W; FireArray[iFireNum].h=FIRE_MAGIC_H; FireArray[iFireNum].movex=0; break; } // 移动数组游标 iFireNum=(iFireNum+1)%MAX_MAP_OBJECT; } iTimeFire=(iTimeFire+1)%TIME_FIRE_BETWEEN; }}
    这和子弹生成的处理相同。唯一区别是旋风不移动,所以movex属性最后设置为0。
    旋风的显示原理
    旋风在屏幕上的绘制和子弹相同,代码部分和子弹相同。但是旋风的帧刷新有些特殊处理:
    void GAMEMAP::ChangeFrame(int itimeclip){ // 子弹,攻击控制 for(i=0;i<MAX_MAP_OBJECT;i++) { // 如果攻击(子弹、旋风)可见 if(FireArray[i].show) { switch(FireArray[i].id) { case ID_ANI_FIRE_MAGIC: // 旋风当前帧加一 FireArray[i].iframe++; // 如果帧为2(即第三张图片) ,图片坐标修正,向右移 if(FireArray[i].iframe == 2) { FireArray[i].x+=FIRE_MAGIC_W; } // 如果帧号大于3,即四张图片播放完,旋风消失,设置为不可见 if(FireArray[i].iframe>3) { FireArray[i].show=0; } break; } }
    至此,旋风显示,动画播放结束后消失。旋风不涉及运动。碰撞检测的处理和子弹相同,唯一区别是:当旋风和小怪碰撞,旋风不消失。
    int GAMEMAP::CheckAni(int itimeclip){ switch(iAttack) { case ATTACK_NORMAL: // 子弹消失 FireArray[j].show=0; break; // 旋风不消失 default: break; }
    那么,再看小怪消失的函数:
    void GAMEMAP::ClearEnemy(int i){ MapEnemyArray[i].health--; if(MapEnemyArray[i].health<=0) { MapEnemyArray[i].show=0; }
    可以看到,此时并不区分攻击方式。但旋风存在的时间长(动画结束后消失),相当于多次调用了这个函数,间接提高了杀伤力。至此,两种攻击方式都已实现。
    再看小怪,分蘑菇兵和火圈两种。
    存储问题和攻击方式处理相同,用数组加游标的方法,蘑菇兵和火圈存储在同一数组中,如下:
    struct ROLE MapEnemyArray[MAX_MAP_OBJECT];int iMapEnemyCursor;
    小怪是由地图文件设定好的,以第二关的地图文件为例,其中小怪部分如下:
    ;enemy21 6 1 1 0 15 2423 6 1 1 0 15 2448 7 2 2 6 0 0 68 5 2 2 8 0 0
    各个参数是什么意义呢?看一下加载函数就全明白了。函数如下所示:
    int GAMEMAP::LoadMap(){// 如果文件没有结束后while(temp[0]!='#' && !feof(fp)) { // 读入小怪数据 横坐标 纵坐标 宽 高 id 运动范围左边界 右边界 sscanf(temp,"%d %d %d %d %d %d %d", &MapEnemyArray[i].x, &MapEnemyArray[i].y, &MapEnemyArray[i].w, &MapEnemyArray[i].h, &MapEnemyArray[i].id, &MapEnemyArray[i].xleft, &MapEnemyArray[i].xright); // 坐标转换.乘以32 MapEnemyArray[i].x*=32; MapEnemyArray[i].y*=32; MapEnemyArray[i].w*=32; MapEnemyArray[i].h*=32; MapEnemyArray[i].xleft*=32; MapEnemyArray[i].xright*=32; MapEnemyArray[i].show=1; // 设置移动速度(负,表示向左) MapEnemyArray[i].movex=-ENEMY_STEP_X; // 动画帧 MapEnemyArray[i].iframe=0; // 动画最大帧 MapEnemyArray[i].iframemax=2; // 设置生命值 switch(MapEnemyArray[i].id) { case ID_ANI_BOSS_HOUSE: MapEnemyArray[i].health=BOSS_HEALTH; break; case ID_ANI_BOSS_HOUSE_A: MapEnemyArray[i].health=BOSS_A_HEALTH; break; default: MapEnemyArray[i].health=1; break; } // 将火圈存储在数组的后半段,数值长30, BOSS_CURSOR为15 if ( i<BOSS_CURSOR && ( MapEnemyArray[i].id == ID_ANI_BOSS_HOUSE || MapEnemyArray[i].id == ID_ANI_BOSS_HOUSE_A) ) { // move data to BOSS_CURSOR MapEnemyArray[BOSS_CURSOR]=MapEnemyArray[i]; memset(&MapEnemyArray[i],0,sizeof(MapEnemyArray[i])); i=BOSS_CURSOR; } i++; // 读取下一行地图数据 FGetLineJumpCom(temp,fp); }
    看来比生成子弹要复杂一些,尤其是火圈,为什么要从第15个元素上存储?因为,火圈要不停地生成蘑菇兵,所以”分区管理”,数值前一半存储蘑菇兵,后一半存储火圈。那么,小怪和火圈是怎样显示和运动的呢?火圈怎样不断产生新的小怪?
    十三、小怪和火圈小怪的显示问题,蘑菇兵和火圈处于同一个数组,很简单:
    void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj){ // 显示敌人 for(i=0;i<MAX_MAP_OBJECT;i++) { if (MapEnemyArray[i].show) { bmobj.DrawItem(MapEnemyArray[i].x,MapEnemyArray[i].y, MapEnemyArray[i].id,MapEnemyArray[i].iframe); } }
    同样,如同图片处理所讲,显示一个图片,只需要坐标、id、帧。
    帧刷新和小怪运动的代码如下所示:
    void GAMEMAP::ChangeFrame(int itimeclip){ // 移动时间:每隔一段时间ENEMY_SPEED,移动一下 if(0 == itimeclip% ENEMY_SPEED) { for(i=0;i<MAX_MAP_OBJECT;i++) { // 如果小怪可见 if(MapEnemyArray[i].show) { // 帧刷新 MapEnemyArray[i].iframe=(MapEnemyArray[i].iframe+1)%MapEnemyArray[i].iframemax; switch(MapEnemyArray[i].id) { case ID_ANI_ENEMY_NORMAL: case ID_ANI_ENEMY_SWORD: // 蘑菇兵移动(士兵,刺客) MapEnemyArray[i].x+=MapEnemyArray[i].movex; // 控制敌人移动:向左移动到左边界后,移动速度movex改为向右。移动到右边界后,改为向左。 if(MapEnemyArray[i].movex<0) { if(MapEnemyArray[i].x<=MapEnemyArray[i].xleft) { MapEnemyArray[i].movex=ENEMY_STEP_X; } } else { if(MapEnemyArray[i].x>=MapEnemyArray[i].xright) { MapEnemyArray[i].movex=-ENEMY_STEP_X; } } break; }
    至此,所有小怪不停移动。(火圈的movex为0,不会移动)
    在前面的子弹、旋风的碰撞处理中已讲过。碰撞后,生命值减少,减为0后,消失。火圈会产生新的蘑菇兵,怎样实现的呢?思路:不断地检测火圈是否出现在屏幕中,出现后,生成蘑菇兵。
    int GAMEMAP::CheckAni(int itimeclip){ // 如果在显示范围之内,则设置显示属性 for(i=0;i<MAX_MAP_OBJECT;i++) { // 判断是否在屏幕范围内 if ( IN_AREA(MapEnemyArray[i].x, viewx, VIEWW) ) { // 如果有生命值,设置为可见 if(MapEnemyArray[i].health) { MapEnemyArray[i].show=1; switch(MapEnemyArray[i].id) { // 普通级火圈 case ID_ANI_BOSS_HOUSE: // 每隔一段时间, 产生新的敌人 if(itimeclip == TIME_CREATE_ENEMY) { MapEnemyArray[iMapEnemyCursor]=gl_enemy_normal; MapEnemyArray[iMapEnemyCursor].x=MapEnemyArray[i].x; MapEnemyArray[iMapEnemyCursor].y=MapEnemyArray[i].y+32; // 移动游标 iMapEnemyCursor=(iMapEnemyCursor+1)%BOSS_CURSOR; } break; // 下面是战斗级火圈,处理相似 } } } else { // 不在显示范围内,设置为不可见 MapEnemyArray[i].show=0; } }
    这样,火圈就不断地产生蘑菇兵。
    再说一下模板,这里的模板不是C++的模板。据说template技术已发展到艺术的境界,游戏中用到的和template无关,而是全局变量。如下:
    // 普通蘑菇兵struct ROLE gl_enemy_normal={ 0, 0, 32, 32, ID_ANI_ENEMY_NORMAL, 0, 2, 0, 0, -ENEMY_STEP_X, // speed 1, 1};
    当火圈不断产生新的蘑菇兵时,直接把这个小怪模板放到数组中,再修改一下坐标即可。(对于蘑菇刺客,还要修改id和生命值)
    游戏的主要逻辑完成。此外,还有金币,爆炸效果等其他动态元素,它们是怎么实现的?
    十四、爆炸效果和金币子弹每次攻击到效果,都会显示一个爆炸效果。由于只涉及图片显示,它的结构很简单。如下:
    struct MapObject{ int x; int y; int w; int h; int id; int iframe; int iframemax; // 最大帧数 int show; // 是否显示};
    存储问题,爆炸效果仍然使用数组加游标的方法,如下:
    struct MapObject BombArray[MAX_MAP_OBJECT];int iBombNum;
    当子弹和小怪碰撞后,生成。
    void GAMEMAP::ClearEnemy(int i){ // 生成BombArray[iBombNum].show=1; BombArray[iBombNum].id=ID_ANI_BOMB; BombArray[iBombNum].iframe=0; BombArray[iBombNum].x=MapEnemyArray[i].x-BOMB_XOFF; BombArray[iBombNum].y=MapEnemyArray[i].y-BOMB_YOFF; // 修改数组游标 iBombNum=(iBombNum+1)%MAX_MAP_OBJECT;
    和子弹、小怪的显示方法相同。
    void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj){ for(i=0;i<MAX_MAP_OBJECT;i++) { if (BombArray[i].show) { ystart=BombArray[i].y; xstart=BombArray[i].x; bmobj.DrawItem(xstart,ystart,BombArray[i].id, BombArray[i].iframe); } }
    和子弹、小怪的帧刷新方法相同。
    void GAMEMAP::ChangeFrame(int itimeclip){for(i=0;i<MAX_MAP_OBJECT;i++) { if(BombArray[i].show) { BombArray[i].iframe++; // 当第四张图片显示完毕,设置为不可见。 if(BombArray[i].iframe>3) { BombArray[i].show=0; } } }
    碰撞检测:爆炸效果不涉及碰撞检测。
    消失:如上所述,爆炸效果在动画结束后消失。
    金币的处理比小怪更简单。当玩家和金币碰撞后,金币消失,增加金钱数量。用数组加游标的方法存储,如下:
    struct MapObject MapCoinArray[MAX_MAP_OBJECT];int iCoinNum;
    金币的生成,和小怪相似,从地图文件中加载。以第二关为例,地图文件中的金币数据是:
    6 5 32 32 3 7 5 32 32 3 8 5 32 32 3 9 5 32 32 3 18 4 32 32 3 19 4 32 32 3 20 4 32 32 3
    数据依次表示横坐标、纵坐标、宽、高、图片id。
    int GAMEMAP::LoadMap(){ while(temp[0]!='#' && !feof(fp)) { sscanf(temp,"%d %d %d %d %d", &MapCoinArray[i].x, &MapCoinArray[i].y, &MapCoinArray[i].w, &MapCoinArray[i].h, &MapCoinArray[i].id); MapCoinArray[i].show=1; MapCoinArray[i].iframe=0; // 坐标转换,乘以32 MapCoinArray[i].x*=32; MapCoinArray[i].y*=32; // 设置这个动画元件的最大帧 switch(MapCoinArray[i].id) { case ID_ANI_COIN: MapCoinArray[i].iframemax=4; break; } i++; iCoinNum++; // 读取下一行数据 FGetLineJumpCom(temp,fp); }
    金币显示和小怪的显示方法相同:
    void GAMEMAP::ShowAniObj(MYANIOBJ & bmobj){ // 显示金币,和其他物品 for(i=0;i<iCoinNum;i++) { ystart=MapCoinArray[i].y; xstart=MapCoinArray[i].x; bmobj.DrawItem(xstart,ystart,MapCoinArray[i].id, MapCoinArray[i].iframe); }
    金币帧刷新和小怪的帧刷新方法相同:
    void GAMEMAP::ChangeFrame(int itimeclip){ for(i=0;i<MAX_MAP_OBJECT;i++) { // 如果金币可见,帧加一 if(MapCoinArray[i].show) { MapCoinArray[i].iframe=(MapCoinArray[i].iframe+1)%MapCoinArray[i].iframemax; } }
    金币碰撞检测和小怪的碰撞检测方法相似,区别在于:金币的碰撞检测没有判断是否可见,只要金币位于屏幕中,和玩家碰撞,则金币消失,金钱数量iMoney增加。
    int GAMEMAP::CheckAni(int itimeclip){ for(i=0;i<iCoinNum;i++) { tempx=MapCoinArray[i].x; tempy=MapCoinArray[i].y; if ( IN_AREA(tempx, viewx-32, VIEWW) ) { // 玩家坐标是rmain.xpos rmain.ypos if( RECT_HIT_RECT(rmain.xpos, rmain.ypos, 32,32, tempx, tempy, MapCoinArray[i].w,MapCoinArray[i].h) ) { switch(MapCoinArray[i].id) { case ID_ANI_COIN: // 增加金钱数量 iMoney+=10; // 金币消失 ClearCoin(i); break; } return 0; } } } // end of for
    金币消失和小怪的消失不一样,不需要设置show为0,而是直接删除元素,即数组移动的方法:
    void GAMEMAP::ClearCoin(int i){ // 检查合法性 if(i<0 || i>=iCoinNum) return; // 减少一个金币,或者减少一个其他物品 for(;i<iCoinNum;i++) { MapCoinArray[i]=MapCoinArray[i+1]; } // 修改数量 iCoinNum--;
    由此可见,直接删除元素,省去了是否可见的判断。但凡事都有两面性,移动数组显然比单个元素的设置要慢(实际上不一定,可以优化)。方法多种多样,这就是程序的好处,永远有更好的答案。
    所有的动态元素都介绍完了。所谓动态元素,就是有一个生成、运行、销毁的过程。只不过,有的复杂一些,如子弹、旋风、蘑菇兵、火圈,有些元素简单一些,如爆炸效果、金币。方法都大同小异,要强调的是,这不是最好的方法。碰到金币后,会出现‘+10’的字样,怎么做呢?
    十五、金币提示和攻击提示提示信息,是玩家得到的反馈。比如,碰到金币,金币消失,此时就要显示“+10”;攻击小怪,小怪却没有消失,这时要显示血条,告知玩家小怪的生命值。下面讲提示信息。
    金币提示+10的字样,并没有用文字处理,而是用图片(4帧的动画)。这样,实现起来很简单,和爆炸效果用同一个数组存储,处理方法相同。
    金币的碰撞检测函数如下所示:
    int GAMEMAP::CheckAni(int itimeclip){ for(i=0;i<iCoinNum;i++) { // 判断玩家是否碰到金币 switch(MapCoinArray[i].id) { case ID_ANI_COIN: // 碰到金币 iMoney+=10; // 金币消失,显示+10字样 ClearCoin(i); break;
    金币消失函数如下所示:
    void GAMEMAP::ClearCoin(int i){ switch(MapCoinArray[i].id) { case ID_ANI_COIN: // 碰到了金币,显示+10字样. 和爆炸效果的处理一样, 只是图片id不同 BombArray[iBombNum].show=1; BombArray[iBombNum].id=ID_ANI_COIN_SCORE; BombArray[iBombNum].iframe=0; BombArray[iBombNum].x=MapCoinArray[i].x-COIN_XOFF; BombArray[iBombNum].y=MapCoinArray[i].y-COIN_YOFF; iBombNum=(iBombNum+1)%MAX_MAP_OBJECT; break; }
    攻击提示需要给出攻击对象名称,血条。存储:
    // 攻击对象提示 char AttackName[20]; // 攻击对象名称 int iAttackLife; // 攻击对象当前生命值 int iAttackMaxLife; // 攻击对象最大生命值
    提示信息设置:在小怪被攻击的时候,设置提示信息。其他攻击对象处理相似。
    void GAMEMAP::ClearEnemy(int i){ // 设置攻击对象生命值 iAttackLife=MapEnemyArray[i].health; switch(MapEnemyArray[i].id) { case ID_ANI_BOSS_HOUSE: // 设置名称 strcpy(AttackName,"普通级火圈"); // 设置最大生命值 iAttackMaxLife=BOSS_HEALTH;
    提示信息显示:
    void GAMEMAP::ShowOther(HDC h){ // 如果攻击对象生命值不为0, 显示提示信息 if(iAttackLife) { // 输出名称 TextOut(h,viewx+ATTACK_TO_TEXT_X, ATTACK_TO_TEXT_Y,AttackName,strlen(AttackName)); // 显示血条 xstart=viewx+ATTACK_TO_X-iAttackMaxLife*10; // 按最大生命值显示一个矩形, 作为背景 bmMap.DrawItemNoMaskWidth(xstart-1, ATTACK_TO_Y-1,ID_MAP_HEALTH_BK, iAttackMaxLife*BMP_WIDTH_HEALTH, 0); // 按当前生命值对应的宽度, 显示一个红色矩形 bmMap.DrawItemNoMaskWidth(xstart, ATTACK_TO_Y,ID_MAP_HEALTH, iAttackLife*BMP_WIDTH_HEALTH, 0); }
    金钱数量显示和攻击提示位于同一个函数:
    void GAMEMAP::ShowOther(HDC h){ sprintf(temp,"MONEY: %d",iMoney); TextOut(h,viewx+20,20,temp,strlen(temp));
    至此,攻击系统(子弹、旋风、蘑菇兵,火圈),金币(金币,金钱数量),提示信息(金币提示,攻击提示),这几类元素都介绍过了,还有一个,武器切换,就是从魂斗罗里抠来的那个东西。
    十六、攻击方式切换当玩家碰到武器包(就是魂斗罗里那个东西),攻击方式切换。

    思路:把它放到存储金币的数组中,用id区别。碰撞检测时,如果是金币,金币消失,如果是武器包,攻击方式切换。存储:和金币位于同一个数组MapCoinArray。生成:由地图文件加载。比如第一关的地图文件数据:
    25 4 52 25 5
    各参数含义:横坐标、纵坐标、宽、高、图片id。
    和金币的加载相同,唯一区别是金币图片有4帧,武器包只有2帧,加载函数如下所示:
    int GAMEMAP::LoadMap(){ MapCoinArray[i].iframemax=2;
    显示和金币的处理相同,相同函数,相同代码。(再次显示出图像层的好处)
    帧刷新和金币的处理相同,相同函数,相同代码。(再次显示出图像层的好处)
    碰撞检测和金币的处理相同,如果是武器包,设置新的攻击方式,武器包消失。
    int GAMEMAP::CheckAni(int itimeclip){ switch(MapCoinArray[i].id) { case ID_ANI_ATTACK: // 设置新的攻击方式 iAttack=ATTACK_MAGIC; // 武器包消失 ClearCoin(i); break; }
    武器包的消失和金币的处理相同,相同函数,相同代码,这是逻辑层的好处(放在同一个数组中,处理简单)。
    至此,攻击系统,金币系统,提示信息,武器切换,全部完成。只需要一个地图把所有的物品组织起来,构成一个虚拟世界,呈现在玩家眼前。
    十七、地图物品自从游戏机发明以来,地图是什么样的呢?打蜜蜂,吃豆,地图是一个矩形,玩家在这个矩形框内活动。后来,地图得到扩展,可以纵向移动,比如打飞机;可以横向移动,比如超级玛丽、魂斗罗等等横板过关游戏。再后来,横向纵向都可以移动,后来又有45度地图,3D技术后终于实现了高度拟真的虚拟世界。

    超级玛丽的地图可以看成是一个二维的格子。每个格子的大小是32x32像素。游戏窗口大小为12个格子高,16个格子宽。游戏地图宽度是游戏窗口的5倍,即12个格子高,5x16个格子宽。
    地图物品有哪些呢?地面,砖块,水管。先看一下存储结构:
    struct MapObject{ int x; int y; int w; int h; int id; int iframe; int iframemax; // 最大帧数 int show; // 是否显示};
    各个成员含义是横坐标、纵坐标、宽、高、id、当前帧、最大帧、是否可见。用第一关地图文件的地图物品举例:(只包含5个参数)
    0 9 10 3 0这个物品是什么呢?横向第0个格子,纵向第9个格子,宽度10个格子,高度3个格子,id为0,表示地面。
    在显示的时候,只要把坐标、宽高乘以32,即可正确显示。
    地图所有物品仍然用数组+游标的方法存储,如下:
    struct MapObject MapArray[MAX_MAP_OBJECT];int iMapObjNum;
    从地图文件中加载并生成地图。
    int GAMEMAP::LoadMap(){ while(temp[0]!='#' && !feof(fp)) { // 读取一个物品 sscanf(temp,"%d %d %d %d %d", &MapArray[i].x, &MapArray[i].y, &MapArray[i].w, &MapArray[i].h, &MapArray[i].id); MapArray[i].show=0; iMapObjNum++; i++; // 读取下一个物品 FGetLineJumpCom(temp,fp); }
    地图显示和物品显示一样,只是地面和砖块需要双重循环。对于每个宽w格,高h格的地面、砖块,需要把单个地面砖块平铺w*h次,所以用双重循环。
    void GAMEMAP::Show(MYANIOBJ & bmobj){ for(i=0;i<iMapObjNum;i++) { ystart=MapArray[i].y*32; switch(MapArray[i].id) { //进出水管 case ID_MAP_PUMP_IN: case ID_MAP_PUMP_OUT: xstart=MapArray[i].x*32; bmobj.DrawItemNoMask(xstart, ystart, MapArray[i].id, 0); break; default: for(j=0;j<MapArray[i].h;j++) { xstart=MapArray[i].x*32; for(k=0;k<MapArray[i].w;k++) { bmobj.DrawItemNoMask(xstart, ystart, MapArray[i].id, 0); xstart+=32; } ystart+=32; } // end of for break;
    其中,水管是一个单独完整的图片,直接显示,不需要循环。
    地面、砖块、水管都是静态图片,不涉及帧刷新。保证玩家顺利地行走,如果玩家不踩在物品上,则不停地下落。
    int GAMEMAP::CheckRole(){ // 检测角色是否站在某个物体上 for(i=0;i<iMapObjNum;i++) { // 玩家的下边线,是否和物品的上边线重叠 if( LINE_ON_LINE(rmain.xpos, rmain.ypos+32, 32, MapArray[i].x*32, MapArray[i].y*32, MapArray[i].w*32) ) { // 返回1,表示玩家踩在这个物品上 return 1; } } // 角色开始下落 rmain.movey=1; rmain.jumpx=0; // 此时要清除跳跃速度,否则将变成跳跃,而不是落体 return 0;
    十八、背景物品背景物品更简单,包括草丛,树木,河流,win标志。这些背景物品只需要显示,不涉及逻辑处理。用数组+游标的方法存储,如下:
    struct MapObject MapBkArray[MAX_MAP_OBJECT];int iMapBkObjNum;
    第一关的背景物品数据(含义和地图物品相同):
    17 5 3 2 0 (草丛)76 7 3 2 1 (win标志)10 10 3 2 2 (河流)
    背景物品加载和地图物品加载方法相同。
    int GAMEMAP::LoadMap(){ while(temp[0]!='#' && !feof(fp)) { sscanf(temp,"%d %d %d %d %d", &MapBkArray[i].x, &MapBkArray[i].y, …... MapBkArray[i].iframe=0; iMapBkObjNum++; i++; // 下一个物品 FGetLineJumpCom(temp,fp); }
    背景物品的显示:
    void GAMEMAP::ShowBkObj(MYANIOBJ & bmobj){ for(i=0;i<iMapBkObjNum;i++) { bmobj.DrawItem(xstart,ystart,MapBkArray[i].id,ibkobjframe); }
    帧刷新:背景物品都是2帧动画。所有背景物品当前帧用ibkobjframe控制。
    void GAMEMAP::ChangeFrame(int itimeclip){ if(0 == itimeclip% WATER_SPEED) { ibkobjframe=1-ibkobjframe;
    十九、视图怎样把所有东西都显示在窗口中,并随着玩家移动呢?
    思路:玩家看到的区域称为视图,即12格高,16格宽的窗口(每格32*32像素)。先把整个地图则绘制在一个DC上,然后从这个地图DC中,截取当前视图区域的图像,绘制到窗口中。修改视图区域的坐标(横坐标增加),就实现了地图的移动。
    初始化:
    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow){ // hwindow是游戏窗口的DC句柄 hwindow=GetDC(hWnd); // hscreen是整个地图对应的DC hscreen=CreateCompatibleDC(hwindow); // 建立一个整个地图大小(5倍窗口宽)的空位图,选入hscreen hmapnull=CreateCompatibleBitmap(hwindow,GAMEW*32*5,GAMEH*32); SelectObject(hscreen,hmapnull);
    视图的显示:
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ case WM_PAINT: // hwindow是游戏窗口的DC句柄 hwindow = BeginPaint(hWnd, &ps); SelectObject(hscreen,hmapnull); case GAME_IN: // 显示天空 bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp); // 显示背景物品 gamemap.ShowBkObj(bmMapBkObj); // 显示地图物品 gamemap.Show(bmMap); // 显示动态元素 gamemap.ShowAniObj(bmAniObj); // 显示提示信息 gamemap.ShowOther(hscreen); // 显示玩家 rmain.Draw(); break; if(gamemap.iScreenScale) { // 窗口大小调整功能,代码略 } else { // 从整个地图的DC中, 截取当前视图区域的图像,绘制到窗口 BitBlt(hwindow, 0, 0, GAMEW*32, GAMEH*32, hscreen, gamemap.viewx, 0, SRCCOPY); }
    可以看到,视图的左上角横坐标是viewx,只需要刷新这个坐标,就实现了地图移动。
    视图坐标刷新思路:用一个函数不停地检测,玩家角色和视图左边界的距离,超过特定值,把视图向右移。如果玩家坐标和视图左边界的距离大于150,移动视图。
    void GAMEMAP::MoveView(){ if(rmain.xpos - viewx > 150) { viewx+=ROLE_STEP; //判断视图坐标是否达到最大值(地图宽度减去一个窗口宽度) if(viewx>(mapinfo.viewmax-1)*GAMEW*32) viewx=(mapinfo.viewmax-1)*GAMEW*32; }
    二十、地图切换地图分两种,普通地图和隐藏地图(指通过水管进入的地图)。先讲普图地图的切换,再讲隐藏地图的切换。
    普通地图的切换思路:很简单,用一个数字iMatch表示当前是第几关。每过一关,iMatch+1,加载下一张地图。
    过关检测:用一个函数不停地检测玩家是否到了地图终点,如果是,加载下一关的地图。
    int GAMEMAP::IsWin(){ // 判断玩家的坐标是否到达地图终点(横坐标大于等于地图宽度) if(rmain.xpos >= MAX_PAGE*GAMEW*32 ) { // iMatch增加 iMatch=mapinfo.iNextMap; if(iMatch>=MAX_MATCH) { // 如果iMatch大于关卡数量(即通过最后一关),加载第一关的数据,代码略 } else { // 没有通关 InitMatch();//初始化游戏数据 // 设置玩家角色坐标,初始化玩家角色 rmain.SetPos(BM_USER,3*32,8*32); rmain.InitRole(0,GAMEW*32*MAX_PAGE-32); // 加载下一关的地图 LoadMap(); }
    函数LoadMap()根据iMatch的值加载某一关的地图。而iMatch的修改代码是:

    对于普通地图iMatch取值为0,1,2,…,只需要+1即可,为什么要有一个复杂的赋值过程呢?是为了实现隐藏地图的切换。
    隐藏地图的切换,先看一下LoadMap加载的地图文件是什么样子?超级玛丽增强版的地图存储在一个文本文件中,结构为:
    *0// 第0关的地图数据*1// 第1关的地图数据…*4// 第4关的地图数据
    其中,编号0,1,2表示前三关的普图地图,编号3,4是隐藏地图(3是第0关的隐藏地图,4是第1关的隐藏地图)。怎样表示地图之间的关系呢?
    思路:设计一张“地图信息表”,格式如下:
    第0关:下一关编号,隐藏地图编号第1关:下一关编号,隐藏地图编号…第4关:下一关编号,隐藏地图编号
    这样就形成一个地图信息的处理:

    从“地图信息表”中读取当前关卡的的地图信息。当玩家到达地图终点,读取“下一关”编号;玩家进入水管,读取“隐藏地图编号”。
    游戏的地图信息结构:
    struct MAPINFO{ int iNextMap; int iSubMap;};
    地图信息表(全局变量): (数组的第i个元素,表示第i关的地图信息)
    struct MAPINFO allmapinfo[]={ {1,3}, {2,4}, {MAX_MATCH,-1, }, {-1,0}, {-1,1}};
    对应的逻辑信息为:

    第0关的下一关是第1关,从水管进入第3关。第1关的下一关是第2关,从水管进入第4关。第2关(最后一关)没有下一关(MAX),没有从水管进入的地图。第3关没有下一关,从水管进入第0关。第4关没有下一关,从水管进入第1关。
    这样,实现了从水管进入隐藏关,又从水管返回的功能。
    地图信息的存储在 struct MAPINFO mapinfo; 结构体变量中,每一关的游戏开始前,都要用这个函数初始化游戏数据。包括读取地图信息:
    void GAMEMAP::InitMatch(){ mapinfo=allmapinfo[iMatch];
    玩家到达地图终点的检测:
    int GAMEMAP::IsWin(){ iMatch=mapinfo.iNextMap;
    切换到下一关的地图编号。
    玩家进入水管的检测思路:当玩家按下方向键“下”,判断是否站在水管上(当然进入地图的水管),如果是,切换地图。
    int GAMEMAP::KeyProc(int iKey){ case VK_DOWN: for(i=0;i<iMapObjNum;i++) { // 判断玩家是否站在一个地图物品上 if( LINE_IN_LINE(玩家坐标,地图物品坐标)) { // 这个物品是水管 if(MapArray[i].id == ID_MAP_PUMP_IN) { // 设置游戏状态:进入水管 iGameState=GAME_PUMP_IN;
    函数WndProc中,不断检测GAME_PUMP_IN状态,代码如下:
    case WM_TIMER: switch(gamemap.iGameState) { case GAME_PUMP_IN: if(c1.DecCount()) { // 如果GAME_PUMP_IN状态结束,加载隐藏地图。 gamemap.ChangeMap();:
    是不是复杂一些?确实,它可以简化。我想这还是有好处,它容易扩展。这仍然是我最初的构思,这是一个代码框架。看一下ChangeMap的处理:
    void GAMEMAP::ChangeMap(){ //读取隐藏地图编号 iMatch=mapinfo.iSubMap; //游戏初始化 InitMatch(); //加载地图 LoadMap();
    可见,ChangeMap的简单很简单。因为,LoadMap的接口只是iMatch,我只要保证iMatch在不同情况下设置正确,地图就会正确地加载。
    至此,地图切换实现。但是,地图切换中,还有其它的游戏数据要刷新,怎样处理呢?
    二十一、游戏数据管理进入每一关之前,需要对所有游戏数据初始化。进入隐藏地图,同样需要初始化。而且,从隐藏地图返回上层地图,还要保证玩家出现在“出水管”处。地图数据、玩家数据、视图数据,都要设置正确。
    所有的游戏数据,即封装在gamemap中的数据,分成如下几种:

    场景数据:包含当前关卡的地图,所有精灵,金币,提示信息。视图数据:视图窗口坐标。玩家数据:玩家角色的个人信息,例如金钱数量,攻击方式,游戏次数。
    1.场景数据
    int iGameState; // 当前游戏状态 int iMatch; // 当前关卡 // 各种精灵的数组: struct MapObject MapArray[MAX_MAP_OBJECT]; // 地图物品 struct MapObject MapBkArray[MAX_MAP_OBJECT]; // 地图背景物品 struct ROLE MapEnemyArray[MAX_MAP_OBJECT]; // 小怪 struct MapObject MapCoinArray[MAX_MAP_OBJECT]; // 金币 struct ROLE FireArray[MAX_MAP_OBJECT]; // 子弹 struct MapObject BombArray[MAX_MAP_OBJECT]; // 爆炸效果 // 当前关卡的地图信息 struct MAPINFO mapinfo; // 图片帧int ienemyframe; // 小怪图片帧 int ibkobjframe; // 背景图片帧 // 玩家攻击 int iTimeFire; // 两个子弹的间隔时间 int iBeginFire; // 是否正在发子弹 // 攻击对象提示 char AttackName[20]; // 攻击对象名称 int iAttackLife; // 攻击对象生命值 int iAttackMaxLife; // 攻击对象最大生命值
    2.视图数据
    int viewx; // 视图起始坐标
    3.玩家数据
    int iMoney; // 金钱数量 int iAttack; // 攻击方式 int iLife; // 玩家游戏次数
    可见,每次加载地图前,要初始化场景数据和视图数据,而玩家数据不变,如金钱数量。
    游戏数据处理,假设没有隐藏地图的功能,游戏数据只需要完成初始化的功能,分别位于以下三个地方:

    程序运行前,初始化;过关后,初始化,再加载下一关地图;失败后,初始化,再加载当前地图;
    1.游戏程序运行,所有游戏数据初始化
    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow){ gamemap.Init();void GAMEMAP::Init(){ // 设置游戏初始状态 iGameState=GAME_PRE; // 设置当前关卡 iMatch=0; // 设置玩家数据 玩家游戏次数,金钱数量,攻击种类 iLife=3; iMoney=0; iAttack=ATTACK_NORMAL; // 设置视图坐标 viewx=0; // 初始化场景数据 InitMatch();void GAMEMAP::InitMatch(){ memset(MapArray,0,sizeof(MapArray)); memset(BombArray,0,sizeof(BombArray)); ienemyframe=0; iFireNum=0; ……
    这样,程序启动,InitInstance中完成第一次初始化。
    2.过关后,游戏数据初始化,加载下一关地图
    int GAMEMAP::IsWin(){ // 判断玩家是否到达地图终点 if(rmain.xpos >= MAX_PAGE*GAMEW*32 ) { // 读取下一关地图编号 iMatch=mapinfo.iNextMap; if(iMatch>=MAX_MATCH) { // 如果全部通过 Init(); // 初始化所有数据 LoadMap(); // 加载地图 } else { InitMatch(); // 初始化场景数据 // 设置玩家坐标 rmain.SetPos(BM_USER,3*32,8*32); rmain.InitRole(0,GAMEW*32*MAX_PAGE-32); // 加载下一关的地图 LoadMap(); }
    3.如果玩家失败,重新加载当前地图
    int GAMEMAP::IsWin(){ // 检测角色和敌人的碰撞 for(i=0;i<MAX_MAP_OBJECT;i++) { if(MapEnemyArray[i].show) { if(HLINE_ON_RECT(玩家坐标 小怪坐标)) { if(0 == rmain.movey) { // 玩家在行走过程中,碰到小怪,游戏失败 Fail(); } else { // 玩家在下落过程中,碰到火圈,游戏失败 switch(MapEnemyArray[i].id) { case ID_ANI_BOSS_HOUSE: case ID_ANI_BOSS_HOUSE_A: Fail(); …… // 玩家到达地图底端(掉入小河),游戏失败 if(rmain.ypos > GAMEH*32) { Fail(); return 0; } void GAMEMAP::Fail(){ // 玩家游戏次数减1 iLife--; // 设置游戏状态 iGameState=GAME_FAIL_WAIT;// GAME_FAIL_WAIT状态结束后,调用函数void GAMEMAP::Fail_Wait()加载地图。void GAMEMAP::Fail_Wait(){ if( iLife <=0) { // 游戏次数为0,重新开始,初始化所有数据 Init(); } else { // 还能继续游戏 } // 设置玩家坐标 rmain.SetPos(BM_USER,3*32,8*32); rmain.InitRole(0,GAMEW*32*MAX_PAGE-32); // 加载当前地图 LoadMap();
    至此,在没有隐藏地图的情况下,游戏数据管理(只有初始化)介绍完了。
    增加了隐藏地图的功能,游戏数据管理包括:初始化,数据刷新。哪些数据需要刷新呢?

    刷新玩家坐标
    例如,从第一关(地图编号为0)进入隐藏地图,玩家出现在(3,8),即横向第3格,纵向第8格。玩家返回第一关后,要出现在“出水管”的位置(66,7)。
    刷新视图坐标
    例如,从第一关进入隐藏地图,玩家出现在(3,8),视图对应地图最左边,玩家返回第一关后,视图要移动到“出水管”的位置。
    刷新背景图片的坐标
    例如,从第一关进入隐藏地图,玩家出现在(3,8),天空背景对应地图最左边,玩家返回第一关后,背景图片要移动到“出水管”的位置。

    void GAMEMAP::ChangeMap(){ // 初始化视图坐标 viewx=0; // 获取隐藏地图编号 iMatch=mapinfo.iSubMap; // 初始化场景数据 InitMatch(); // 设置玩家坐标 rmain.SetPos(BM_USER,mapinfo.xReturnPoint*32,mapinfo.yReturnPoint*32); // 玩家角色初始化rmain.InitRole(0,GAMEW*32*MAX_PAGE-32); // 设定视图位置 if(rmain.xpos - viewx > 150) { SetView(mapinfo.xReturnPoint*32-32); // 往左让一格 if(viewx>(mapinfo.viewmax-1)*GAMEW*32) viewx=(mapinfo.viewmax-1)*GAMEW*32; } // 设定人物活动范围 rmain.SetLimit(viewx, GAMEW*32*MAX_PAGE); // 设定背景图片坐标 bmSky.SetPos(BM_USER,viewx,0); // 加载地图 LoadMap();}
    所以,地图信息表中,要包含“出水管”的坐标。完整的地图信息表如下:
    struct MAPINFO{ int iNextMap; // 过关后的下一关编号 int iSubMap; // 进入水管后的地图编号 int xReturnPoint; // 出水管的横坐标 int yReturnPoint; // 出水管的纵坐标 int iBackBmp; // 背景图片ID int viewmax; // 视图最大宽度};struct MAPINFO allmapinfo[]={{1,3,66,7,0,5},{2,4,25,4,1,5},{MAX_MATCH,-1,-1,-1,2,5},{-1,0,3,8,3,1},{-1,1,3,8,3,2}};
    第0关
    {1,3,66,7,0,5},表示第0关的下一关是第1关,从水管进入第3关,出水管位于(66,7),天空背景id为0,视图最大宽度为5倍窗口宽度。
    第3关
    {-1,0,3,8,3,1},表示第3关没有下一关,从水管进入第0关,出水管位于(3,8),天空背景id为3,视图最大宽度为1倍窗口宽度。
    这样,隐藏地图切换的同时,视图数据,玩家数据均正确。
    各个动态元素,地图的各种处理都已完成,只需要让玩家控制的小人,走路,跳跃,攻击,进出水管。玩家的动作控制怎样实现?
    二十二、玩家角色类MYROLE玩家控制的小人,和各种小怪基本一致。没什么神秘的。主要有三个功能要实现:键盘响应,动作控制,图片显示。
    为了方便图片显示,玩家角色类MYROLE直接派生自图片类MYBITMAP。
    MYROLE类定义如下所示:
    class MYROLE:public MYBITMAP{public: // 构造函数,析构函数 MYROLE(); ~MYROLE(); // 初始化部分 // 功能 初始化玩家信息 // 入参 玩家运动范围的左边界 右边界() void InitRole(int xleft, int xright); // 功能 设置玩家运动范围 // 入参 玩家运动范围的左边界 右边界() void SetLimit(int xleft, int xright); // 图片显示部分 // 功能 显示玩家角色图片(当前坐标 当前帧) // 入参 指定的横坐标 纵坐标 帧 void Draw(int x,int y,int iframe); // 功能 刷新帧,该函数没有使用, 帧刷新的功能在其它地方完成 // 入参 无 void ChangeFrame(); // 功能 设置玩家状态. 该函数没有使用 // 入参 玩家状态 void SetState(int i); // 动作部分 // 功能 玩家角色移动 // 入参 无 void Move(); // 功能 玩家角色跳跃. 该函数没有使用 // 入参 指定地点横坐标 纵坐标 void MoveTo(int x,int y); // 功能 从当前位置移动一个增量 // 入参 横坐标增量 纵坐标增量 void MoveOffset(int x,int y); // 功能 向指定地点移动一段距离(移动增量是固定的) // 入参 指定地点横坐标 纵坐标 void MoveStepTo(int x,int y); // 动画部分 // 功能 播放动画 // 入参 无 void PlayAni(); // 功能 设置动画方式 // 入参 动画方式 void SetAni(int istyle); // 功能 判断是否正在播放动画, 如果正在播放动画,返回1.否则,返回0 // 入参 无 int IsInAni(); // 数据部分 // 玩家状态, 该变量没有使用 int iState; // 图片数据 // 玩家当前帧 int iFrame; // 动作控制数据 // 玩家活动范围: 左边界 右边界(只有横坐标) int minx; int maxx; // 运动速度 int movex; // 正值,向右移动 int movey; // 正值,向下移动 // 跳跃 int jumpheight; // 跳跃高度 int jumpx; // 跳跃时, 横向速度(正值,向右移动) // 玩家运动方向 int idirec; // 动画数据 int iAniBegin; // 动画是否开始播放 int iparam1; // 动画参数 int iAniStyle; // 动画方式};
    各个功能的实现:
    键盘响应
    玩家通过按键,控制人物移动。
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ case WM_KEYDOWN: if(gamemap.KeyProc(wParam)) InvalidateRect(hWnd,NULL,false); break; case WM_KEYUP: gamemap.KeyUpProc(wParam); break;
    按键消息包括“按下”和“抬起”两种方式:
    int GAMEMAP::KeyProc(int iKey){ switch(iGameState) { case GAME_PRE: // 选择游戏菜单 switch(iKey) { case 0xd: // 按下回车键 switch(iMenu) { case 0: // 菜单项0“开始游戏” c1.ReStart(TIME_GAME_IN_PRE); // 计时两秒 iGameState=GAME_IN_PRE; // 进入游戏LIFE/WORLD提示状态 break; case 1: // 菜单项1“操作说明” SetGameState(GAME_HELP); // 进入游戏状态“操作说明”,显示帮助信息 break; } break; case VK_UP: // 按方向键“上”,切换菜单项 iMenu=(iMenu+1)%2; break; case VK_DOWN: // 按方向键“下”,切换菜单项 iMenu=(iMenu+1)%2; break; } return 1; case GAME_HELP: // 游戏菜单项“操作说明”打开 switch(iKey) { case 0xd: // 按回车键,返回游戏菜单 SetGameState(GAME_PRE); // 设置游戏状态:选择菜单 break; } return 1; case GAME_IN: // 游戏进行中 // 如果人物正在播放动画,拒绝键盘响应 if(rmain.IsInAni()) { break; } // 根据方向键, X, Z, 触发移动,跳跃,攻击等功能 switch(iKey) { case VK_RIGHT: case VK_LEFT: case VK_DOWN: case KEY_X: // 跳 case KEY_Z: // FIRE // 秘籍 case 0x7a: // 按键F11, 直接切换攻击方式 iAttack=(iAttack+1)%ATTACK_MAX_TYPE; break; case 0x7b: // 按键F12 直接通关(游戏进行中才可以,即游戏状态GAME_IN) rmain.xpos = MAX_PAGE*GAMEW*32; break; } break; } return 0;}
    可见,按键响应只需要处理三个状态:

    菜单选择GAME_PRE操作说明菜单打开GAME_HELP游戏进行中GAME_IN
    说明前两个状态属于菜单控制,函数返回1,表示立即刷新屏幕。对于状态GAME_IN,返回0。游戏过程中,屏幕刷新由其它地方控制。
    按键“抬起”的处理:
    void GAMEMAP::KeyUpProc(int iKey){ switch(iKey) { // 松开方向键“左右”,清除横向移动速度 case VK_RIGHT: rmain.movex=0; break; case VK_LEFT: rmain.movex=0; break; case KEY_X: // 松开跳跃键,无处理 break; case KEY_Z: // 松开攻击键,清除变量iBeginFire,表示停止攻击 iBeginFire=0; break; case KEY_W: // 按W,调整窗口为默认大小 MoveWindow(hWndMain, (wwin-GAMEW*32)/2, (hwin-GAMEH*32)/2, GAMEW*32, GAMEH*32+32, true); break; }
    显示问题:
    void MYROLE::Draw(){ // 判断是否播放动画,即iAniBegin为1 if(iAniBegin) { // 显示动画帧 PlayAni(); } else { // 显示当前图片 SelectObject(hdcsrc,hBm); BitBlt(hdcdest,xpos,ypos, width,height/2, hdcsrc,iFrame*width,height/2,SRCAND); BitBlt(hdcdest,xpos,ypos, width,height/2, hdcsrc,iFrame*width,0,SRCPAINT); }
    二十三、玩家动作控制玩家移动:把行走和跳跃看成两个状态,各自用不同的变量表示横纵方向的速度。
    相关属性:

    行走:横向速度为movex,纵向不移动跳跃:横向速度为jumpx,纵向速度为movey。当前跳跃高度jumpheight运动方向:idirec
    思路:

    第一步:玩家按键,按键处理函数设置这些属性。按键松开,清除动作属性。第二步:用一个函数不停检测这些变量,控制玩家移动。
    按键触发
    int GAMEMAP::KeyProc(int iKey){ switch(iKey) { case VK_RIGHT: // 按右 // 判断是否正在跳跃, 即纵向速度不为0 if(rmain.movey!=0) { // 跳跃过程中, 设置横向速度, 方向向右, 大小为4像素 rmain.jumpx=4; } rmain.movex=4; // 设置横向速度, 方向向右, 大小为4像素 rmain.idirec=0; // 设置玩家方向, 向右 break; case VK_LEFT: // 按左 // 如果是跳跃过程中, 设置横向速度, 方向向左, 大小为4像素 if(rmain.movey!=0) { rmain.jumpx=-4; } rmain.movex=-4; // 设置横向速度, 方向向左, 大小为4像素 rmain.idirec=1; // 设置玩家方向, 向左 break; case KEY_X: // X键跳 // 如果已经是跳跃状态,不作处理,代码中断 if(rmain.movey!=0) break; // 设置纵向速度,方向向上(负值),大小为13 rmain.movey=-SPEED_JUMP; // 将当前的横向速度,赋值给“跳跃”中的横向速度 rmain.jumpx=rmain.movex; break; case KEY_Z: // FIRE if(iBeginFire) break; // 如果已经开始攻击,代码中断 iTimeFire=0; // 初始化子弹间隔时间 iBeginFire=1; // 置1,表示开始攻击 break;
    按键松开
    void GAMEMAP::KeyUpProc(int iKey){ // 松开左右键,清除横向速度 case VK_RIGHT: rmain.movex=0; break; case VK_LEFT: rmain.movex=0; break; case KEY_X: // 跳// 不能清除跳跃的横向速度jumpx// 例如,移动过程中起跳,整个跳跃过程中都要有横向速度 break; case KEY_Z: // FIRE iBeginFire=0; // 停止攻击 break;
    控制移动
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ case WM_TIMER: switch(gamemap.iGameState) { case GAME_IN: rmain.Move();//人物移动 …… break;
    每45毫秒产生一个WM_TIMER消息,在GAME_IN状态下,调用各种检测函数。其中rmain.Move()就是不断检测玩家动作属性,实现移动。
    void MYROLE::Move(){ if(0 == movey) { // 如果不是跳跃, 横向移动 MoveOffset(movex, 0); } else { // 跳跃, 先横向移动, 再纵向移动 MoveOffset(jumpx, 0); MoveOffset(0, movey); } // 玩家帧控制 ”纠错法” if(movex<0 && iFrame<3) { iFrame=3; // 如果玩家向左移动, 而图片向右, 则设置为3(第4张图片) } if(movex>0 && iFrame>=3) { iFrame=0; // 如果玩家向右移动, 而图片向右, 则设置为0(第1张图片) } // 帧刷新 if(movex!=0) { if(0==idirec) iFrame=1-iFrame; // 如果方向向右, 图片循环播放0,1帧 else iFrame=7-iFrame; // 如果方向向左, 图片循环播放3,4帧 } if(movey!=0) {// 跳跃过程中, 帧设置为0(向右),3(向左)// 帧刷新后, 重新设置帧, 就实现了跳跃过程中, 图片静止 iFrame=idirec*3; } // 跳跃控制 if(movey<0) { // 向上运动(纵向速度movey为负值) jumpheight+=(-movey); // 增加跳跃高度 // 重力影响,速度减慢 if(movey<-1) { movey++; } // 到达顶点后向下落, 最大跳跃高度为JUMP_HEIGHT * 32, 即3个格子的高度 if(jumpheight >= JUMP_HEIGHT * 32) { jumpheight = JUMP_HEIGHT * 32; // 跳跃高度置为最大 movey=4; // 纵向速度置为4, 表示开始下落 } } else if(movey>0) { // 下落过程, 跳跃高度减少 jumpheight -= movey; // 重力影响,速度增大 movey++; }
    玩家移动
    void MYROLE::MoveOffset(int x,int y){ // 横纵增量为0,不移动,代码结束 if(x==0 && y==0) return; // 如果碰到物体,不移动,代码结束 if(!gamemap.RoleCanMove(x,y)) return; // 修改玩家坐标 xpos+=x; ypos+=y; // 判断是否超出左边界 if(xpos<minx) xpos=minx; // 设置玩家坐标为左边界 // 判断是否超出右边界 if(xpos>maxx) xpos=maxx;
    碰撞检测
    无论行走,跳跃,都是用函数MoveOffset操纵玩家坐标。这时,就要判断是否碰到物体。如果正在行走,则不能前进;如果是跳跃上升,则开始下落。
    int GAMEMAP::RoleCanMove(int xoff, int yoff){ int canmove=1;// 初始化, 1表示能移动 for(i=0;i<iMapObjNum;i++) { if( RECT_HIT_RECT(玩家坐标加增量,地图物品坐标)) { // 碰到物体,不能移动 canmove=0; if(yoff<0) { // 纵向增量为负(即上升运动), 碰到物体开始下落 rmain.movey=1; } if(yoff>0) { // 纵向增量为正(即下落运动), 碰到物体, 停止下落 rmain.jumpheight=0; // 清除跳跃高度 rmain.movey=0; // 清除纵向速度 rmain.ypos=MapArray[i].y*32-32;// 纵坐标刷新,保证玩家站在物品上 } break; } } return canmove;
    玩家移动的过程中,要不断检测是否站在地图物品上。如果在行走过程中,且没有站在任何物品上,则开始下落。
    int GAMEMAP::CheckRole(){ if(rmain.movey == 0 ) { // 检测角色是否站在某个物体上 for(i=0;i<iMapObjNum;i++) { // 玩家的下边线,是否和物品的上边线重叠 if( LINE_ON_LINE(rmain.xpos, rmain.ypos+32, 32, MapArray[i].x*32, MapArray[i].y*32, MapArray[i].w*32) ) { // 返回1,表示玩家踩在这个物品上 return 1; } } // 角色开始下落 rmain.movey=1; rmain.jumpx=0;// 此时要清除跳跃速度,否则将变成跳跃,而不是落体 return 0;
    至此,玩家在这个虚拟世界可以做出各种动作,跳跃,行走,攻击。增强版中,加入了水管,玩家在进出水管,就需要动画。
    二十四、角色动画玩家在进出水管的时候,需要进入水管、从水管中升起两个动画。当动画播放结束后,切换到新的地图。动画播放过程中,禁止键盘响应,即玩家不能控制移动。
    玩家进水管
    地图物品中,水管分两个,进水管(玩家进入地图)和出水管(从别的地图返回)。两种水管对应不同的图片ID:
    #define ID_MAP_PUMP_IN 9#define ID_MAP_PUMP_OUT 10
    玩家进入水管的检测:
    int GAMEMAP::KeyProc(int iKey){ // 检测玩家按“下”,如果玩家站在进水管上,开始播放动画 case VK_DOWN: for(i=0;i<iMapObjNum;i++) { if( LINE_IN_LINE(玩家坐标的下边界,地图物品的上边界)) { // 判断是否站在进水管上 if(MapArray[i].id == ID_MAP_PUMP_IN) { // 如果站在设置角色动画方式,向下移动 rmain.SetAni(ROLE_ANI_DOWN); iGameState=GAME_PUMP_IN; // 设置游戏状态:进水管 c1.ReStart(TIME_GAME_PUMP_WAIT);// 计时2秒 } } } break;
    动画设置函数:
    void MYROLE::SetAni(int istyle){ iAniStyle=istyle; // 设置动画方式 iparam1=0; // 参数初始化为0 iAniBegin=1; // 表示动画开始播放
    iparam1是动画播放中的一个参数,根据动画方式不同,可以有不同的含义。
    动画播放
    玩家角色显示函数:
    void MYROLE::Draw(){ //判断是否播放动画,即iAniBegin为1 if(iAniBegin) { PlayAni(); //播放当前动画 }
    动画播放函数:
    void MYROLE::PlayAni(){ // 根据不同的动画方式,播放动画 switch(iAniStyle) { case ROLE_ANI_DOWN: // 玩家进入水管的动画,iparam1表示下降的距离 if(iparam1>31) { // 下降距离超过31(即图片高度),玩家完全进入水管,无需图片显示 break; } // 玩家没有完全进入水管,截取图片上半部分,显示到当前的坐标处 SelectObject(hdcsrc,hBm); BitBlt(hdcdest, xpos,ypos+iparam1, width,height/2-iparam1, hdcsrc, iFrame*width,height/2,SRCAND); BitBlt(hdcdest, xpos,ypos+iparam1, width,height/2-iparam1, hdcsrc, iFrame*width,0,SRCPAINT); // 增加下降高度 iparam1++; break;
    玩家进入水管后,切换地图
    在时间片的处理中,当GAME_PUMP_IN状态结束,切换地图,并设置玩家动画:
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ case GAME_PUMP_IN: if(c1.DecCount()) { gamemap.ChangeMap(); // 切换地图 gamemap.SetGameState(GAME_IN); // 设置游戏状态 c1.ReStart(TIME_GAME_IN); // 计时300秒 rmain.SetAni(ROLE_ANI_UP); // 设置动画,图片上升 } InvalidateRect(hWnd,NULL,false); break;
    从水管中上升
    根据不同的动画方式,播放动画:
    void MYROLE::PlayAni(){ switch(iAniStyle) { case ROLE_ANI_UP: if(iparam1>31) { // 如果上升距离超过31(图片高度),动画结束 break; } // 人物上升动画,截取图片上部,显示到当前坐标 SelectObject(hdcsrc,hBm); BitBlt(hdcdest, xpos,ypos+32-iparam1, width,iparam1, hdcsrc, iFrame*width,height/2,SRCAND); BitBlt(hdcdest, xpos,ypos+32-iparam1, width,iparam1, hdcsrc, iFrame*width,0,SRCPAINT); // 增加上升距离 iparam1++; // 如果上升距离超过31(图片高度) if(iparam1>31) { iAniBegin=0; // 动画结束,清除动画播放状态 }
    至此,两个动画方式都实现了。但是,如果在动画播放过程中,玩家按左右键,移动,就会出现,角色一边上升,一边行走,甚至跳跃。怎样解决?如果播放动画,屏蔽键盘响应。
    int GAMEMAP::KeyProc(int iKey){ case GAME_IN: // 如果人物正在播放动画,拒绝键盘响应 if(rmain.IsInAni()) { break; }
    这样,在播放过程中,不受玩家按键影响。玩家所有功能全部实现,接下来看一下整个游戏逻辑。
    二十五、GAMEMAP全局变量类所有游戏数据都需要封装到实际的变量中。整个游戏,就是用类GAMEMAP表示的。
    GAMEMAP类定义如下所示:
    class GAMEMAP{public: // 加载地图 int LoadMap(); // 初始化所有游戏数据 void Init(); // 初始化场景数据 void InitMatch(); // 显示地图物品 void Show(MYANIOBJ & bmobj); // 显示地图背景物品,河流,树木 void ShowBkObj(MYANIOBJ & bmobj); // 显示所有动态元素,金币,小怪等 void ShowAniObj(MYANIOBJ & bmobj); // 显示LIFE, WORLD提示 void ShowInfo(HDC h); // 显示金钱, 攻击提示信息 void ShowOther(HDC h); // 键盘处理 int KeyProc(int iKey); // 按键抬起处理 void KeyUpProc(int iKey); // 移动视图 void MoveView(); // 设置视图起始坐标 void SetView(int x); // 设置视图状态, 函数没有使用 void SetViewState(int i); // 设置游戏状态 void SetGameState(int i); // 碰撞检测 // 判断人物能否移动 int RoleCanMove(int xoff, int yoff); // 检测人物是否站在物品上 int CheckRole(); // 检测所有动态元素之间的碰撞, 子弹和蘑菇兵的生成 int CheckAni(int itimeclip); // 清除一个小怪 void ClearEnemy(int i); // 清除一个金币 void ClearCoin(int i); // 帧刷新 void ChangeFrame(int itimeclip); // 逻辑检测 int IsWin(); // 胜负检测 void Fail(); // 失败处理 void Fail_Wait(); //失败后, 加载地图 // 地图切换 void ChangeMap(); // 错误检查 void CodeErr(int i); // 菜单控制 void ShowMenu(MYANIOBJ & bmobj); // 构造和析构函数 GAMEMAP(); ~GAMEMAP(); // 数据部分 int iMatch; // 当前关卡 int iLife; // 游戏次数 int iGameState; // 游戏状态 // 地图物品数组 游标 struct MapObject MapArray[MAX_MAP_OBJECT]; int iMapObjNum; // 地图背景物品数组 游标 struct MapObject MapBkArray[MAX_MAP_OBJECT]; int iMapBkObjNum; // 小怪火圈数组 游标 struct ROLE MapEnemyArray[MAX_MAP_OBJECT]; int iMapEnemyCursor; // 金币武器包 数组 游标 struct MapObject MapCoinArray[MAX_MAP_OBJECT]; int iCoinNum; // 下一个地图编号, 变量没有使用 int iNextMap; // 玩家数据 int iMoney; // 金钱数量 int iAttack; // 攻击方式 // 视图数据 int viewx; // 视图横坐标 int viewy; // 视图纵坐标 int iViewState; // 视图状态 // 地图信息 struct MAPINFO mapinfo; // frame control int ienemyframe; // 小怪帧 int ibkobjframe; // 背景物品帧 // 子弹数组 游标 struct ROLE FireArray[MAX_MAP_OBJECT]; int iFireNum; int iTimeFire; // 两个子弹的时间间隔 int iBeginFire; // 是否开始攻击 // 爆炸效果,+10字样 数组 游标 struct MapObject BombArray[MAX_MAP_OBJECT]; int iBombNum; // 攻击对象提示 char AttackName[20]; // 名称 int iAttackLife; // 生命值 int iAttackMaxLife; // 最大生命值 // 菜单部分 int iMenu; // 当前菜单项编号 // 屏幕缩放 int iScreenScale; // 是否是默认窗口大小};
    所有的数据都存储到一系列全局变量中:
    // 所有菜单文字char *pPreText[]={ "操作: Z:子弹 X:跳 方向键移动 W:默认窗口大小",};// 所有动态元素的图片宽 高int mapani[2][10]={{32,32,64,32,32,52,64,32,64,32},{32,32,64,32,32,25,64,32,64,32},};// 所有地图物品的图片宽 高int mapsolid[2][13]={{32,32,32,32,32,32,32,32,32,64,64,20,100},{32,32,32,32,32,32,32,32,32,64,64,10,12}};// 所有背景物品的图片宽 高int mapanibk[2][4]={{96,96,96,96},{64,64,64,64},};// 旋风的宽 高int mapanimagic[2][1]={{192},{128}};// 所有地图信息struct MAPINFO allmapinfo[]={{1,3,66,7,0,5},{2,4,25,4,1,5},{MAX_MATCH,-1,-1,-1,2,5},{-1,0,3,8,3,1},{-1,1,3,8,3,2}};// 普通蘑菇兵模板struct ROLE gl_enemy_normal={ 0, 0, 32, 32, ID_ANI_ENEMY_NORMAL,};// 跟踪打印// FILEREPORT f1;// 计时器MYCLOCK c1;// 游戏全部逻辑GAMEMAP gamemap;//各种图片MYBITMAP bmPre; // 菜单背景,通关,GAMEOVERMYBKSKY bmSky; // 天空背景MYANIOBJ bmMap; // 地图物品MYANIOBJ bmMapBkObj; // 地图背景物品MYANIOBJ bmAniObj; // 所有动态元素MYROLE rmain; // 玩家角色MYANIMAGIC bmMagic; // 旋风// 字体管理MYFONT myfont; // 字体// DC句柄HDC hwindow,hscreen,hmem,hmem2;// 窗口DC, 地图DC, 临时DC,临时DC2// 空位图HBITMAP hmapnull;// 窗口大小int wwin,hwin; // 显示器屏幕宽 高int wwingame,hwingame; // 当前窗口宽 高HWND hWndMain; // 窗口句柄
    二十六、菜单控制 窗口缩放菜单控制:开始菜单只有两项:0项“开始游戏”,1项“操作说明”,菜单编号用iMenu表示。
    菜单文字显示:
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ // 在WM_PAINT绘制消息中: case GAME_PRE: gamemap.viewx=0; // 设置视图坐标 bmPre.Stretch(2,2,0); // 菜单背景图片 myfont.SelectFont(0); // 设置文字字体 myfont.SelectColor(TC_BLACK, TC_YELLOW_0);// 设置文字颜色 // 显示3行文字 myfont.ShowText(150,260,pPreText[4]); myfont.ShowText(150,290,pPreText[5]); myfont.ShowText(150,320,pPreText[6]); // 显示箭头 gamemap.ShowMenu(bmAniObj); break;
    菜单箭头显示:
    void GAMEMAP::ShowMenu(MYANIOBJ & bmobj){ // 根据当前菜单编号,决定箭头的纵坐标 bmobj.PlayItem(115,280+iMenu*30, ID_ANI_MENU_ARROW);
    箭头会不停闪烁,怎样刷新帧?就在显示函数PlayItem中,如下
    void MYANIOBJ::PlayItem(int x,int y,int id){ // 按照坐标,ID,显示图片 …… // 切换当前帧 iframeplay=(iframeplay+1)%2;}
    菜单的按键响应:
    int GAMEMAP::KeyProc(int iKey){ switch(iGameState) { case GAME_PRE:// 选择游戏菜单 switch(iKey) { case 0xd:// 按下回车键 switch(iMenu) { case 0: // 菜单项0“开始游戏” c1.ReStart(TIME_GAME_IN_PRE); // 计时两秒 iGameState=GAME_IN_PRE;// 进入游戏LIFE WORLD提示状态 break; case 1: // 菜单项1“操作说明” SetGameState(GAME_HELP); // 进入游戏状态“操作说明”,显示帮助信息 break; } break; case VK_UP: // 按方向键“上”,切换菜单项 iMenu=(iMenu+1)%2; break; case VK_DOWN: // 按方向键“下”,切换菜单项 iMenu=(iMenu+1)%2; break; } return 1; // 表示立即刷新画面
    窗口缩放功能的实现
    窗口是否为默认大小,用iScreenScale表示。iScreenScale为1,表示窗口被放大,将视图区域缩放到当前的窗口大小。
    初始化由构造函数完成,窗口大小检测,用户拉动窗口,触发WM_SIZE消息。
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ case WM_SIZE: // 获取当前窗口宽 高 wwingame=LOWORD(lParam); hwingame=HIWORD(lParam); // 如果窗口小于默认大小,仍然设置为默认数值,图像不缩放 if( wwingame <= GAMEW*32 || hwingame <= GAMEH*32) { wwingame = GAMEW*32; hwingame = GAMEH*32; gamemap.iScreenScale = 0; } else { // 宽度大于高度的4/3 if(wwingame*3 > hwingame*4) { wwingame = hwingame*4/3; // 重新设置宽度 } else { hwingame = wwingame*3/4; // 重新设置高度 } gamemap.iScreenScale =1; // 表示图像需要缩放 } break;
    图像缩放,在WM_PAINT消息处理中,绘制完所有图片后,根据iScreenScale缩放视图区域的图像。
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ // 判断是否缩放图像 if(gamemap.iScreenScale) { // 缩放视图区域图像 StretchBlt(hwindow,0,0, wwingame,hwingame, hscreen, gamemap.viewx,0, GAMEW*32,GAMEH*32, SRCCOPY); } else { // 不缩放,视图区域拷贝到窗口 BitBlt(hwindow, 0, 0, GAMEW*32, GAMEH*32, hscreen, gamemap.viewx, 0, SRCCOPY); }
    二十七、程序框架WinProc怎样把所有的功能组织起来,形成一个完整的游戏呢?游戏状态。不同的游戏状态下,对应不同的图片显示、逻辑处理、按键响应。这样就形成了一个结构清晰的框架。各个模块相对独立,也方便扩展。
    由于是消息处理机制,所有功能对应到消息处理函数WndProc,程序框架如下:
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ 绘图消息WM_PAINT: 状态1:状态1绘图。 状态2:状态2绘图。 …… 计时消息WM_TIMER: 状态1:状态1逻辑处理。发WM_PAINT消息,通知绘图。 状态2:状态2逻辑处理。发WM_PAINT消息,通知绘图。 …… 按键消息WM_KEYDOWN WM_KEYUP: 状态1:状态1逻辑处理。发WM_PAINT消息,通知绘图。 状态2:状态2逻辑处理。发WM_PAINT消息,通知绘图。 ……}
    程序入口:
    int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){ MyRegisterClass(hInstance); // 类注册 // 初始化 if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } // 消息循环 while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam;}
    整个消息处理循环,是默认的结构。InitInstance函数复杂初始化。类注册函数MyRegisterClass中,把菜单栏取消了,即wcex.lpszMenuName=NULL,其它不变。
    消息处理函数:
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM){ switch (message) { case WM_PAINT: // 窗口DC hwindow = BeginPaint(hWnd, &ps); // 初始化空图 SelectObject(hscreen,hmapnull); switch(gamemap.iGameState) { case GAME_ERR: // 地图文件加载错误 gamemap.viewx=0; // 视图坐标 // 显示错误信息 bmPre.Stretch(2,2,0); // 背景图片 myfont.SelectColor(TC_WHITE,TC_BLACK);// 文字颜色 myfont.SelectFont(0); // 字体 myfont.ShowText(150,290,pPreText[3]); // 显示文字 break; case GAME_PRE: // 菜单显示 (代码略) break; case GAME_HELP: // 菜单项“操作说明” (代码略) break; case GAME_IN_PRE: // 游戏LIFE,WORLD提示 gamemap.viewx=0; // 视图坐标 bmPre.Stretch(2,2,2); // 背景图片 gamemap.ShowInfo(hscreen); // 显示LIFE,WORLD break; case GAME_IN: // 游戏进行中 case GAME_WIN: // 游戏进行中,过关 case GAME_FAIL_WAIT: // 游戏进行中,失败 case GAME_PUMP_IN: // 游戏进行中,进入水管 bmSky.DrawRollStretch(2,2,gamemap.mapinfo.iBackBmp);// 背景图片 gamemap.ShowBkObj(bmMapBkObj); // 地图背景物品 gamemap.Show(bmMap); // 地图物品 gamemap.ShowAniObj(bmAniObj); // 动态元素 gamemap.ShowOther(hscreen); // 金钱数量,攻击提示 rmain.Draw(); // 玩家角色 break; case GAME_OVER: // 游戏结束 gamemap.viewx=0; bmPre.Stretch(2,2,1); // 输出图片GAME OVER break; case GAME_PASS: // 游戏通关 gamemap.viewx=0; bmPre.Stretch(2,2,3); // 输出图片通关 break; } if(gamemap.iScreenScale) { // 窗口缩放,放大视图区域 StretchBlt(hwindow,0,0,wwingame,hwingame,hscreen, gamemap.viewx,0,GAMEW*32,GAMEH*32,SRCCOPY); } else { // 拷贝视图区域 BitBlt(hwindow,0,0,GAMEW*32,GAMEH*32,hscreen, gamemap.viewx, 0, SRCCOPY); } EndPaint(hWnd, &ps); // 绘图结束 break; case WM_TIMER: switch(gamemap.iGameState) { case GAME_PRE: // 游戏菜单 c1.DecCount();// 计时器减1 if(0 == c1.iNum%MENU_ARROW_TIME) { // 每隔10个时间片(即箭头闪烁的时间),刷新屏幕 InvalidateRect(hWnd,NULL,false); } break; case GAME_IN_PRE: // 游戏LIFE,WORLD提示 if(c1.DecCount()) { // 计时结束,进入游戏。 gamemap.SetGameState(GAME_IN); c1.ReStart(TIME_GAME_IN); // 启动计时300秒 } InvalidateRect(hWnd,NULL,false); // 刷新屏幕 break; case GAME_IN: // 游戏进行中 case GAME_WIN: // 游戏进行中,过关 c1.DecCount();// 计时器计时 if(0 == c1.iNum%SKY_TIME) { bmSky.MoveRoll(SKY_SPEED);// 云彩移动 } gamemap.ChangeFrame(c1.iNum);// 帧控制 rmain.Move();// 人物移动 gamemap.MoveView();// 视图移动 gamemap.CheckRole();// 角色检测 gamemap.CheckAni(c1.iNum);// 逻辑数据检测 gamemap.IsWin(); // 胜负检测 InvalidateRect(hWnd,NULL,false); // 刷新屏幕 break; case GAME_WIN_WAIT: // 游戏进行中,过关,停顿2秒 if(c1.DecCount()) { // 计时结束,进入游戏LIFE,WORLD提示 gamemap.SetGameState(GAME_IN_PRE); InvalidateRect(hWnd,NULL,false); // 刷新屏幕 } break; case GAME_PUMP_IN: // 游戏进行中,进入水管,停顿2秒 if(c1.DecCount()) { // 计时结束,切换地图 gamemap.ChangeMap(); gamemap.SetGameState(GAME_IN); // 进入游戏 c1.ReStart(TIME_GAME_IN); // 启动计时300秒 rmain.SetAni(ROLE_ANI_UP); // 设置玩家出水管动画 } InvalidateRect(hWnd,NULL,false); // 刷新屏幕 break; case GAME_FAIL_WAIT: // 游戏进行中,失败,停顿2秒 if(c1.DecCount()) { // 计时结束,加载地图 gamemap.Fail_Wait(); } break; case GAME_PASS: //全部通关,停顿2秒 if(c1.DecCount()) { // 计时结束,设置游戏状态:游戏菜单 gamemap.SetGameState(GAME_PRE); } InvalidateRect(hWnd,NULL,false); // 刷新屏幕 break; case GAME_OVER: // 游戏结束,停顿3秒 if(c1.DecCount()) { // 计时结束,设置游戏状态:游戏菜单 gamemap.SetGameState(GAME_PRE); } InvalidateRect(hWnd,NULL,false); // 刷新屏幕 break; } break; case WM_KEYDOWN: // 按键处理 if(gamemap.KeyProc(wParam)) InvalidateRect(hWnd,NULL,false); break; case WM_KEYUP: // 按键“抬起”处理 gamemap.KeyUpProc(wParam); break; case WM_SIZE: // 窗口大小调整,代码略 break; case WM_DESTROY: // 窗口销毁,释放DC, 代码略 break;
    终于,所有模块全部完成,游戏制作完成。整个工程差不多3000行代码。第一个制作超级玛丽的程序员,是否用了这么多代码,肯定没有。当时,应该是汇编。3000行C++代码,还达不到汇编程序下的地图规模、图片特效、游戏流畅度。可见,程序的乐趣无穷。
    二十八、InitInstance函数说明BOOL InitInstance(HINSTANCE, int){ // 默认窗口大小 wwingame=GAMEW*32; hwingame=GAMEH*32; // 显示器屏幕大小 wwin=GetSystemMetrics(SM_CXSCREEN); hwin=GetSystemMetrics(SM_CYSCREEN); // 创建窗口 hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, (wwin-wwingame)/2, (hwin-hwingame)/2, wwingame, hwingame+32, NULL, NULL, hInstance, NULL); // 设置窗口句柄 hWndMain=hWnd; //DC hwindow=GetDC(hWnd); // 窗口DC hscreen=CreateCompatibleDC(hwindow); // 地图绘制DC hmem=CreateCompatibleDC(hwindow); // 临时DC hmem2=CreateCompatibleDC(hwindow); // 临时DC // 用空位图初始化各个DC hmapnull=CreateCompatibleBitmap(hwindow,GAMEW*32*5,GAMEH*32); SelectObject(hscreen,hmapnull); SelectObject(hmem,hmapnull); SelectObject(hmem2,hmapnull); // 释放窗口DC ReleaseDC(hWnd, hwindow); // 位图初始化 // 菜单背景图片,通关,GAMEOVER bmPre.Init(hInstance,IDB_BITMAP_PRE1,1,5); bmPre.SetDevice(hscreen,hmem,GAMEW*32,GAMEH*32); bmPre.SetPos(BM_USER,0,0); // 天空背景图片 bmSky.Init(hInstance,IDB_BITMAP_MAP_SKY,1,4); bmSky.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32); bmSky.SetPos(BM_USER,0,0); // 地图物品图片 bmMap.Init(hInstance,IDB_BITMAP_MAP,1,1); bmMap.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32); bmMap.InitAniList(mapsolid[0],mapsolid[1], sizeof(mapsolid[0])/sizeof(int),0); // (其它位图代码略) // 玩家图片初始化 rmain.Init(hInstance,IDB_BITMAP_ROLE,5,1); rmain.SetDevice(hscreen,hmem,GAMEW*32*MAX_PAGE,GAMEH*32); // 字体初始化 myfont.SetDevice(hscreen); // 游戏数据初始化 gamemap.Init(); // 玩家角色初始化坐标,数据初始化 rmain.SetPos(BM_USER,3*32,8*32); rmain.InitRole(0,GAMEW*32*MAX_PAGE-32); // 文件检查 if(!gamemap.LoadMap()) { // 文件加载失败,设置游戏状态:文件错误 gamemap.CodeErr(ERR_MAP_FILE); } // 计时器初始化 c1.SetDevice(hscreen); // 计时器启动,每40毫秒一次WM_TIMER消息 c1.Begin(hWnd, GAME_TIME_CLIP ,-1); // 设置显示方式,显示窗口 ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);}
    1 评论 30 下载 2018-10-04 21:31:36 下载需要6点积分
  • 基于JSP和SQL SERVER实现的B/S架构的超市管理系统

    1 引言社会生活的现代化,使得市场的走向发生巨大变化,由于经济的发展,人们对生活的需求已经不再满足于丰衣足食的低度要求,许多人们往往不是单纯为满足生活必需去购买,而是凭着喜欢、意欲和感观去购买。如果一个商店能够打动顾客、吸引顾客,自然会顾客盈门,而近几年新兴产业中超级市场ERMARKET)的现代化管理方式和便捷的购物方式,尤其是它轻松的购物环境。往往是打动顾客,吸引顾客的最主要的原因,且良好的周密的销售服务更是赢得信誉、吸引顾客的优势所 在。商品经济的高速现代化发展也促进了竞争,使一切不甘落后的商家都争先恐后地采用最新的管理方法来加强自己的竞争地位。因此,超市经营者如果不掌握当今市场发展的这一走向,不能将超市现代化经营作为努力开拓的目标,就无法使经营活络、财源茂盛。
    随着计算机网络技术以及数据库技术的迅速发展,管理信息系统得到了广泛应用。对于一个超市来讲:货品数量少则数以百计,多则数以万计;另外,不同超市的实际情况也有所不同。要对这些货品进行统一、高效的管理,靠人工完成工作量庞大、难免有错漏之处。为此,一个自动化的超市货品管理系统的开发非常必要。
    1.1 背景最初的超市资料管理,都是靠人力来完成的。但近几年我国超市经营规模日趋扩大,销售额和门店数量大幅度增加,而且许多超市正在突破以食品为主的传统格局,向品种多样化发展。小型超市在业务上需要处理大量的库存信息,还要时刻更新产品的销售信息,不断添加商品信息,并对商品各种信息进行统计分析。因此,在超市管理中引进现代化的办公软件,实现超市庞大商品的控制和传输,从而方便销售行业的管理和决策,为超市和超市管理人员解除后顾之忧。
    1.2 技术方案开发和管理一个基于B/S模式的管理信息系统需要开发和利用高效率的网络资源,并且应该充分利用高技术含量的技术。本系统开发中使用了Java Server Pages和Java Bean。为了能将Java Server Pages、Java Bean以及Java Servlets三种技术有机结合起来,本系统的总体架构采用了MVC模式。

    我们可以只使用JSP构建电子商务系统,但如果想完成一个有效的应用程序并用于商业上,则需要综合Java Server Pages,Java Bean,以及Java Servlets三种技术相互补充的力量。这种情况下就必然要使用MVC模式,它把应用程序划分为独立的数据管理Model,表现形式View,和控制组件Controller,成为最先进的图形用户接口的基础。这些划分模块支持独立开发并且以重复使用组件。
    2 系统所有功能模块详细介绍根据实际购物流程,绘制系统流程图,是编写程序代码的逻辑依据。在系统的开发之初,作为开发者,我们查阅了很多资料,并参考现有电子商务模式,从顾客网上购物真实流程及需求考虑,最终找到了购物的基本流程作为程序编写的结构框架。始终模拟实际购物,摆明线索,划清模块做到了有路可循。
    系统处理流程图如下所示:

    2.1 人力资源信息管理
    2.1.1 人事部门信息管理部门信息添加:作为一个中型超市管理信息系统,其中会存在多个部门,部门信息有时会需要添加,部门信息添加模块可以方便快捷的实现部门添加。

    界面描述:部门管理员输入部门编号和部门名称,点击提交后将数据保存到Departments表中。
    部门信息修改:超市管理信息系统,其中存在多个部门,部门信息有时因为各种原因需要修改,部门信息修改模块可以方便快捷的实现部门修改。

    界面描述:部门编号从数据库中的表Departments中读出,部门名称由部门管理员修改。提交后更新表Departments。
    部门信息删除:中型超市管理信息系统中,可能因为企业改革等,现有的部门已经不在适合企业的管理,部门信息需要删除,部门信息删除模块可以灵活的实现部门删除。

    界面描述:部门管理员输入部门编号查询部门,点击删除链接后将删除所选部门,并更新Departments表中的数据。
    2.1.2 员工详细信息管理员工信息添加:任何一个企业都是由各种各样的人才组成的,一个中型超市管理信息系统对员工的信息管理是必不可少的,员工信息添加模块可以方便快捷的实现员工详细信息的添加。

    界面描述:系统从数据库的Employees表中读取数据,并显示在页面上。
    员工信息修改:企业中员工信息的变化是时常发生的,这就需要对员工的信息实时的做出改变,员工信息修改模块可以快捷实时的实现员工信息的修改。

    界面描述:部门管理员输入员工编号查询表Employees中的员工,员工编号从表Employees中读取,其他可以修改的信息也从表Employees中读取,点击修改后更新此表。
    员工信息删除:企业中员工信息的变化是时常发生的,有时因为员工的离职,或者各种其它原因,员工已经离开了该企业,这就需要对员工的信息相应的改变,员工信息删除模块可以快捷实时的实现员工信息的删除。

    界面描述:部门管理员输入员工编号从表Employees中查询员工信息并显示到页面上,点击修改按钮后将Employees表中的数据删除。
    员工批量删除:企业中多个员工信息需要删除时,逐一手工删除是一件很麻烦的事情,员工批量删除正是考虑以上原因而设计的,员工批量删除模块可以快捷,大量的实现多个员工信息的删除。

    界面描述:系统从Employees表中读取数据并显示到页面上,部门管理员点击删除链接删除一条相应的信息。
    员工信息查询:一个大型企业可能有成千上万的员工,当管理人员需要找某一个特定员工时,如果逐一用人眼查询,这几乎是不可能的,员工信息查询模块可以准确的查找特定的员工。

    界面描述:部门管理员输入员工编号并点击查询按钮,系统会查询Employees表中是否有该信息。
    2.1.3 员工考勤信息管理员工考勤信息添加:一个企业为了使员工高效,积极的实现企业下达的各种任务,这就需要各种监督措施,其中员工考勤信息管理正可以实现对员工的督促和鼓励作用,其次,也可以作为各项奖励的标准,员工考勤添加模块可以实现对每一个员工各个方面的考察。

    界面描述:部门管理员输入相应的信息,点击添加按钮后将数据保存到Evaluation表中。
    员工考勤信息修改:企业中员工考勤信息的有时会因为人为主观原因造成各种错误,这就需要对员工的考勤信息快速的做出修改,员工考勤信息修改模块可以及时的实现员工考勤信息的修改。

    界面描述:部门管理员输入考勤编号查询,系统读取Evaluation表中的数据并显示到页面上,管理员修改相应的数据,点击修改后更新表中的数据。
    员工考勤信息删除:企业中员工考勤信息的删除是时常发生的,有时因为员工的离职,或者各种过期考勤信息,以及各种冗余信息等,这就需要对员工的考勤信息及时的删除,员工考勤信息删除模块可以快捷实时的实现员工考勤信息的删除。

    界面描述:系统读取Evaluation表中的数据,显示到页面上,管理员点击删除后删除表中的数据。
    员工考勤信息查询:一个大型企业可能有成千上万的员工的考勤信息,每一个员工也可能有多个不同方面的考勤信息,当管理人员或者员工个人需要找某一个特定员工考勤信息时,如果逐一查询,这可定是不可能的,也是很不合理的,员工考勤信息查询模块可以准确的查找特定员工的考勤信息,或者特定员工的某一方面的考勤信息。

    界面描述:部门管理员输入考勤编号或者用户编号,点击查询后系统将查询Evaluation表中的数据并显示到页面上。
    2.2 公司财务信息管理
    2.2.1 员工工资信息管理员工工资信息添加:作为企业的一员,当付出劳动时,企业也一定要对他们做出回报,工资管理,就是企业对员工物质奖励的最好表示,员工工资添加模块可以快速的对企业所有员工的工资做出具体详细的管理。

    界面描述:部门管理员输入相应的信息,点击添加后将数据保存到Salary表中。
    员工工资信息修改:企业中工资管理偶尔也会发生各种错误,这就需要管理人员能及时的做出修改,员工工资修改模块可以准确的修改某一个具体员工的工资信息。

    界面描述:部门管理员输入员工工资编号查询Salary表,系统将查询的数据显示到页面上,管理员修改数据后更新Salary表中的数据。
    员工工资信息删除:当企业员工离职时,或者经过一段时间后,会发现员工工资表中一些信息时无用的,员工工资删除模块可以解决这样的问题。

    界面描述:部门管理员输入员工工资编号查询Salary表,系统将查询的结果显示到页面上,管理员点击删除删除表中的相应数据。
    员工工资信息的查询:当企业管理人员要准确的知道某一个员工,某一具体时间的工资是,就会发现工资查询时很必要的,工资查询正是针对这一问题提出的。

    界面描述:部门管理员输入员工工资编号查询Salary表,系统将查询的结果显示到页面上。
    2.2.2 商品销售业绩信息管理商品销售业绩显示:商品业绩显示可以很好的反应公司的运营情况。使得决策人员可以准确的做出相应的决策。

    界面描述:系统读取Checkout表中的数据并显示到页面上。
    商品销售业绩删除:随着时间的推移有很多的商品销售信息时冗余的,这就需要管理人员对各种信息经过判断之后做出删除。商品销售业绩删除功能能尽最大可能满足管理人员的需要。

    界面描述:系统读取Checkout表中的数据并显示到页面上,管理员点击删除后系统将Checkout表中的相应数据删除。
    2.2.3 商品采购费用信息管理商品采购费用显示:当公司采购部每采购一批商品是,都要将信息及时的反应到企业财务部,使得企业财务管理人员对企业帐目有章可循。一个企业为了使员工高效,积极的实现企业下达的各种任务,这就需要各种监督措施,其中员工考勤信息管理正可以实现对员工的督促和鼓励作用,其次,也可以作为各项奖励的标准,员工考勤添加模块可以实现对每一个员工各个方面的考察。

    界面描述:系统从Purchase表中读取所有数据并显示到页面上。
    2.3 商品采购部信息管理
    2.3.1 商品类型信息管理商品类型信息添加:为了对商品做出合理的管理商品类型信息的添加是很必要的。

    界面描述:部门管理员输入相应的信息,点击提交后系统将数据保存到ProType表中。
    商品类型信息修改:根据商品编号可以查询商品详细信息,然后修改商品的所有信息。

    界面描述:系统从ProType表中读取数据并显示到页面上,部门管理员修改数据后,点击修改按钮,系统将更新表中的数据。
    商品类型信息删除:根据商品类型编号可以删除该商品的类型信息。

    界面描述:部门管理员输入商品编号并点击查询按钮,系统将从ProType表中查询相应的数据并显示到页面上,管理员点击删除后将删除ProType表中的相应数据。
    2.3.2 商品详细信息管理商品信息添加:作为超市综合管理系统,商品信息的管理是很重要的每当采购部门采购到新的商品是商品信息就要增加。超市也可能因为其它原因增加商品信息,商品添加模块都可以做出快捷的解决方案。

    界面描述:部门管理员输入相应的信息,点击添加后将数据保存到Product表中。
    商品信息删除:当企业经营策略发生改变时,商品信息也会相应的发生改变,商品信息删除模块可以使商品信息跟随经营而改变。

    界面描述:系统将Product表中所有的商品信息显示到页面上,管理员点击删除后系统删除Product表中相应的数据。
    商品信息修改:商品信息的变化是瞬间千变万化的,同一个商品随时间的不同,它的具体信息也是不同的,只有实时的调整才能适应市场的变化,商品信息修改使该变化的最佳方案。

    界面描述:部门管理员输入商品编号查询Product表中相应的商品,系统将查询结果显示到页面上,管理员修改数据后点击修改按钮,系统将数据保存到Product表中。
    商品信息查询:在成千上万种商品种,如果人为寻找某一个商品肯定是不可能的,只有通过商品信息查询模块才能为用户或管理人员解决这个难题。

    界面描述:部门管理员输入商品编号查询Product表中相应的商品,系统将查询结果显示到页面上。
    2.3.3 商品供应商厂家信息管理商品供应商厂家信息添加:“诚信“是当前企业管理的管理,以诚信建立的企业与企业之间的关系是种巨大的财富,如何保留这种财富,创造这种财富,商品供应商厂家信息可以大量的存储这种信息。

    界面描述:部门管理员输入相应的数据,点击添加后将数据保存到Supplyer表中。
    商品供应商厂家信息修改:每一个企业的信息随时间都会有或多或少的改变,商品供应商厂家信息修改可以适应这种变化。

    界面描述:部门管理员输入供应商编号查询Supplyer表中的数据并显示到页面上,修改相应的数据,点击修改后将数据保存到Supplyer表中。
    商品供应商厂家信息删除:企业倒闭或者经营策略的改变,当它对超市商品的供应没有作用时,商品供应商厂家信息的删除是正常的。

    界面描述:部门管理员输入供应商编号查询Supplyer表中的数据并显示到页面上,点击删除后系统将Supplyer表中的相应数据删除。
    商品供应商厂家信息查询:

    界面描述:部门管理员输入供应商编号,点击查询后系统将查询Supplyer表中的数据,并将结果显示到页面上。
    2.3.4 商品供应商联系人信息管理商品供应商毕竟是一种抽象的信息,只有通过商品供应商联系人这种载体,才能充分的利用,商品供应商联系人管理可以完成如下任务:
    商品供应商联系人信息添加;

    界面描述:部门管理员输入相应的信息,点击添加后将数据保存到Saler表中。
    商品供应商联系人信息修改:

    界面描述:部门管理员输入联系人编号,点击查询按钮,系统查询Saler表中的数据,并将结果显示到页面上,管理员修改相应的数据,点击更新后将数据保存到Saler表中。
    商品供应商联系人信息删除:

    界面描述:部门管理员输入联系人编号,点击查询按钮,系统查询Saler表中的数据,并将结果显示到页面上,管理员点击删除后,系统将Saler表中的相应数据删除。
    2.3.5 商品采购信息管理商品是维系超市正常运行的必要条件,商品采购是维持这一活动必不可少的条件,商品采购信息管理可以高效的实现它,包含的功能如下:
    商品采购信息添加:

    界面描述:部门管理员输入相应的数据,点击添加后将数据保存到Purchase表中。
    商品采购信息修改:

    界面描述:系统读取Purchase表中的数据并显示到页面上,管理员修改数据后点击修改按钮,系统更新Purchase表中的相应数据。
    商品采购信息删除:

    界面描述:系统从Purchase表中读取数据并显示到页面上,部门管理员点击删除后,系统将删除Purchase表中的相应数据。
    2.4 商品销售部信息管理
    商品销售信息管理:作为一个超市正是为出售商品而存在的,因此销售管理显得尤为重要,商品销售模块正是它的重要组成部分。

    界面描述:系统从Product表中读取数据并显示到页面上,点击付账后会加入到Checkout表中。
    商品购物清单管理:每次购物后,如果结帐则系统自动生成购物清单。

    界面描述:系统从商品销售信息管理页面读取数据并显示到页面上,用户点击付账后将数据保存到Checkout表中。
    2.5 用户权限及个人密码修改用户权限修改:超市综合管理信息系统中,肯定存在各种不同角色,不同的角色就应该有不同的权限,而只有超级管理员才有角色赋予权利。

    界面描述:超级管理员点击相应的部门会进入相应的修改页面,更新Users表中的数据。
    用户密码的修改:为了系统的安全,用户的应该只有用户个人才能修改,这不仅保证了整个公司的利益也保护了个人隐私。

    界面描述:管理员填写相应的数据后,点击提交后将数据保存到Users表中。
    3 接口设计3.1 用户接口包括商品基本信息管理、进货管理、销售管理等管理界面,其中商品信息管理对商品信息的增,改,删除等设置;进货管理分供应商档案管理和供应商商品管理,增、改、删除供应商及其商品信息;销售管理提供销售时对商品的信息显示及修改。
    3.2 外部接口应用系统通过ODBC和数据库沟通。

    3.3 内部接口该系统适合windows操作系统,没有和其他软件的接口。
    4 运行设计4.1 运行模块组合 商品信息管理模块用来管理商品的一些基本信息,是本系统中数据管理的基本对象。管理超市的全部商品信息。销售管理模块提供销售时商品信息的确认与更新,是本系统的主要模块。销售模块提取数据库里商品的基本信息然后在销售成功时修改货架上商品的数量,当商品货架上的数量低于一定程度是,系统提示管理员,从仓库提取商品补充货架。仓库管理系统负责管理仓库的货物信息,管理人员通过仓库管理模块将仓库的商品转移到货架上。当仓库的货物数量下降时,管理人员通过进货管理联系供应商以采集相应商品。为了用户方便快捷的使用本系统,可以参考系统说明模块。

    商品信息的收集与修改功能: 商品信息管理模块,仓库管理模块商品数量更新功能: 销售管理模块,仓库管理模块查询,打印功能: 商品信息管理模块,销售管理模块,仓库管理模块
    5 系统数据结构设计5.1 逻辑结构设计要点主要逻辑结构如下:
    1.员工信息表

    包括的必填数据项:员工编号,员工姓名,员工性别;可选填数据项:员工籍贯,出生年月,学历,是否结婚,身份证号码,员工电话,员工地址,员工描述;说明:员工编号是唯一的员工标识,使此表的主键. 系统通过添加员工可以使用户登陆到系统相应的管理模块。如图5-1所示。

    2.商品类别表

    商品类别号、商品类别名称。说明:商品类别编号为主键 如图5-2所示。

    3.商品信息表

    商品编号、商品类别号、商品名称、商品单位、商品当前价格、商品进货价格、商品数量、商品描述。编号方法:商品的编号采用位数分类的方法,如图5-3所示。

    4.商品采购信息表

    商品采购信息编号、商品编号、商品名称、采购人员编号、供应商联系人编号、采购数量、采购时单位商品价格、采购时间、采购地点、采购描述、采购日期。说明:这张表标识的是商品采购信息的信息情况的外部信息,采购信息编号为该表的主键。如图5-4所示。

    5.商品销售信息

    商品编号、商品名称、商品单位、商品库存数量、商品当前价格。说明:这张表标识的是商品销售的内部信息列表,商品编号是该表的主键,它与商品信息一一对应。编号方法:商品编号采用自动生成方式。如图5-5所示。

    6.员工部门信息

    部门编号、部门名称。说明:这张表标识的是超市管理信息系统员工部门的信息列表,部门编号是该表的主键。编号方法:部门编号采用自动生成方式。如图5-6所示。

    7.员工考勤信息

    员工考勤编号、员工编号、考勤时间、考勤主题、考勤结果、考勤分数、考勤描述。说明:这张表标识的是超市管理信息系统员工考勤的信息列表,员工考勤编号是该表的主键。编号方法:员工考勤编号采用自动生成方式,员工编号与人事管理系统中员工编号一一对应。如图5-7所示。

    8.员工工资信息

    员工工资编号、员工编号、员工基本工资、员工季度奖金、员工年度奖金、员工鼓励奖金、员工发工资时间。说明:这张表标识的是超市管理信息系统员工工资的信息列表,员工工资编号是该表的主键。编号方法:员工工资编号采用自动生成方式,员工编号与人事管理系统中员工编号一一对应。如图5-8所示。

    9.商品供应商信息

    供应商编号、供应商名称、供应商地址、供应商邮编、供应商生产产品的名称。说明:这张表标识的是超市管理信息系统中商品采购模块中商品供应商的信息列表,供应商编号是该表的主键。编号方法:商品供应商编号采用自动生成方式。如图5-9所示。

    10.商品供应商联系人信息

    供应商联系人编号、联系人姓名、联系人性别、联系人职位、联系人公司名称、联系人爱好、联系人电话、联系人描述、联系人公司编号。说明:这张表标识的是超市管理信息系统中商品采购模块中商品供应商联系人的信息列表,供应商联系人编号是该表的主键。编号方法:商品供应商联系人编号采用自动生成方式。如图5-10所示。

    11.系统登陆用户信息

    用户编号、用户姓名、用户密码、用户登陆身份。说明:这张表标识的是超市管理信息系统中登陆到系统的用户的信息列表,用户编号是该表的主键,其中用户编号与员工信息表中的用户编号是一一对应的。如图5-11所示。

    6 经验与教训中小型超市综合管理信息系统的开发是在Window7平台上,以JSP+JavaBean为前台,采用SQL Server 2008作为数据库管理系统管理后台数据库。本系统是超市信息管理建设中必不可少的一部分,它实现了现代管理信息系统的大部分功能需要。使用本系统可以使企业管理更加方便快捷,合理的页面设计也使得这个企业用户充分享受到未来基于Internet管理信息系统的优越。本系统开发说明:

    功能完备
    在开发初期,查看了大量关于电子商务,管理信息系统,J2EE等方面的资料,同时借鉴了很多其他电子商务网站和管理信息的流程。经过总结,确定了满足需求分析的基本模块。系统总体设计上实现了整个系统模块的划分,系统主要包含5大模块,分别是:人事管理信息,企业财务管理,商品采购管理,商品销售管理,个人信息咨询,基本上实现了综合管理系统的所有功能。 
    界面友好
    系统用户登陆到管理页面后,每页有导航和引领的作用。系统根据用户角色的不同,直接进入不同的管理页面,同时导航条方便快捷的引导用户进行各种合理的操作。
    管理科学
    本系统一开始就从管理学的角度做出了详细细致的考虑,后来有参考了ERP,现代电子商务管理等,最后才做出了系统总体设计,同时在设计中也遵循现代企业管理的理念,因此可以讲该系统是较为科学的。

    这一次团队开发综合管理信息系统,从开始选择课题的困惑到最终完成了一个我们还算满意的作品,我们学到了很多很多东西。需求分析—>系统架构设计—>总体模块设计—>详细模块设计—>编码—>调试测试,按照这个步骤一步一步走过来,我们的进度可以说是相对比较慢的。后台管理部分就是在后期制作完成的。近1个月的不断磨练,我们最大的收获除了学到了真正可以应用的知识外,更重要的是激发了我们对Java和JSP的强烈兴趣。
    1 评论 37 下载 2018-10-04 21:03:49 下载需要20点积分
显示 45 到 53 ,共 8 条
eject