分类

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

资源列表

  • 基于QT的考试管理系统设计与实现

    一、项目概要1.1 项目名称
    考试管理系统
    1.2 项目目标
    培养快速学习新的知识,解决问题的能力规划项目的整体功能以及相关需求分析,并设计出合理的数据库,并熟悉整个试题系统的开发流程。
    1.3 软件概要开发一个考试管理系统,考生可以进行练习,和在线考试,管理员负责管理题库以及生成试卷,登陆主界面如图1.3所示。

    1.4 功能描述
    涉及到两个模块:学生登录和管理员登陆。
    基于学生的功能有:
    练习试题(此试题为题库中所有试题类型的所有题,考生可以任意答题且参考标准答案)
    在线考试(试卷从后台试卷库里面随机挑选,考生必须在指定时间内答完试题,交卷后显示考生成绩以及所用时间等信息)
    基于管理员的功能有:
    试题管理(管理题库中所有题,可以进行增删改查,支持关键字、难度等级查询)
    试卷管理(按照要求从题库随机挑选试题生成试卷,对已生成的试卷进行增删改查,支持成批生成试卷,成批删除试卷,根据试卷名,试卷内容,难度等级查询试卷,以及查看试卷内容)

    1.5 开发环境
    操作系统:Microsoft Windows 10开发环境:Qt Creator 8.1数据库:MySql Server 5.5
    1.6 关键技术
    面向对象设计与分析C++Qt数据库编程Qt信号槽
    二、软件详细需求2.1 学生功能主界面学生登录后进入如图2.1所示的界面,进行考试考试或者练习。

    2.2 管理员功能主界面管理员登陆后进入如图2-2所示界面,管理员可以对题库里的题增删改查,也可为学生在线考试随机组卷。

    2.3 学生在线考试系统实现学生进入考试系统,从已生成的试卷随机抽取答题,进入如图2-3-1所示界面,要求学生在规定的时间内答完试卷,可随机跳转试题,且将已做过或即时更新的的答案保存到数组。点击交卷或者退出考试将显示如图2-3-2所示界面,显示考试用时和考试成绩等信息。


    2.4 学生练习系统实现该考试练习从题库按照各种类型题抽取,考生可以切换题型,答完题也可查看正确答案。

    2.5 试题管理系统实现该试题管理系统将对题库类型题分类管理,每个类型题对应一个增删改查界面,如图2-5-2所示。



    题型
    添加题目
    修改题目
    删除题目
    查询题目




    选择题
    题目id自动增加,填写相关题干,abcd选项内容,答案,设置分数,等级难度等信息,点提交即可写入数据库
    页面显示题库该类型题所有信息,点击修改
    页面显示题目该类型题所有信息,选中行点击删除弹出是否删除页面,若确定则删除该题,可刷新页面
    支持难度和关键字以与关系的四种查询方式


    判断题
    题目id自动增加,填写内容题干,答案,设置分数,等级难度等信息,点击提交即可
    页面显示题库该类型题所有信息,点击修改
    页面显示题目该类型题所有信息,选中行点击删除弹出是否删除页面,若确定则删除该题,可刷新页面
    支持难度和关键字以与关系的四种查询方式


    填空题
    题目id自动增加,填写内容题干,输入空格数量,在下面随机生成,填写入对应空格即可设置分数,等级难度等信息,点击提交即可
    页面显示题库该类型题所有信息,点击修改,其中空格数量不可修改
    页面显示题目该类型题所有信息,选中行点击删除弹出是否删除页面,若确定则删除该题,可刷新页面
    支持难度和关键字以与关系的四种查询方式







    2.6 组卷系统主界面
    2.7 试卷生成实现可按照要求从题库随机抽选题型组成填写的试卷数量,要求所选题型与对应个数成绩相加等于总分,且题库里有该填写内容的要求的试题,否则弹出相关不满足要求的题型表,如图2-7-2所示,若不填知识点描述默认为综合,不选择难度等级则是随机。


    2.8 查看已生成试卷信息


    试卷查询
    支持试卷名,难度等级,知识点查询的三种方式随机组合查询




    现有试卷数
    始终随着查询,删除的更新变化,显示当前试卷数


    删除试卷
    选中行点击删除弹出是否删除试卷,若确定则删除该试卷,可刷新页面,支持成批删除(选中多行删除)


    查看试卷
    对选中试卷查看具体信息,显示试卷名,以及改试卷所有题型(使用QScrollArea控件显示)





    三、系统整体设计3.1 系统结构图
    3.2 模块要求
    功能界面层
    Qt主界面
    学生功能界面
    管理员功能界面
    DataBase MySql
    提供给上层的数据库访问,完成指定试卷试题学生管理员等数据信息的取得
    各种类型题已保存在数据库中
    对数据的添加、修改、删除,查询提供指定数据表

    四、登陆测试



    Accou:wrong Answ:wrong
    Accou:wrong Answer:right
    Account:right Answ:wrong
    Account:right Answer:right





    请选择身份
    请选择身份
    请选择身份
    请选择身份


    学生
    用户名或密码错误
    用户名或密码错误
    用户名或密码错误
    进入学生功能界面


    管理员
    用户名或密码错误
    用户名或密码错误
    用户名或密码错误
    进入管理员功能界面





    五、数据库设计(试卷管理系统)
    5 评论 96 下载 2018-10-05 22:46:51 下载需要15点积分
  • 基于Bootstrap框架和SSH框架实现的旅游自助系统网站APP

    1 需求分析1.1 五类地方旅游类App下载量分布图从下载量来看,交通出行类App显然是最受用户欢迎的,在数量少于景点攻略类App的情况下,交通出行类App总下载量为前者2.43倍,占总下载量的70%。六大平台中仅有应用宝上,交通出行类App总下载量低于景点攻略类App。

    1.2 六大商店地方旅游类App下载量分布图六大商店地方旅游类App下载量显示,在百度手机助手上线的地方旅游类App总计下载量为8912449次,明显领先360助手、安卓市场、安智市场、豌豆荚、应用宝五家商店。

    2 可行性分析经济可行性

    服务器:Linux系统的阿里云服务器
    PC机:开发电脑3台
    数据库:MySQL
    建模工具:Rotational Rose 2003

    技术可行性

    版本控制系统:SVN
    前端:Bootstrap框架、Html5+CSS3、JavaScript、Ajax
    后台:Struts2框架、Hibernate框架、Spring框架
    设计模式:拟采用单例模式、适配器和外观模式
    算法:AES加密算法、MD5加密算法、路径规划算法

    社会因素可行性分析

    所有软件都选用正版
    所有技术资料都由提出方保管
    合同制定确定违约责任
    用户使用可行性分析

    3 分析图用例建模


    流程图
    系统分析
    时序图
    E-R图



    数据库设计
    四、关键技术
    版本控制系统:SVN
    前端:Bootstrap框架、Html5+CSS3、JavaScript、Ajax
    后台:Struts2框架、Hibernate框架、Spring框架
    设计模式:拟采用单例模式、适配器和外观模式
    算法:AES加密算法、MD5加密算法、路径规划算法



    五、模块测试景点和旅游路线的查询,支持模糊查询



    预定旅游服务,用户登录注册之后就可以预定旅游服务

    后台管理系统,旅游公司的管理员可以进行接收旅游服务订票操作


    旅游公司管理员可以新开旅游路线

    维护旅游公司的旅游路线

    开发景点,支持图片上传,带Jquery的城市选择器

    进行景点维护,采用UEditor富文本编辑器框架进行编辑,可以将样式写入数据库
    2 评论 28 下载 2018-11-28 11:36:15 下载需要11点积分
  • 基于python的中文聊天机器人

    前言发布这篇 Chat 的初衷是想和各位一起分享一下动手来做聊天机器人的乐趣,因此本篇文章适合用于深度机器学习的研究和兴趣发展,因为从工业应用的角度来看使用百度、科大讯飞的 API 接口会更加的适合。在这篇文章中,希望和大家一起共同交流和探索动手实践的乐趣,当然也欢迎大神来做深度的探讨以及吐槽。这篇 Chat 的基础源代码来自互联网,我进行了综合优化和部分代码的重写,我也会在这边文章发布的同时将所有源代码上传到 Git 分享出来,这样在文章中我就不占用篇幅贴出全部的源代码,大家可以从 Git 上 pull 下来对照着文章来看。
    一、系统设计思路和框架本次系统全部使用 Python 编写,在系统设计上遵循着配置灵活、代码模块化的思路,分为数据预处理器、数据处理器、执行器、深度学习模型、可视化展示五个模块。模块间的逻辑关系大致为:数据预处理是将原始语料进行初步的处理以满足于数据处理模块的要求;执行器是整个系统引擎分别在运转的时候调用数据处理器、深度学习模型进行数据处理、模型训练、模型运作等工作;深度学习模型是一个基于TF的seq2seq模型,用于定义神经网络并进行模型计算;可视化展示是一个用Flask前端框架写的简单的人机交互程序,在运行时调用执行器进行人机对话。整体的框架图如下:

    本系统利用seq2seq模型的特点,结合word2vec的思路(当然这样做有点简单粗暴,没有进行分词),将训练语料分为Ask语料集和Replay语料集,并根据一定的比例分为训练语料集和验证语料集,然后就是word2vec。这些步骤都是在数据预处理器和数据处理器中完成的,关于训练语料集与验证语料集的数量都是做成可外部配置的,这也是这个系统我认为设计的最合理的地方(可惜不是我设计的)。在完成数据的处理后,执行器就会根据训练模式(这也是外部可配置的)来调用seq2seq进行神经网络的创建,神经网络的超参数同样也是可以外部进行配置的。在进行训练过程中,使用perprelixy来计算模型的loss,通过自动的调整learning rate来逐步的取得最优值,当learning rate减少为0达到最优值。最后,就是可视化展示模块启动一个进程调用执行器来实时在线提供聊天服务,在语句输入和输出利用seq2seq的特点,直接将输入seq转换成vec作为已经训练好的神经网络,然后神经网络会生成一个seq向量,然后通过查询词典的方式将生成的向量替换成中文句子。在神经网络模型这里,TF有GRU和LSTM模型可供选择,我比较了GRU和LSTM的训练效果,发现还是GRU比较适合对话语句场景的训练,这点在后面的代码分析环节会详细解释。
    二、源码结构
    datautls.py(数据预处理器)包含的函数convertseq2seq_files,将主程序分好的ask语料集和response语料集转换成seq2seq文件,以便数据处理器进行进一步的数据处理。对于整个系统,一般该数据预处理器只需要运行一次即可
    prepareData.py(数据处理器)包含的函数:createvocabulary、converttovector、preparecustomdata、 basictokenizer、initialize_vocabulary,这些函数的作用分别是创建字典、句子转向量、根据超参定制化训练数据、基础数据标记、初始化词典
    execute.py(执行器)包含的函数:getconfig、readdata、createmodel、train、 selftest、initsession、decodeline,这些函数的作用分别是获取配置参数和超参、读取数据、创建模型、训练模型、模式测试、初始化会话、在线对话。
    seq2seqmodel.py(深度机器学习模型)包含的函数:init、sampledloss、seq2seqf、step、getbatch,这些函数的作用分别是程序初始化、loss函数、seq2seq函数、拟合模型、获取批量数据
    app.py(可视化展示模块)包含的函数: heartbeat、reply、index,这些函数的作用分别是心跳、在线对话、主页入口

    三、源码讲解3.1 数据预处理器(data_utls.py)首先在代码的头部声明代码编码类型,#!/usr/bin/env Python #coding=utf-8,然后导入所需的依赖:
    import osimport randomimport getConfig
    os是提供对python进行一些操作系统层面的工具,比如read、open等,主要是对文件的一些操作。 random是一个随机函数,提供对数据的随机分布操作。 getConfig是外部定义的一个函数,用来获取seq2seq.ini里的参数配置。 接下来是对原始语料的处理,主要是通过语句的奇偶行数来区分问答语句(由于语料是电影的台词,所以直接默认是一问一答的方式),然后把区分后的语句分别存储下来,保存为ask文件和response文件。
    gConfig = {}gConfig=getConfig.get_config()conv_path = gConfig['resource_data']if not os.path.exists(conv_path): exit()convs = [] # 用于存储对话集合with open(conv_path) as f: one_conv = [] # 存储一次完整对话 for line in f: line = line.strip('\n').replace('/', '') if line == '': continue if line[0] == gConfig['e']: if one_conv: convs.append(one_conv) one_conv = [] elif line[0] == gConfig['m']: one_conv.append(line.split(' ')[1])ask = [] # 用来存储问的语句response = [] # 用来存储回答的语句for conv in convs: if len(conv) == 1: continue if len(conv) % 2 != 0: # 保持对话是一问一答 conv = conv[:-1] for i in range(len(conv)): if i % 2 == 0: ask.append(conv[i]) #因为这里的i是从0开始的,因此偶数为问的语句,奇数为回答的语句 else: response.append(conv[i])
    然后调用convertseq2seqfiles函数,将区分后的问答语句分别保存成训练集和测试机,保存文件为train.enc、train.dec、test.enc、test.dec。convertseq2seqfiles函数就不一一贴出来,可以对照源码来看。
    3.2 数据处理器(prepareData.py)编码声明和导出依赖部分不再重复,在真正的数据处理前,需要对训练集和测试集中的一些特殊字符进行标记,以便能够进行统一的数据转换。特殊标记如下:
    PAD = "__PAD__"#空白补位GO = "__GO__"#对话开始EOS = "__EOS__" # 对话结束UNK = "__UNK__" # 标记未出现在词汇表中的字符START_VOCABULART = [PAD, GO, EOS, UNK]PAD_ID = 0#特殊标记对应的向量值GO_ID = 1#特殊标记对应的向量值EOS_ID = 2#特殊标记对应的向量值UNK_ID = 3#特殊标记对应的向量值
    接下来定义生成词典函数,这里及后续的函数只贴出函数名和参数部分,详细的代码参见Git上的源码:
    def create_vocabulary(input_file,output_file):
    这个函数算法思路会将input_file中的字符出现的次数进行统计,并按照从小到大的顺序排列,每个字符对应的排序序号就是它在词典中的编码,这样就形成了一个key-vlaue的字典查询表。当然函数里可以根据实际情况设置字典的大小。
    def convert_to_vector(input_file, vocabulary_file, output_file):
    这个函数从参数中就可以看出是直接将输入文件的内容按照词典的对应关系,将语句替换成向量,这也是所有seq2seq处理的步骤,因为完成这一步之后,不管原训练语料是什么语言都没有区别了,因为对于训练模型来说都是数字化的向量。
    def prepare_custom_data(working_directory, train_enc, train_dec, test_enc, test_dec, enc_vocabulary_size, dec_vocabulary_size, tokenizer=None):
    这个函数是数据处理器的集成函数,执行器调用的数据处理器的函数也主要是调用这个函数,这个函数是将预处理的数据从生成字典到转换成向量一次性搞定,将数据处理器对于执行器来说实现透明化。working_directory这个参数是存放训练数据、训练模型的文件夹路径,其他参数不一一介绍。
    3.3 seq2seq 模型seq2seq模型是直接参照TF官方的源码来做的,只是对其中的一些tf参数针对tf的版本进行了修正。如Chat简介中说的,考虑到大家的接受程度不一样,本次不对代码、算法进行太过深入的分析,后面我会开一个达人课专门详细的进行分析和相关知识的讲解。
    class Seq2SeqModel(object):
    前面的导入依赖不赘述了,这里在开头需要先定义一个Seq2SeqModel对象,这个对象实现了一个多层的RNN神经网络以及具有attention-based解码器,其实就是一个白盒的神经网络对象,我们只需拿来用即可,详细的可以参阅http://arxiv.org/abs/1412.7449这个paper。关于这个paper这次不做解读。
    def __init__(self, source_vocab_size, target_vocab_size, buckets, size, num_layers, max_gradient_norm, batch_size, learning_rate, learning_rate_decay_factor, use_lstm=False, num_samples=512, forward_only=False):
    这个函数是整个模型的初始化函数(父函数),这个函数里面会定义多个子函数,简单来讲这个函数执行完之后就完成了训练模型的创建。这个函数中的大部分参数都是通过seq2seq.ini文件进行朝参数配置的,其中uselstm这个参数是决定是使用gru cell还是lstm cell来构建神经网络,gru其实是lstm的变种,我两个cell都测试了,发现在进行语句对话训练时使用gru cell效果会更好,而且好的不是一点。由于时间的缘故,只对超参size、numlayers、learningrate、learningratedecayfactor、use_lstm进行简单对比调试,大家有兴趣的话可以自己进行调参,看看最优的结果值preprlexity会不会小于10。
    sampledloss、seq2seqf、step、get_batch这些子函数不一一的讲了,大家可以百度一下,都有很详细的解释和讲解。如果需要,我会在达人课里对这些子函数进行讲解。
    3.4 执行器_buckets = [(1, 10), (10, 15), (20, 25), (40, 50)]
    这个bukets的设置特别关键,也算是一个超参数,因为这个关系到模型训练的效率。具体设置的时候,有两个大原则:尽量覆盖到所有的语句长度、每个bucket覆盖的语句数量尽量均衡。
    def read_data(source_path, target_path, max_size=None):
    这个函数是读取数据函数,参数也比较简单,其中max_size这个参数默认是空或者是None的时候表示无限制,同时这个参数也是可以通过seq2seq.ini进行设置。
    def create_model(session, forward_only):
    这个函数是用来生成模型,参数简单。model的定义也只有一句:
    model = seq2seq_model.Seq2SeqModel( gConfig['enc_vocab_size'], gConfig['dec_vocab_size'], _buckets, gConfig['layer_size'], gConfig['num_layers'], gConfig['max_gradient_norm'], gConfig['batch_size'], gConfig['learning_rate'], gConfig['learning_rate_decay_factor'], forward_only=forward_only)
    这里可以看到,模型的生成直接调用了seq2seq_model中的对象Seq2SeqModel,将相应的参数按照要求传进去就可以,具体这个对象的作用以及详细的细节如前面所说可以参照具体的paper来研究,但是作为入门的兴趣爱好者来说可以先不管这些细节,先拿来用就可以了,主要关注点建议还是在调参上。
    def train():
    train函数没有参数传递,因为所有的参数都是通过gconfig来读取的,这里面有一个特殊的设计,就是将prepareData函数调用放在train()函数里,这样做的话就是每次进行训练时都会对数据进行处理一次,我认为这是非常好的设计,大家可以参考,因为这个可以保证数据的最新以及可以对增长的数据进行训练。具体代码如下:
    enc_train, dec_train, enc_dev, dec_dev, _, _ = prepareData.prepare_custom_data(gConfig['working_directory'],gConfig['train_enc'],gConfig['train_dec'],gConfig['test_enc'],gConfig['test_dec'],gConfig['enc_vocab_size'],gConfig['dec_vocab_size'],tokenizer=None)def selftest():和def initsession(sess, conf='seq2seq.ini'):
    这两个函数分别是进行测试以及初始会话用的。由于TF的特殊机制,其每次图运算都是要在session下进行的,因此需要在进行图运算之前进行会话初始化。
    def decode_line(sess, model, enc_vocab, rev_dec_vocab, sentence):
    这个函数就是我们整个对话机器人的最终出效果的函数,这个函数会加载训练好的模型,将输入的sentence转换为向量输入模型,然后得到模型的生成向量,最终通过字典转换后返回生成的语句。
    由于执行器包含多种模式,因此我们在最后加上一个主函数入口并对执行模式判断,
    if __name__ == '__main__':if gConfig['mode'] == 'train': # start training train()elif gConfig['mode'] == 'test': # interactive decode decode() else: print('Serve Usage : >> python3 webui/app.py') #当我们使用可视化模块调用执行器时,需要在可视化模块所在的目录下进行调用,而是可视化模块由于包含很多静态文件,所以统一放在webui目录下,因此需要将执行器与可视化模块放在同一个目录下。 print('# uses seq2seq_serve.ini as conf file')#seq2seq_serve.ini与seq2seq.ini除了执行器的模式外所有配置需要保持一致。
    3.5 可视化展示模块def heartbeat():
    由于可视化展示模块需要账期在线运行,为了了解运行状态加了一个心跳函数,定时的输出信息来check程序运行情况。
    def reply():
    这个函数是人机对话交互模块,主要是从页面上获取提交的信息,然后调用执行器获得生成的回答语句,然后返回给前端页面。
    其中有一点设计可以注意一下,就是在进行语句转向量的过程中,为了保证准确识别向量值,需要在向量值中间加空格,比如123,这个默认的会识别成一个字符,但是1 2 3就是三个字符。因此在获取到前端的语句后,在传给执行器之前需要对语句进行字符间加空格处理,如下:
    req_msg=''.join([f+' ' for fh in req_msg for f in fh])def index():
    这个函数是可视化展示模块的首页加载,默然返回一个首页html文件。
    另外,由于TF的特殊机制,需要在可视化模块中初始化会话,这样执行器才能运行,如下:
    import tensorflow as tf import execute sess = tf.Session() sess, model, enc_vocab, rev_dec_vocab = execute.init_session(sess, conf='seq2seq_serve.ini')
    最后和执行器一样,需要加一个主函数入口启动可视化模块,并配置服务地址和端口号,如下:
    if (__name__ == "__main__"): app.run(host = '0.0.0.0', port = 8808)
    四、训练过程和最优值
    2 评论 52 下载 2018-11-20 23:18:57 下载需要11点积分
  • Python自定义豆瓣电影种类,排行,点评的爬取与存储(初级)


    Python 2.7
    IDE Pycharm 5.0.3
    Firefox 47.0.1

    起因
    就是想写个豆瓣电影的爬取,给我电影荒的同学。。。。当然自己也练手啦

    目的
    根据用户输入,列出豆瓣高分TOP(用户自定义)的电影,链接,及热评若干。
    制作不需要Python环境可运行的exe,但由于bug未修复,需要火狐浏览器支持

    方案
    使用PhantomJS+Selenium+Firefox实现

    实现过程
    get到首页后,根据选择,点击种类,然后根据输入需求,进行排序
    抓取每个电影及超链接,进入超链接后,抓取当前电影的热评及长评
    当用户所要求TOP数目大于第一页的20个时候,点击加载更多,再出现20个电影,重复2操作


    以豆瓣高分,然后按评分排序的点击过程(其余操作一致,先种类后排序选择,再爬)


    实现代码# -*- coding: utf-8 -*-#Author:哈士奇说喵#爬豆瓣高分电影及hot影评from selenium import webdriverimport selenium.webdriver.support.ui as uiimport timeprint "---------------system loading...please wait...---------------"SUMRESOURCES = 0 #全局变量driver_detail = webdriver.PhantomJS(executable_path="phantomjs.exe")#driver_item=webdriver.PhantomJS(executable_path="phantomjs.exe")driver_item=webdriver.Firefox()url="https://movie.douban.com/"#等待页面加载方法wait = ui.WebDriverWait(driver_item,15)wait1 = ui.WebDriverWait(driver_detail,15)#获取URL和文章标题def getURL_Title(): global SUMRESOURCES###############################################################################需要键入想要获取的信息,比如种类,排序方式,想看多少内容############################################################################## print "please select:" kind=input("1-Hot\n2-Newest\n3-Classics\n4-Playable\n5-High Scores\n6-Wonderful but not popular\n7-Chinese film\n8-Hollywood\n9-Korea\n10-Japan\n11-Action movies\n12-Comedy\n13-Love story\n14-Science fiction\n15-Thriller\n16-Horror film\n17-Cartoon\nplease select:") print "--------------------------------------------------------------------------" sort=input("1-Sort by hot\n2-Sort by time\n3-Sort by score\nplease select:") print "--------------------------------------------------------------------------" number = input("TOP ?:") print "--------------------------------------------------------------------------" ask_long=input("don't need long-comments,enter 0,i like long-comments enter 1:") print "--------------------------------------------------------------------------" global save_name save_name=raw_input("save_name (xx.txt):") print "---------------------crawling...---------------------" driver_item.get(url)###############################################################################进行网页get后,先进行电影种类选择的模拟点击操作,然后再是排序方式的选择#最后等待一会,元素都加载完了,才能开始爬电影,不然元素隐藏起来,不能被获取#wait.until是等待元素加载完成!############################################################################## wait.until(lambda driver: driver.find_element_by_xpath("//div[@class='fliter-wp']/div/form/div/div/label[%s]"%kind)) driver_item.find_element_by_xpath("//div[@class='fliter-wp']/div/form/div/div/label[%s]"%kind).click() wait.until(lambda driver: driver.find_element_by_xpath("//div[@class='fliter-wp']/div/form/div[3]/div/label[%s]"%sort)) driver_item.find_element_by_xpath("//div[@class='fliter-wp']/div/form/div[3]/div/label[%s]"%sort).click() num=number+1#比如输入想看的TOP22,那需要+1在进行操作,细节问题 time.sleep(2) #打开几次“加载更多” num_time = num/20+1 wait.until(lambda driver: driver.find_element_by_xpath("//div[@class='list-wp']/a[@class='more']")) for times in range(1,num_time): time.sleep(1) driver_item.find_element_by_xpath("//div[@class='list-wp']/a[@class='more']").click() time.sleep(1) wait.until(lambda driver: driver.find_element_by_xpath("//div[@class='list']/a[%d]"%num)) #print '点击\'加载更多\'一次' #使用wait.until使元素全部加载好能定位之后再操作,相当于try/except再套个while把 for i in range(1,num): wait.until(lambda driver: driver.find_element_by_xpath("//div[@class='list']/a[%d]"%num)) list_title=driver_item.find_element_by_xpath("//div[@class='list']/a[%d]"%i) print '----------------------------------------------'+'NO' + str(SUMRESOURCES +1)+'----------------------------------------------' print u'电影名: ' + list_title.text print u'链接: ' + list_title.get_attribute('href') #print unicode码自动转换为utf-8的 #写入txt中部分1 list_title_wr=list_title.text.encode('utf-8')#unicode码,需要重新编码再写入txt list_title_url_wr=list_title.get_attribute('href') Write_txt('\n----------------------------------------------'+'NO' + str(SUMRESOURCES +1)+'----------------------------------------------','',save_name) Write_txt(list_title_wr,list_title_url_wr,save_name) SUMRESOURCES = SUMRESOURCES +1 try:#获取具体内容和评论。href是每个超链接也就是资源单独的url getDetails(str(list_title.get_attribute('href')),ask_long) except: print 'can not get the details!'###############################################################################当选择一部电影后,进入这部电影的超链接,然后才能获取#同时别忽视元素加载的问题#在加载长评论的时候,注意模拟点击一次小三角,不然可能会使内容隐藏##############################################################################def getDetails(url,ask_long): driver_detail.get(url) wait1.until(lambda driver: driver.find_element_by_xpath("//div[@id='link-report']/span")) drama = driver_detail.find_element_by_xpath("//div[@id='link-report']/span") print u"剧情简介:"+drama.text drama_wr=drama.text.encode('utf-8') Write_txt(drama_wr,'',save_name) print "--------------------------------------------Hot comments TOP----------------------------------------------" for i in range(1,5):#四个短评 try: comments_hot = driver_detail.find_element_by_xpath("//div[@id='hot-comments']/div[%s]/div/p"%i) print u"最新热评:"+comments_hot.text comments_hot_wr=comments_hot.text.encode('utf-8') Write_txt("--------------------------------------------Hot comments TOP%d----------------------------------------------"%i,'',save_name) Write_txt(comments_hot_wr,'',save_name) except: print 'can not caught the comments!' #加载长评 if ask_long==1: try: driver_detail.find_element_by_xpath("//img[@class='bn-arrow']").click() #wait.until(lambda driver: driver.find_element_by_xpath("//div[@class='review-bd']/div[2]/div/div")) time.sleep(1) #解决加载长评会提示剧透问题导致无法加载 comments_get = driver_detail.find_element_by_xpath("//div[@class='review-bd']/div[2]/div") if comments_get.text.encode('utf-8')=='提示: 这篇影评可能有剧透': comments_deep=driver_detail.find_element_by_xpath("//div[@class='review-bd']/div[2]/div[2]") else: comments_deep = comments_get print "--------------------------------------------long-comments---------------------------------------------" print u"深度长评:"+comments_deep.text comments_deep_wr=comments_deep.text.encode('utf-8') Write_txt("--------------------------------------------long-comments---------------------------------------------\n",'',save_name) Write_txt(comments_deep_wr,'',save_name) except: print 'can not caught the deep_comments!'###############################################################################将print输出的写入txt中查看,也可以在cmd中查看,换行符是为了美观##############################################################################def Write_txt(text1='',text2='',title='douban.txt'): with open(title,"a") as f: for i in text1: f.write(i) f.write("\n") for j in text2: f.write(j) f.write("\n")def main(): getURL_Title() driver_item.quit()main()
    上面的代码是可以实现的,但需要Firefox的配合,因为我其中一个引擎调用了Firefox,另一个抓评论的用了PhantomJS。
    实现效果

    存入的txt文件



    因为打包成exe必须是中文的键入,所以没办法,我改成英文来着,不然会出现这种情况。。。



    输出内容是没有问题的。。。。。。

    问题及解决方案Q: 使用PhantomJS和Firefox出现不同效果的问题,第21个回到起点。
    A: 解决方案,暂且我也没有找到,只有调用Firefox然后完事后再关闭,分析请见伪解决Selenium中调用PhantomJS无法模拟点击(click)操作
    Q: 在对unicode输出在txt出现的问题,但是在print可以直接中文输出的。
    A: 解决方案:详见Python输出(print)内容写入txt中保存
    Q: 元素无法定位问题
    A: 首先查看是不是隐藏元素,其次再看自己的规则有没有写错,还有就是是不是页面加载未完成,详见解决网页元素无法定位(NoSuchElementException: Unable to locate element)的几种方法
    Q: 只采集自己需要的数据,剔除无用数据,比如说,刚开始我用
    driver_detail.find_elements_by_xpath然后写个取出list中元素的方法,但是这样的话,一个便签下内容未必太多,并不是我想要的如图:

    比如说,我只想要红色的部分,那么,采取elements就不太好处理。
    A: 我采用的方法是格式化字符串!根据元素的特性,可以发现,每个热评的正文标签不一样的,其余标签一样,只要格式化正文标签即可,像这样
    for i in range(1,5):#取了前四条热评 try: comments = driver_detail.find_element_by_xpath("//div[@id='hot-comments']/div[%s]/div/p"%i) print u"最新热评:"+comments.text except: print 'can not caught comments!'
    Q: 一个引擎干有个事!我现在没办法,只有将第一个需要处理的页面用Firefox来处理,之后评论用PhantomJS来抓取,之后可以用quit来关闭浏览器,但是启动浏览器还是会耗费好多资源,而且挺慢,虽然PhantomJS也很慢,我12G内存都跑完了。。。。。。看样子是给我买8x2 16G双通道的借口啊。
    Q: 备注不标准也会导致程序出错,这个是我没想到的,我一直以为在’’’备注’’’之间的都可以随便来,结果影响程序运行了,之后分模块测试才注意到这个问题,也是以前没有遇到过的,切记!需要规范自己代码,特别是像Python这样缩进是灵魂的语言。。。。
    Q: 补充,长评论的抓取

    这是点击之后的图,可以看到元素定位也是不一样的,注意
    2 评论 1 下载 2019-04-26 22:35:17 下载需要9点积分
  • 基于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
    2 评论 59 下载 2018-10-06 00:06:12 下载需要9点积分
  • 基于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);}
    2 评论 40 下载 2018-10-04 21:31:36 下载需要6点积分
  • 基于Java实现的多用户同步MarkDown编辑器

    一、功能简介
    1.1 离线功能支持本地文件导入、保存,导出HTML、Word文档,添加CSS样式、导入外部CSS文件,大文件处理。如果用户一直输入,没有停下超过1秒,右边的HTML视图就不会更新,这样可以减少无用的计算。左侧的导航栏用树形结构表示,点击标题可以跳转到对应行。

    1.2 在线编辑模拟多人在线编辑的效果,可以用IDE运行多个当前程序,除了第一个正常,之后的会提示无法创建本地服务器等等好几个错误,这是正常的,因为服务器已经由第一个进程创建,之后的就不能再次创建。忽略即可。

    如图,运行3个进程。

    每一个都注册。然后让一个人创建一个房间,另外两个加入进去。

    他们三个现在在0号房间内,并且用户“大王”是房间主。目前设计的模式是只有房间主人可以进行文本的修改,其他成员无权修改。

    二、技术实现2.1 Markdown解析2.1.1 Markdown导出主要用了开源的markdown4j包。
    2.1.2 实时显示为了减少不必要的计算,这里用线程阻塞的方式,结合SwingUtilities.InvokeLater()实现高效地更新。每次,想要更新的线程先sleep 1000毫秒,如果之后又有想要更新的线程,就将上一个阻塞,这样的话上一个就直接返回,放弃更新的操作。这样的效果就是,只要持续输入或删除内容,对文本内容做出改变的间隔不超过1秒钟,就一直不会更新,当用户停止1秒后,才会执行更新操作。大大减少了无用的计算。
    private void updateUIEfficiently() { new Thread(() -> { Thread last = lastThread; lastThread = Thread.currentThread(); try { //阻塞上一个更新的线程 if(last != null) { last.interrupt(); } Thread.sleep(1000); } catch(InterruptedException exc) { return; } if(Thread.currentThread().isInterrupted()) return; SwingUtilities.invokeLater(() -> {update();}); if(mIsHost) { String updation = mTextArea.getText(); try { mClient.disposeRequest(RequestType.UPLOAD_UPDATION, updation); } catch (Exception e) { e.printStackTrace(); Utility.error("与服务器端连接出现错误!"); } } }).start();}
    2.1.3 导航栏用了JTree,生成关于目录的树状结构,并对每个节点添加点击事件。
    2.2 Socket编程创建一个本地服务器,开放2个端口,8080和8081。总共创建了2个类Server和Client。Server处理客户端发来的所有请求,并返回回复;Client用于在客户端帮助用户发送所有请求。工程建立在C/S架构上。
    所有客户端一旦试图进行登录或注册,就会创建一个Client,并建立相应的端口进行通信。服务器也会新开一个线程对这个客户端进行服务。客户端进行的一切操作都是在向服务器发送请求,然后得到服务器的回复。当客户端退出登录或是退出程序,就断开连接的端口,释放资源,防止内存浪费。
    一旦一个用户创建或是加入了一个房间,那么服务器就必须通过另一个端口——8081,来进行房间内内容的更新等操作。所以加入房间后,客户端会试图连接服务器的8081端口。对应的服务器上8081端口专门有一个线程来进行处理,依然是每有一个客户端连接过来就新开一个线程进行服务。这个端口做的事就是专门把房间主人发过来的文本内容发送给房间其他所有成员让他们进行同步。所以,每个客户端也必须开一个线程,不断尝试从对应8081端口的socket读取内容,然后更新。
    关于请求的类型定义在RequestType.java中:
    /** * 向服务器提交请求,请求格式为:[请求类型]#[参数列表(中间用#隔开)] * @param request*/public void disposeRequest(RequestType type, String ... args) throws Exception { //传输请求 switch (type) { case LOGIN: disposeLogin(type, args[0], args[1]); break; //注册和登录在客户端的处理不区分 case REGISTER: disposeLogin(type, args[0], args[1]); break; case CUT_CONNECT: disposeCut(type); break; case CREATE_ROOM: disposeCreateRoom(type); break; case JOIN_ROOM: disposeJoinRoom(type, args[0]); break; case UPLOAD_UPDATION: disposeUpdation(type, args[0]); break; default: break; }}
    一共定义了这么几种类型。同样,服务器端也根据某些特征判断发来的请求是什么类型的,然后执行相应的处理方法。
    目前设计的模式是只有房间主人可以对文本内容进行更改。由于时间有限,没有添加申请成为房间主人的功能,不过这个只不过是在RequestType中加入了一个新的请求。
    1 评论 4 下载 2019-10-17 07:30:24 下载需要12点积分
  • 《WINDOWS黑客编程技术详解》配套资源下载

    《WINDOWS黑客编程技术详解》是一本面向黑客编程初学者的书,较为全面的地总结黑客编程技术。其内容重在实践,着重剖析技术实现原理,向读者讲解黑客编程技术的实现方法。
    本书介绍的是些黑客编程的基础技术,涉及用户层下的Windows编程和内核层下的Rootkit编程。全书分为用户篇和内核篇两部分,用户篇包括11章,配套49个示例程序源码;内核篇包括7章,配套28个示例程序源码。本书每个技术都有详细的实现原理分析,以及对应的示例代码(配套代码均支持32位和64位Windows 7、Windows 8.1及Windows 10系统),帮助初学者建立起黑客编程技术的基础技能。
    本书面向对计算机系统安全开发感兴趣,或者希望提升安全开发水平的读者,以及恶意代码分析研究方面的安全人员。
    购书方式
    淘宝、天猫、京东等各大电商网站均有纸质书和电子书销售,请搜索 “WINDOWS黑客编程技术详解”。
    当当:http://product.dangdang.com/25859838.html
    京东:https://item.jd.com/12464379.html
    天猫:https://detail.tmall.com/item.htm?spm=a230r.1.14.76.cb1940a5YFnLgL&id=582626540408&ns=1&abbucket=18
    书籍封面(正面)

    书籍封面(反面)

    目录

    第1篇 用户篇

    第1章 开发环境

    1.1 环境安装1.2 工程项目设置1.3 关于Debug模式和Release模式的小提示
    第2章 基础技术

    2.1 运行单一实例2.2 DLL延时加载2.3 资源释放
    第3章 注入技术

    3.1 全局钩子注入3.2 远线程注入3.3 突破SESSION 0隔离的远线程注入3.4 APC注入
    第4章 启动技术

    4.1 创建进程API4.2 突破SESSION 0隔离创建用户进程4.3内存直接加载运行
    第5章 自启动技术

    5.1 注册表5.2 快速启动目录5.3 计划任务5.4 系统服务
    第6章 提权技术

    6.1 进程访问令牌权限提升6.2 Bypass UAC
    第7章 隐藏技术

    7.1 进程伪装7.2傀儡进程7.3 进程隐藏7.4 DLL劫持
    第8章 压缩技术

    8.1 数据压缩API8.2 ZLIB压缩库
    第9章 加密技术

    9.1 Windows自带的加密库9.2 Crypto++密码库
    第10章 传输技术

    10.1 Socket通信10.2 FTP通信10.3 HTTP通信10.4 HTTPS通信
    第11章 功能技术

    11.1 进程遍历11.2 文件遍历11.3 桌面截屏11.4 按键记录11.5 远程CMD11.6 U盘监控11.7 文件监控11.8 自删除

    第2篇 内核篇

    第12章 开发环境

    12.1 环境安装12.2 驱动程序开发与调试12.3 驱动无源码调试12.4 32位和64位驱动开发
    第13章 文件管理技术

    13.1 文件管理之内核API13.2 文件管理之IRP13.3 文件管理之NTFS解析
    第14章 注册表管理技术

    14.1 注册表管理之内核API14.2 注册表管理之HIVE文件解析
    第15章 HOOK技术

    15.1 SSDT Hook15.2过滤驱动
    第16章 监控技术

    16.1 进程创建监控16.2 模块加载监控16.3 注册表监控16.4 对象监控16.5 Minifilter文件监控16.6 WFP网络监控
    第17章 反监控技术

    17.1 反进程创建监控17.2 反线程创建监控17.3 反模块加载监控17.4 反注册表监控17.5 反对象监控17.6 反Minifilter文件监控
    第18章 功能技术

    18.1 过PatchGuard的驱动隐藏18.2 过PatchGuard的进程隐藏18.3 TDI网络通信18.4 强制结束进程18.5 文件保护18.6 文件强删

    附录 函数一览表

    PS:源码下载可以直接点击下述附件下载,也可以到 github 和人民邮电出版社-异步社区上面下载:
    https://github.com/BigGan/Windows-Hack-Programming
    https://www.epubit.com/bookDetails?id=N39391
    PS:若对书中内容有疑惑或者发现错误,可以直接戳下面的勘误收集链接哦
    https://www.write-bug.com/article/1966.html
    19 评论 405 下载 2018-11-26 11:48:09
  • 基于VC++的画图板程序

    1. 概述1.1 简介使用VC开发平台,MFC框架实现一个画图程序,尽可能多的实现Windows自带的画图功能,并扩展其功能。
    1.2 功能需求1.2.1 基本绘图功能
    能够用鼠标操控方式,绘制直线、矩形、椭圆。
    在绘图时,选择绘制某种图像后(如直线),在画布中按住鼠标左键后移动鼠标,在画布中实时的根据鼠标的移动显示相应的图形。在松开鼠标左键后,一次绘图操作完成。
    能够在绘制一图形(如一条直线)前设置线的粗细、颜色。(以菜单方式)
    可以以矢量图方式保存绘制的图形。
    可以读取保存的矢量图形文件,并显示绘图的结果。

    界面友好的要求:

    有画直线、矩形、椭圆的工具箱。
    有颜色选择工具箱。
    对于当前选中的绘图工具,以“下沉”的形式显示。
    在状态栏中显示鼠标的位置。
    在鼠标移向一工具不动时,有工具的功能提示。
    在菜单上有当前选中的菜单项标识(即前面有小钩)
    可以用鼠标操作方式,通过“拖拽”方式,改变画布的大小。
    在画布大而外框小时,应有水平或垂直方向的滚动条。

    1.2.2 高级编辑功能
    具有Undo功能。
    可以用鼠标选中绘制的某一图形。被选中的图形符号有标识(参见Word,如一直线段,其两端点上加了两个小框;矩形上有8个小框点)。
    当鼠标靠近某一目标时,鼠标的形状发生改变
    修改被选中的图形。通过鼠标的“拖拽”,可以改变图形的位置、或大小。
    修改被选中图形的颜色、笔划的粗细。
    删除被选中的图形。
    可以使用鼠标“拖拽”一个虚矩形框,一次选择多个图形。
    可以使用 Ctrl 或Shift加鼠标左键选择多个图形对象。

    1.2.3 附加功能
    可选择打开或关闭工具栏。
    应用程序的标题栏上有程序的图标。
    将图形转换成位图文件的形式保存。
    在选择一个图形元素后(如直线),会有进一步选择线型或线宽的界面。
    仿Word,选择“线型”、“粗细”图标后,会出现进一步选择的选项卡。
    2.主要功能描述

    右键修改选中图形的颜色,粗细,线型,删除选中图形

    右键和鼠标调整图形

    对话框矢量修改所有图形

    3. 技术细节3.1 代码结构3.1.1 代码文件MFC自动生成的文件1个CHDrawPView1个HStroke2个Dialog(HStrokeEditDlg+HStrokeTextDlg)1个ToolBar(HColorBar)
    3.1.2 代码类HDrawPView文件只有一个类:CHDrawPView,该类集成自MFC的CScrollView,主要实现维护画布类CHDrawView和滚动功能。
    HStroke文件里包含目前所有的图形类信息,包括集成与MFC的CObject类的基类HStroke,以及集成自HStroke的具体图形类HStrokeLine(直线),HStrokeRect(矩形),HStrokeEllipse(椭圆),HStrokeText(文本),HStrokePoly(曲线)。
    HStrokeEditDlg文件只有一个类:HStrokeEditDlg,该类集成自MFC的CDialog类,主要用来编辑已有图形类,如下图所示:

    HStrokeTextDlg文件只有一个类:HStrokeTextDlg,该类集成自MFC的CDialog类,主要用来画文本时输入文本信息,如图所示:

    HColorBar类只有一个类:HColorBar类,该类集成自MFC的CToolBar类,呈现一个颜色框,方便用户在绘图时选择不同的颜色。
    3.2 SetROP2实现重绘在画图状态下,鼠标移动时既要擦除旧图形,又要绘制新图形。这里主要有两种实现方法:一是全部重绘,二是先擦除旧图形。
    如果使用矢量图全部重绘,频繁的绘图动作消耗很大,很容易造成屏幕闪动。但是如果将已有图形保存为位图,然后重绘的时候只要绘制位图即可,这样能避免闪动。第二种方法要考虑的就是擦除旧图形的问题,本程序使用SetROP2函数设置MASK的方式,每次绘图时采用非异或运算的方式擦除旧图形:
    pDC->SetROP2(R2_NOTXORPEN); //设置ROP2 DrawStroke(pDC); //画图擦除旧线(自定义函数) SetCurrentPoint(point); //设置新点的坐标(自定义函数) DrawStroke(pDC); //画新线(自定义函数)
    3.3 嵌套View实现画布 m_drawView = new CHDrawView();//创建画布View if (!m_drawView->CreateEx(WS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR, AfxRegisterWndClass(CS_VREDRAW | CS_HREDRAW,LoadCursor(NULL,IDC_CROSS), (HBRUSH)GetStockObject(WHITE_BRUSH),NULL),///白色画布 "",WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, m_tracker.m_rect.left,m_tracker.m_rect.top, m_tracker.m_rect.right-1,m_tracker.m_rect.bottom-1, this->m_hWnd,NULL)){ TRACE0("Failed to create toolbar\n"); return -1; // fail to create } m_drawView->SetDocument((CHDrawDoc*)m_pDocument);//传递CDocument给新View m_drawView->ShowWindow(SW_NORMAL); m_drawView->UpdateWindow(); //设置背景View颜色为灰色 SetClassLong(m_hWnd,GCL_HBRBACKGROUND,(long)GetStockObject(GRAY_BRUSH));
    3.4 鼠标靠近目标时突出显示在鼠标移动的时候,OnMouseMove函数会遍历已有图形,判断鼠标所在点是否属于已有图形范围,如果是,则高亮显示该图形。
    高亮显示的方法比较简单,只要增加CRectTracker即可,而判断当前点是否属于某图形比较有意思:
    3.4.1 判断一点是否属于矩形HStrokeRect使用用MFC的CRect类的IsPointIn方法,当鼠标在矩形边框附近时,认为该点属于HStrokeRect。如图,实线矩形表示HStrokeRect。外矩形为外面的虚线矩形,内矩形为里面的虚线矩形:

    BOOL HStrokeRect::IsPointIn(const CPoint &point){ //矩形左上角x坐标 int x1 = m_points.GetAt(0).x < m_points.GetAt(1).x ? m_points.GetAt(0).x : m_points.GetAt(1).x; //矩形左上角y坐标 int y1 = m_points.GetAt(0).y < m_points.GetAt(1).y ? m_points.GetAt(0).y : m_points.GetAt(1).y; //矩形右下角x坐标 int x2 = m_points.GetAt(0).x > m_points.GetAt(1).x ? m_points.GetAt(0).x : m_points.GetAt(1).x; //矩形右下角y坐标 int y2 = m_points.GetAt(0).y > m_points.GetAt(1).y ? m_points.GetAt(0).y : m_points.GetAt(1).y; //构建外矩行和内矩形 CRect rect(x1,y1,x2,y2), rect2(x1+5,y1+5,x2-5,y2-5); //如果在外矩形内并在内矩形外 if(rect.PtInRect(point) && !rect2.PtInRect(point)) return TRUE; else return FALSE;}
    3.4.2 判断一点是否属于线段首先判断一点是否属于这条线段所属的直线,根据直线的判定公式y1/x1 = y2/x2得到x1y2-x2y1=0,但是在画图中应该在直线附近就能选中,所以在本程序中:|x1y2-x2y1| < 偏差,然后判断该点是否属于这条线段。
    //计算该点到线段HStrokeLine的两个顶点的线段(x1,y1), (x2,y2) int x1 = point.x - m_points.GetAt(0).x; int x2 = point.x - m_points.GetAt(1).x; int y1 = point.y - m_points.GetAt(0).y; int y2 = point.y - m_points.GetAt(1).y; //计算判断量x1*y2 - x2*y1 int measure = x1*y2 - x2*y1; //误差允许范围,也就是直线的“附近” int rule = abs(m_points.GetAt(1).x - m_points.GetAt(0).x) +abs(m_points.GetAt(0).y - m_points.GetAt(1).y); rule *= m_penWidth;//将线宽考虑进去 //属于直线 if(measure < rule && measure > -rule){ //判断该点是否属于这条线段 if(x1 * x2 < 0) return TRUE;; } return FALSE;
    3.4.3 判断一点是否属于椭圆根据椭圆的定义椭圆上的点到椭圆的两个焦点的距离之和为2a,首先计算出椭圆的a, b, c,然后计算出椭圆的两个焦点。
    针对某个点,首先根据点坐标和两个焦点的坐标计算出该点到椭圆焦点的距离,然后减去2a,如果在“附近”,则认为其属于HStrokeEllipse,否则不属于。
    //计算椭圆的a, b, c int _2a = abs(m_points.GetAt(0).x - m_points.GetAt(1).x); int _2b = abs(m_points.GetAt(0).y - m_points.GetAt(1).y); double c = sqrt(abs(_2a*_2a - _2b*_2b))/2; //计算椭圆的焦点 double x1,y1,x2,y2; if(_2a > _2b){//横椭圆 x1 = (double)(m_points.GetAt(0).x + m_points.GetAt(1).x)/2 - c; x2 = x1 + 2*c; y1 = y2 = (m_points.GetAt(0).y + m_points.GetAt(1).y)/2; } else{//纵椭圆 _2a = _2b; x1 = x2 = (m_points.GetAt(0).x + m_points.GetAt(1).x)/2; y1 = (m_points.GetAt(0).y + m_points.GetAt(1).y)/2 - c; y2 = y1 + 2*c; } //点到两个焦点的距离之和,再减去2a //distance(point - p1) + distance(point - p2) = 2*a; double measure = sqrt((x1 - point.x)*(x1-point.x) + (y1 - point.y)*(y1-point.y) ) + sqrt( (point.x - x2)*(point.x - x2) + (point.y - y2)*(point.y - y2)) - _2a; //计算椭圆的“附近” double rule = 4*m_penWidth; if(measure < rule && measure > -rule) return TRUE; else return FALSE;
    3.5 文档序列化MFC提供了良好的序列化机制,只要在类定义时加入DECLARE_SERIAL宏,在类构造函数的实现前加入IMPLEMENT_SERIAL宏,然后实现Serialize方法即可。本程序即使用该方法序列化:首先在CHDrawDoc类实现Serialize方法,保存画布大小和所有图形信息:
    void CHDrawDoc::Serialize(CArchive& ar){ if (ar.IsStoring()) { //保存时,首先保存画布高和宽,然后序列化所有图形 ar<<m_cavasH<<m_cavasW; m_strokeList.Serialize(ar); } else { //打开时,首先打开画布高和宽,然后打开所有图形 ar>>m_cavasH>>m_cavasW; m_strokeList.Serialize(ar); }}
    m_strokeList.Serialize(ar);这一句很神奇,Debug追踪的时候会发现,容器类会自动序列化容器内的元素数量,并调用每个元素的序列化方法序列化,所以还需要对每个图形元素实现序列化,以HStrokeLine为例:在HStrokeLine的类声明中:
    class HStrokeLine : public HStroke {public: HStrokeLine(); DECLARE_SERIAL(HStrokeLine)
    然后在HStrokeLine的构造函数实现前:
    IMPLEMENT_SERIAL(HStrokeLine, CObject, 1)HStrokeLine::HStrokeLine(){ m_picType = PIC_line;}
    最后实现HStrokeLine的序列化函数,因为这里HStrokeLine集成自HStroke类而且没有特殊的属性,而HStroke类实现了Serialize函数,所以HStrokeLine类不需要实现Serilize方法,看一下HStroke的Serialize方法即可:
    void HStroke::Serialize(CArchive& ar){ if(ar.IsStoring()){ int enumIndex = m_picType; ar<<enumIndex<<m_penWidth<<m_penColor; m_points.Serialize(ar); } else{ int enumIndex; ar>>enumIndex>>m_penWidth>>m_penColor; m_picType = (enum HPicType)enumIndex; m_points.Serialize(ar); }}
    3.6 打开保存导出文档序列化实现以后,程序的打开和保存功能就已经完成了。但是从序列化方法可以看出,打开和保存的都是矢量图形,所以这里实现了一个导出为BMP图像的方法,导出:
    //保存文件对话框,选择导出路径 CFileDialog dlg(FALSE, "bmp","hjz.bmp"); if(dlg.DoModal() != IDOK){ return ; } CString filePath = dlg.GetPathName(); // CClientDC client(this);//用于本控件的,楼主可以不用此句 CDC cdc; CBitmap bitmap; RECT rect;CRect r; GetClientRect(&rect); int cx = rect.right - rect.left; int cy = rect.bottom - rect.top; bitmap.CreateCompatibleBitmap(&client, cx, cy); cdc.CreateCompatibleDC(NULL); //获取BMP对象 CBitmap * oldbitmap = (CBitmap* ) cdc.SelectObject(&bitmap); //白色画布 cdc.FillRect(&rect, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH))); //画图 for(int i = 0; i < GetDocument()->m_strokeList.GetSize(); i ++){ GetDocument()->m_strokeList.GetAt(i)->DrawStroke(&cdc); } cdc.SelectObject(oldbitmap); ::OpenClipboard(this->m_hWnd); ::EmptyClipboard(); ::SetClipboardData(CF_BITMAP, bitmap); ::CloseClipboard(); HBITMAP hBitmap = (HBITMAP)bitmap; HDC hDC; int iBits; WORD wBitCount; DWORD dwPaletteSize=0, dwBmBitsSize=0, dwDIBSize=0, dwWritten=0; BITMAP Bitmap; BITMAPFILEHEADER bmfHdr; BITMAPINFOHEADER bi; LPBITMAPINFOHEADER lpbi; HANDLE fh, hDib, hPal,hOldPal=NULL; hDC = CreateDC("DISPLAY", NULL, NULL, NULL); iBits = GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES); DeleteDC(hDC); if (iBits <= 1) wBitCount = 1; else if (iBits <= 4) wBitCount = 4; else if (iBits <= 8) wBitCount = 8; else wBitCount = 24; GetObject(hBitmap, sizeof(Bitmap), (LPSTR)&Bitmap); bi.biSize = sizeof(BITMAPINFOHEADER); bi.biWidth = Bitmap.bmWidth; bi.biHeight = Bitmap.bmHeight; bi.biPlanes = 1; bi.biBitCount = wBitCount; bi.biCompression = BI_RGB; bi.biSizeImage = 0; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrImportant = 0; bi.biClrUsed = 0; dwBmBitsSize = ((Bitmap.bmWidth * wBitCount + 31) / 32) * 4 * Bitmap.bmHeight; hDib = GlobalAlloc(GHND,dwBmBitsSize + dwPaletteSize + sizeof(BITMAPINFOHEADER)); lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDib); *lpbi = bi; hPal = GetStockObject(DEFAULT_PALETTE); if (hPal) { hDC = ::GetDC(NULL); hOldPal = ::SelectPalette(hDC, (HPALETTE)hPal, FALSE); RealizePalette(hDC); } GetDIBits(hDC, hBitmap, 0, (UINT) Bitmap.bmHeight, (LPSTR)lpbi + sizeof(BITMAPINFOHEADER) +dwPaletteSize, (BITMAPINFO *)lpbi, DIB_RGB_COLORS); if (hOldPal) { ::SelectPalette(hDC, (HPALETTE)hOldPal, TRUE); RealizePalette(hDC); ::ReleaseDC(NULL, hDC); } fh = CreateFile(filePath, GENERIC_WRITE,0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (fh == INVALID_HANDLE_VALUE) return ; bmfHdr.bfType = 0x4D42; // "BM" dwDIBSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize + dwBmBitsSize; bmfHdr.bfSize = dwDIBSize; bmfHdr.bfReserved1 = 0; bmfHdr.bfReserved2 = 0; bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + dwPaletteSize; WriteFile(fh, (LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER), &dwWritten, NULL); WriteFile(fh, (LPSTR)lpbi, dwDIBSize, &dwWritten, NULL); GlobalUnlock(hDib); GlobalFree(hDib); CloseHandle(fh);
    3.7 友好用户界面菜单项选中和工具栏图标下沉。该功能的实现非常简单,而且用户体验很好,以当前所画的图形为例:第一步:增加3个菜单项名称 ID直线 ID_DRAW_LINE椭圆 ID_DRAW_ELLIPSE矩形 ID_DRAW_RECT第二步:在工具栏上增加3个工具栏项,注意ID要和上面的三个ID相同。第三步:在CHDrawDoc类的ClassWizard中增加消息响应函数,分别为以上三个ID增加COMMAND和UPDATE_COMMAND_UI的Handler,COMMAND的Handler就是针对按下工具栏按钮或菜单项的响应函数,而UPDATE_COMMAND_UI则是显示菜单栏时执行的操作,有点类似OnDraw。以直线为例,ID_DRAW_LINE的COMMAND的Handler为OnDrawLine
    void CHDrawDoc::OnDrawLine() { //设置当前画图的图形类型为直线 m_picType = PIC_line;}ID_DRAW_LINE的UPDATE_COMMAND_UI的Handler为OnUpdateDrawLine:void CHDrawDoc::OnUpdateDrawLine(CCmdUI* pCmdUI) { //如果当前画图类型为直线,设置菜单项前加对号,工具栏项下沉 pCmdUI->SetCheck(PIC_line == m_picType);}
    3.8 右键菜单修改选中图形的属性实现方法如下:第一步:在资源视图中增加一个菜单第二步:在CHDrawView中增加右键菜单响应函数OnRButtonDown:
    void CHDrawView::OnRButtonDown(UINT nFlags, CPoint point) { //检查所有处于选中状态的图形,可以有多个 CHDrawDoc *pDoc = GetDocument(); m_strokeSelected.RemoveAll();//首先清空旧数据 for(int i = 0; i < pDoc->m_strokeList.GetSize(); i ++){ if(pDoc->m_strokeList.GetAt(i)->IsHightLight()) m_strokeSelected.Add(pDoc->m_strokeList.GetAt(i)); } //显示右键菜单 CMenu rmenu; rmenu.LoadMenu(IDR_MENU_SET);//加载资源中的菜单IDR_MENU_SET ClientToScreen(&point);//需要坐标转换 rmenu.GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN, point.x, point.y, this); //因为这里的rmenu是局部变量,所以必须Detach掉 rmenu.Detach(); CView::OnRButtonDown(nFlags, point);}
    第三步:增加菜单响应函数,这里以删除当前所选图形为例:
    void CHDrawView::OnPicDelete() { //获取存储数据的文档类 CHDrawDoc *pDoc = GetDocument(); //移除所有处于选中状态的图形 int i = 0, j = 0; for(; i < m_strokeSelected.GetSize(); i ++){ //这里的j没有归0,是有原因的,可以很有效的提高效率 //遍历复杂度为两个数组的和 for(; j < pDoc->m_strokeList.GetSize(); j ++){ if(m_strokeSelected.GetAt(i) == pDoc->m_strokeList.GetAt(j)){ delete pDoc->m_strokeList.GetAt(j); pDoc->m_strokeList.RemoveAt(j); break; } } } //如果没有处于选中状态的图形,则不需要刷新。 if(i > 0) Invalidate();}
    3.9 撤销和恢复操作MFC提供了默认的撤销和恢复的ID,但是并没有提供默认实现,本程序的思路是,定义一个数组和一个数组索引,每执行一个操作,就把当前状态存储到数组中,并把数组索引加1。撤销时,把索引减一的数组元素恢复到当前文档,恢复时,把索引加一的数组元素恢复到当前文档。在程序中的步骤为:第一步:定义数组,数组索引和备份,恢复函数:
    CObArray m_backup; int m_backup_index; void ReStore(BOOL backward); void BackUp();void CHDrawDoc::BackUp(){ //备份操作,有利有弊。简单,节省内存,序列化有变时不需修改;产生文件占据磁盘 CString fileName; fileName.Format("hjz%d", m_backup.GetSize()); OnSaveDocument(fileName); //这里使用Insert而不是Add是因为恢复是并没有删除 m_backup.InsertAt(m_backup_index++, NULL, 1);}void CHDrawDoc::ReStore(BOOL backward){ m_backup_index -= backward ? 1 : -1;//撤销还是恢复 //…把数组元素恢复到当前文档 OnOpenDocument(m_backup.GetAt(m_backup_index-1));}
    第二步:添加撤销和恢复菜单项,并添加消息句柄:
    void CHDrawDoc::OnEditUndo() { ReStore(TRUE); UpdateAllViews(NULL);}void CHDrawDoc::OnEditRedo() { ReStore(FALSE); UpdateAllViews(NULL);}
    第三步:在每次对文档的修改操作之前,调用GetDocument()->Backup()
    3.10 使用鼠标拖拽选中多个图形
    首先自HStrokeRect类继承一个HStrokeSelect类,实现DrawStroke方法:
    void HStrokeSelect::DrawStroke(CDC *pDC){ m_penColor = RGB(255,0,0); m_penWidth = 1; m_penStyle = PS_DASH; HStrokeRect::DrawStroke(pDC);}
    然后在LButtonUp时选中区域内的图形,并将HStrokeSelect对象删除:
    //Step0.2 选择框 else if(PIC_select == m_stroke->m_picType){ bool refresh = false;//是否需要刷新 CRect rect(m_stroke->m_points.GetAt(0),m_stroke->m_points.GetAt(1)); for(int i = 0; i < pDoc->m_strokeList.GetSize(); i ++){ //是否在所框区域内 if(rect.PtInRect(pDoc->m_strokeList.GetAt(i)->m_points.GetAt(0)) && rect.PtInRect(pDoc->m_strokeList.GetAt(i)->m_points.GetAt(1))){ //设置选中状态 pDoc->m_strokeList.GetAt(i)->m_bSelected = true; refresh = true;//标志需要刷新 } } if(refresh) Invalidate();//刷新 delete m_stroke;//释放内存 }
    3.11 直线HStrokeLine的Tracker只显示两个Point
    CRectTracker在选中状态下会显示8个点,这对于矩形是合理的,而对于线段来讲,只要显示两个点就可以了,这里重载了CRectTracker类的Draw方法:
    void HStrokeTracker::Draw(CDC* pDC) const{ CRect rect; //一般图形用CRectTracker的方法即可 CRectTracker::Draw(pDC); //对于直线 if((m_picType == PIC_line) && ((m_nStyle&(resizeInside|resizeOutside))!=0)){ UINT mask = GetHandleMask(); for (int i = 0; i < 8; ++i) { if (mask & (1<<i)) { int p1, p2; //直线斜率小于0,即左上+右下 if(m_picExtra == 0) { p1 = 1, p2 = 4; } //直线斜率大于0,即左下+右上 else{ p1 = 2, p2 = 8; } if( ((1<<i) == p1) || ((1<<i) == p2)){ GetHandleRect((TrackerHit)i, &rect); pDC->FillSolidRect(rect, RGB(0, 0, 0)); } else{ GetHandleRect((TrackerHit)i, &rect); pDC->FillSolidRect(rect, RGB(255, 255, 255)); } } } }}
    3.12 键盘控制重载PreTranslate函数,响应Ctrl+A,Delete,Shift+(UP|DOWN|LEFT|RIGHT)键盘事件,实现全选,删除所选,控制所选多个图形移动功能。
    CHDrawDoc *pDoc = GetDocument(); BOOL deleted = FALSE; int i, x, y; if (pMsg->message == WM_KEYDOWN) { switch (pMsg->wParam){ //删除 case VK_DELETE: for(i = 0; i <pDoc->m_strokeList.GetSize(); i ++){ if(pDoc->m_strokeList.GetAt(i)->m_bSelected){ pDoc->m_strokeList.RemoveAt(i--); deleted = TRUE; } } if(deleted) Invalidate(); break; //全选 case 'A': case 'a': if(::GetKeyState(VK_CONTROL) < 0){ for(int i = 0; i <pDoc->m_strokeList.GetSize(); i ++){ pDoc->m_strokeList.GetAt(i)->m_bSelected = TRUE; } Invalidate(); } break; //移动 case VK_UP: case VK_DOWN: case VK_LEFT: case VK_RIGHT: x = (pMsg->wParam==VK_RIGHT) - (pMsg->wParam==VK_LEFT); y = (pMsg->wParam==VK_DOWN) - (pMsg->wParam==VK_UP); //Shift键加速移动 if(::GetKeyState(VK_SHIFT) < 0){ x *= 8; y *= 8; } for(int i = 0; i <pDoc->m_strokeList.GetSize(); i ++){ if(pDoc->m_strokeList.GetAt(i)->m_bSelected){ pDoc->m_strokeList.GetAt(i)->Move(x,y); } } Invalidate(); break; } }
    3.13 对话框控制
    HStrokeEditDlg对话框,实现对所有图形的矢量化编辑,可以直接修改图形的坐标,颜色,宽度,删除图形等操作。
    3.14 动画程序图标第一步:在资源中增加5个图标资源ICON:IDI_ICON1~IDI_ICON5第二步:在CMainFrame中增加变量HICON m_icons[5],并在构造函数中加载资源:
    m_icons[0] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON1)); m_icons[1] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON2)); m_icons[2] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON3)); m_icons[3] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON4)); m_icons[4] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON5));
    第三步:在CMainFrame的OnCreate函数中加载资源,并启动计数器:
    SetClassLong(m_hWnd, GCL_HICON, (LONG)m_icons[0]); SetTimer(WM_ICONALT, 1000, NULL); //设置计数器每秒
    第四步:在计数器函数OnTimer中增加修改图标的代码:
    static int iconIndex = 1; //静态变量计算第几个图标 if(nIDEvent == WM_ICONALT){ SetClassLong(m_hWnd, GCL_HICON, (LONG)m_icons[iconIndex]); iconIndex = (++iconIndex) % 5; } CFrameWnd::OnTimer(nIDEvent);
    3.15 LButtonDown流程如果Ctrl键没有被按下,遍历图形数组,如果有Track的图形,执行Track操作
    Step1:If Ctrl键没有被按下For 每个图形 If 该图形被选中If Track 移动图形 标记b_Track为真,表示当前是Track而不是绘图EndIf EndIf EndForEndIfStep2:For每个图形,If当前鼠标点在其范围内 If Ctrl键被按下 该图形的选中状态取反 Else 没有按下Ctrl键 选中当前图形 EndIfElse 当前鼠标点不再其范围内 If Ctrl键被按下 无操作,认为是用户多选时的误操作Else Ctrl键没有被按下取消该图形的选中状态 EndIf EndIfEnd ForStep3:If b_Track为假,表示当前是绘图而不是TrackStep3.1. 设置捕获鼠标SetCapture();Step3.2. 加入新图形m_stroke = pDoc->NewStroke(); Step3.3. 设置起点m_stroke->SetCurrentPoint(point); Step4. 设置文件已经修改状态EndIf3.16 LButtonUp流程If 用户点下鼠标后就松开,等于没有画 删除指针,释放内存ElseIf 画图类型为选择框 For 每个图形 If 该图形在选择框内 设置状态为选中 EndIf EndFor 删除指针,释放内存Else 画图 设置当前鼠标坐标 加入图形 备份EndIf3.17 MouseMove流程If 当前处于捕获状态(参考LButtonDown) 重画图形Else For 每个图形 If 当前鼠标坐标在该图形内 设置该图形高亮状态为真 Else 设置该图形高亮状态为假 EndIf EndForEndIf4. 总结4.1 Tricks4.1.1 子View和父View公用一个Doc本程序使用了两个View,CHDrawPView中创建了CHDrawView,而为了在CHDrawView中使用CHDrawDoc对象,需要将CHDrawPView的Doc传给CHDrawView。这个需求只要为CHDrawView增加一个方法,将m_pDocument指针传过去即可。
    但是这样就引来另外一个问题,在CView析构的时候,它会去析构m_pDocument,这样CHDrawDoc就会被CHDrawView和CHDrawPView一共析构两次,出现错误,为了解决这个问题,在传递m_pDocument的时候,用另外一个CDocument指针储存CHDrawView的m_pDocument,然后在CHDrawView析构函数中,再将m_pDocument修改回去。
    4.1.2 在类中获取其它类的句柄在CMainframe中:
    SDI中获取View ((CMainFrame*)AfxGetApp()->m_hMainWnd)->GetActiveView();SDI中获取View CMainFrame::GetActiveView()SDI中获取Doc((CMainFrame*)AfxGetApp()->m_hMainWnd)->GetActiveDocument();MDI中获取ViewAfxGetApp()->m_pMainWnd)->GetActiveFrame()->GetActiveView();MDI中获取DocAfxGetApp()->m_pMainWnd)->GetActiveFrame()->GetActiveDocument();MDI中获取View GetActiveFrame()->GetActiveView()MDI中获取View MDIGetActive()->GetActiveView()在CxxxDoc类中:MDI中获取View GetFirstViewPosition();MDI中获取View GetNextView()在CxxxView类中:SDI中获取Frame getMainFrame AfxGetApp()->m_hMainWnd;SDI中获取Frame getMainFrame CWnd::GetParentFrame()SDI中获取Frame getMainFrame AfxGetMainWnd()SDI中获取Doc GetDocument()MDI中获取Doc GetDocument();4.1.3 CRectTracker用法CRectTracker是MFC提供的一个很好用的类,简单易用。使用步骤:第一步:声明CRectTracker变量
    CRectTracker m_tracker;
    第二步:初始化CRectTracker的区域m_rect和样式m_style
    m_tracker.m_rect.SetRect(0,0,GetDocument()->m_cavasW, GetDocument()->m_cavasH); m_tracker.m_nStyle=CRectTracker::resizeOutside;
    第三步:override OnSetCursor方法:
    CPoint point; //Step1. get cursor position GetCursorPos(&point); //Step2. convert point from screen to client ScreenToClient(&point); if(m_tracker.HitTest(point) >= 0){ //Step3. set cursor, **notice, use nHitTest instead of return of tracker m_tracker.SetCursor(pWnd, nHitTest); …
    第四步:在OnLButtonDown函数中调用HitTest检测并用Track函数跟踪
    int hit = m_tracker.HitTest(point); switch(hit){ case 2: case 5: case 6: if(m_tracker.Track(this,point)){ //step1. cavas reset GetDocument()->m_cavasH = m_tracker.m_rect.bottom; GetDocument()->m_cavasW = m_tracker.m_rect.right; //step2. scroll or not CRect clientRect; GetClientRect(&clientRect); SetScrollSizes(MM_TEXT, CSize(m_tracker.m_rect.Width()+10, m_tracker.m_rect.Height()+10)); m_drawView->MoveWindow(m_tracker.m_rect.left, m_tracker.m_rect.top, m_tracker.m_rect.right,m_tracker.m_rect.bottom); GetDocument()->BackUp();//备份 Invalidate(); } }
    使用时容易出现的问题:如果在调用CRectTracker的Track方法之前调用了SetCapture函数,会发现Track方法失效。因为SetCapture方法会捕获鼠标事件,而Track则需要独立处理鼠标事件,两个函数争夺鼠标活动的处理权,并以Track的失败告终。
    3.1.4 内存泄露内存泄露问题发生的概率非常高,MFC的Debug功能对内存泄露的检测虽然算不上完美,但是基本够用了,使用F5启动调试,然后尽可能多的执行操作,关闭后在Debug窗口显示调试结构,如果有内存泄露,则会出现以下类型的信息:
    Detected memory leaks!Dumping objects ->afxtempl.h(370) : {1208} normal block at 0x00376880, 40 bytes long. Data: < . > BE 00 00 00 2E 00 00 00 AC 00 00 00 A1 00 00 00 E:\code\less01\HDraw\HDrawDoc.cpp(131) : {1202} client block at 0x00376770, subtype 0, 48 bytes long.a HStrokeLine object at $00376770, 48 bytes longafxtempl.h(370) : {960} normal block at 0x00376708, 40 bytes long. Data: < ~ > 92 00 00 00 1E 00 00 00 7E 00 00 00 A8 00 00 00 E:\code\less01\HDraw\HDrawDoc.cpp(131) : {954} client block at 0x003765B0, subtype 0, 48 bytes long.a HStrokeLine object at $003765B0, 48 bytes longafxtempl.h(370) : {723} normal block at 0x00376548, 40 bytes long. Data: <Q [ w > 51 00 00 00 5B 00 00 00 07 01 00 00 77 00 00 00 E:\code\less01\HDraw\HDrawDoc.cpp(131) : {717} client block at 0x00377768, subtype 0, 48 bytes long.a HStrokeLine object at $00377768, 48 bytes longafxtempl.h(370) : {422} normal block at 0x00377910, 40 bytes long. Data: << # Y > 3C 00 00 00 23 00 00 00 E4 00 00 00 59 00 00 00 E:\code\less01\HDraw\HDrawDoc.cpp(131) : {419} client block at 0x00377800, subtype 0, 48 bytes long.a HStrokeLine object at $00377800, 48 bytes longObject dump complete.
    双击相应的信息就能定位到未释放内存的申请地址,然后考虑应该在什么地方释放。
    0 评论 2 下载 2019-07-15 21:47:35 下载需要10点积分
  • 基于Python和opencv实现抖音上墨镜和烟卷效果

    一、项目简介现今较火的抖音上有一个十分有趣的特效,其可以自动检测出人脸并且放置墨镜和烟卷,鉴于此,想自己实现动手实现以下该特效的制作。
    二、工作环境Python 3.6,opencv+Dlib,Windows操作系统,pycharm
    三、流程
    从摄像头获取视频流,并转换为一帧一帧的图像,然后将图像信息传递给opencv这个工具库处理,返回灰度图像。
    程序启动后,根据监听器信息,使用一个while循环,不断的加载视频图像,然后返回给opencv工具呈现图像信息。
    创建一个键盘事件监听,按下”d”键,则开始执行面部匹配,并进行面具加载(这个过程是动态的,你可以随时移动)。
    面部匹配使用Dlib中的人脸检测算法来查看是否有人脸存在。如果有,它将为每个人脸创建一个结束位置,眼镜和烟卷会移动到那里结束。
    然后我们需要缩放和旋转我们的眼镜以适合每个人的脸。我们将使用从Dlib的68点模型返回的点集来找到眼睛和嘴巴的中心,并为它们之间的空间旋转。
    在我们实时获取眼镜和烟卷的最终位置后,眼镜和烟卷从屏幕顶部进入,开始匹配你的眼镜和嘴巴
    假如没有人脸,程序会直接返回你的视频信息,不会有面具移动的效果。默认一个周期是4秒钟。然后你可以通过”d”键再次检测。
    退出程序按下q键。

    四、关键代码
    创建面具加载服务类DynamicStreamMaskService及其对应的初始化属性:
    读取摄像头视频流并转化为图象:
    实现人脸定位函数,及眼镜和烟卷的定位:
    接下来实现画图函数:
    创建启动函数start:

    五、运行结果运行后,首先会打开摄像头,然后按下键盘d键,就会看到墨镜和烟卷从屏幕上方出来,自动定位到眼睛和嘴巴。参考结果如下:图片经过马赛克处理了。
    注意代码中需要调用shape_predictor_68_face_landmarks.dat人脸库,需要下载,然后在代码调用处中指定路径即可。
    0 评论 4 下载 2019-07-11 14:00:18 下载需要15点积分
  • 基于移动网络通讯行为的用户风险识别

    一、实验内容参加“基于移动网络通讯行为的用户风险识别”大赛,本次大赛以模拟的语音通话、短信收发、网站及App访问记录等移动网络使用行为为基础,参赛队伍需要通过数据挖掘技术和机器学习算法,构建识别风险用户的预测模型,判别用户属于风险用户的可能性。从而为各行业提供风控保障,助力新时代大数字生态的健康有序发展。
    二、实验主要思想与方法本实验主要根据用户的通话记录、短信记录、网站和APP访问记录的信息,对用户进行分类和预测。在实验方法介绍分为三部分,第一部分为数据集训练与可视化,第二部分介绍特征提取的主要策略,在第三部分将介绍此次实验使用的LightGBM模型。
    2.1 数据集训练与可视化2.1.1 用户通话记录用户通话记录数据

    通话号码长度分布

    不同通话类型所占比例

    打入和打出电话所占比例

    打入和打出电话不同长度出现次数

    打入和打出电话号码不同头部出现次数

    每日活跃次数

    打入和打出情况下每天的活跃次数

    5种不同类型下每天的活跃次数

    通话号码不同长度出现次数

    已给风险用户的通话号码长度出现次数

    通话号码头部出现次数(仅截取主要部分)

    已给风险用户通话号码头部出现次数(仅截取主要部分)

    2.1.2 用户短信记录用户短信记录数据

    短信号码长度分布

    短信号码不同头部出现次数

    已给风险用户的短信号码头部出现次数

    短信号码不同长度出现次数

    已给风险用户的短信号码长度出现次数

    接收/发送短信情况下不同短信头部出现次数

    接收/发送短信情况下不同短信长度出现次数

    2.1.3 用户网站/App访问记录用户网站/App访问记录数据

    用户访问网站/APP分别占的比例

    两种类型每日活跃次数

    2.2 特征提取经过对可视化结果的分析,对特征进行如下处理:

    通话记录:

    voice_time:将end_time、start_time转为时间格式并计算通话时长(end_time-start_time);voice_date:取start_time的前两位
    短信记录:sms_date:取start_time的前两位。
    对已给特征使用常规的提取方法,如取:sum、mean、min、max、median等。
    将提取的特征与in_out、call_type、wa_type等进行结合。
    多变量进行结合,如:
    voice_feature[‘voice_cnt_peruniquecall’]= voice_feature[‘voice_opp_num_unique’]/voice_feature[‘voice_date_cnt’],求每天有通话的号码数量等。

    经过以上特征处理,最终得到如下特征:

    feature,importancesms_opp_head_max_in,92voice_len_mean_in,70sms_opp_len_mean_in,57voice_cnt_peruniquecall,56wa_type1_date_mean,54wa_dura_sum_percnt,50wa_type0_namecnt_perday,50sms_opp_head_min_in,49voice_head_mean_in,47sms_opp_head_mean_in,41sms_opp_len_mean,39voice_opp_head_min,39voice_headinunique_perday,37voice_head_mean_type1,36wa_type0_dura_perday,36wa_down_flow_median,35voice_time_median,35voice_date_mean_type1,34voice_timesum_peruniquecall,34voice_head_mean_out,33sms_start_time_mean,33voice_time_min,33voice_start_mean_in,33wa_type1_dura_perday,33voice_head_max_type1,32wa_namecnt_perday,32sms_callunique_perday,31voice_len_max_in,31voice_start_mean_out,30sms_calluniquein_mean_perday,30sms_calluniquein_perday,30sms_len_perday,30voice_headoutunique_perday,29voice_time_mean_out,29voice_len_mean_type1,29wa_type1_date_cnt,29wa_up_flow_median,28wa_type1_namecnt_perday,28voice_head_unique_in,28wa_type0_namedura_perday,28wa_type0_date_mean,27voice_headout_perday,27voice_timeoutmean_percall,26voice_headin_perday,26voice_opp_head_mean,26sms_callcnt_perday,26wa_visit_date_cnt,26wa_type1_namevicnt_perday,26wa_type0_dura_mean,25voice_opp_head_max,25voice_date_cnt_in,25wa_type1_namedura_perday,25wa_name_count_uinque,24wa_type0_visitcnt_mean,24voice_time_max,24sms_opp_num_count_in,23wa_type0_date_cnt,23voice_headmean_peruniquecall,23wa_type0_namevicnt_perday,23wa_type1_namevicntall_perday,23voice_time1mean_percall,23wa_visit_cnt_mean,23wa_type1_dura_mean,23sms_opp_head_max,22voice_opp_len_max,22wa_visit_dura_median,22sms_head_unique_perday,22voice_cntoutsum_perday,22voice_opp_unique_in,22wa_down_flow_mean,22wa_type1_downflow_mean,22sms_start_time_mean_in,21voice_time_mean_type1,21wa_visitdura_perday,21wa_up_flow_std,21voice_time_mean_in,21sms_opp_len_sum,21wa_namedura_perday,20sms_start_time_mean_out,20wa_type0_up_persec,20wa_type1_visitcnt_all,20wa_type0_visitcnt_perday,20voice_timeoutunique_percall,20voice_len_mean_out,20wa_up_flow_max,20wa_type0_flow_mean,19voice_cntoutmean_perday,19voice_timeoutsum_perday,19voice_head_peruniquecall,19wa_type1_upflow_mean,19sms_calluniquemeanout_perday,19sms_calluniquemeanin_perday,19wa_type1_visitcnt_perday,19voice_time_std,19wa_visitcnt_perday,19wa_type1_down_percnt,19wa_flow_sum_perday,19sms_opp_head_min,19wa_type0_downflow_all,18voice_cntinsum_perday,18wa_type1_flow_mean,18sms_calluniqueout_mean_perday,18voice_numcnt3mean_perday,18wa_type1_name_unique,18sms_opp_head_mean_out,17wa_down_flow_std,17voice_timemean_peruniquecall,17voice_lenmean_peruniquecall,17voice_cnt_perday,17wa_type0_name_unique,17sms_opp_head_mean,17voice_timeout_percall,16voice_opp_unique_out,16wa_type0_down_perday,16wa_type0_namevicntall_perday,16wa_flow,16sms_date_cnt_in,16wa_upflowcnt_perday,16voice_timeinunique_percall,16sms_opp_len_max,15wa_up_flow_mean,15wa_visit_dura_std,15sms_date_cnt,15wa_visit_dura_mean,15voice_numcnt1mean_perday,15voice_opp_len_mean,15voice_date_mean_type2,15voice_len_max_out,15wa_namevicnt_perday,15sms_date_mean_in,14voice_lensum_peruniquecall,14voice_timeoutsum_percall,14voice_time_sum_out,14wa_upflow_perday,14voice_timeoutmean_perday,14voice_time_mean_type3,14wa_type1_visitcnt_mean,14wa_type0_upflow_mean,14voice_time_sum_type3,13wa_downflowcnt_perday,13wa_type1_up_perday,13wa_flow_sum_percnt,13sms_date_cnt_out,13wa_down_flow_max,13voice_timemean_percall,13sms_calluniquecntin_perday,13voice_timein_percall,13voice_date_mean_out,12voice_time_sum_in,12voice_date_count_type1,12voice_time_sum_type1,12voice_date_count_type3,12wa_type1_up_persec,12voice_head_mean_type2,12voice_time1_percall,12voice_time2_percall,12voice_time3_percall,12voice_len_peruniquecall,12wa_type0_upflow_all,12wa_flow_sum_persecond,12wa_type0_down_percnt,12voice_opp_len_unique,11wa_type0_visitcnt_all,11wa_type0_up_perday,11sms_calluniqueout_perday,11voice_time3mean_percall,11wa_down_flow_sum,11voice_timeinmean_percall,11wa_visit_cnt_std,11voice_date_mean_type3,11voice_date_cnt_out,10wa_up_flow_sum,10wa_upflowdura_persec,10voice_time2mean_percall,10voice_cntinmean_perday,10voice_head_mean_type3,10sms_opp_head_min_out,10wa_visit_cnt_median,10wa_visit_cnt_sum,10voice_timeinsum_perday,10wa_type0_downflow_mean,9wa_type1_down_perday,9sms_opp_num_unique_in,9wa_visit_dura_sum,9sms_opp_num_count_unique,8voice_opp_count_all,8sms_calluniquecntout_perday,8voice_date_count_type2,8sms_date_mean_out,8voice_date_cnt,8voice_time_mean_type2,8voice_time_mean,8voice_opp_num_unique,8voice_numcnt1_perday,8voice_numcnt2mean_perday,8sms_opp_num_count_out,8wa_visit_cnt_max,8wa_type1_upflow_all,7voice_opp_count_out,7voice_numcnt2_perday,7wa_downflowdura_persec,7voice_headsum_peruniquecall,7wa_visit_dura_max,7voice_timeinsum_percall,7voice_date_mean_in,6wa_downflow_perday,6sms_opp_len_mean_out,6voice_time_sum,6sms_opp_num_count_all,6voice_time_percall,6voice_head_unique_out,6voice_num_cnt_type1,6voice_num_cnt_type3,6voice_opp_head_unique,5sms_opp_num_unique_out,5wa_type1_downflow_all,4voice_len_sum_out,4sms_opp_head_max_out,4voice_numcnt3_perday,4sms_opp_head_unique,3voice_timeinmean_perday,3voice_head_max_type3,3voice_time_sum_type2,2voice_head_max_type2,2voice_len_sum_in,2voice_opp_count_in,2voice_num_cnt_type2,0

    在特征工程中,删除了一些低分的特征,最终确定维数为228。
    2.3 LightGBMLightGBM与其他流行算法采用depth-wise的叶子生长策略不同,其使用的是带深度限制的leaf-wise的叶子生长策略。与depth-wise相比,leaf-wise算法收敛得更快。但leaf-wise的一个缺点就是:参数选择不当可能会产生比较深的决策树从而产生过拟合。因此LightGBM在Leaf-wise上增加了一个最大深度限制,在保证高效率的同时防止过拟合。
    在调参方面,由于LightGBM使用的是leaf-wise算法,因此调节使用的是num_leaves控制模型复杂度,大致设置为num_leaves=2^max_depth。另外,min_data_in_leaf参数用于处理过拟合的问题,设置大可以防止过拟合,但需要注意可能会导致欠拟合。
    由于特征过多,必须要考虑多过拟合的处理,所以设置较小的num_leavea和max_depth以避免过深的树,同时要设置合适的min_data_in_leaf避免过拟合或欠拟合。
    以下为参数设置:
    lgb_params = { 'boosting_type': 'gbdt', 'objective': 'binary', 'is_training_metric': False, 'learning_rate': 0.08, 'num_leaves':16, 'max_depth':6, 'min_data_in_leaf':40, 'verbosity':-1, 'is_training_metric':False, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq':1, 'is_unbalance':True}三、实验结果通过特征提取和调整模型参数,提交结果前最终线下测试结果为0.809,线上测试结果为0.760,最终排名142/211。
    比赛结果截图:

    四、心得体会本次实训对我来说实际上是一次很大的考验。因为先前既没有python的基础,也没有上过数据挖掘理论课,所以一开始对相关的理论概念一无所知。
    在这样的情况下,我从网上的教程一步步开始学习,包括学习python的网站以及在同学推荐下看数据挖掘和机器学习的网课,慢慢摸索。尽管这样,在初赛阶段,写出来的代码和测试结果仍然惨不忍睹(线上测试分数只有0.39)。
    后来,在同学的帮助下(包括模型讲解和编码的指导)和参考了助教提供的baseline,逐渐把分数提高,最终有这样的成绩。
    可以说现在我依然对数据挖掘相关的理论知识一知半解,仅仅停留在照着葫芦画瓢的阶段。但至少在这个过程中我逐渐熟悉了python的使用,以及一些很实用的python库如numpy、pandas等,还算是有些收获。
    0 评论 0 下载 2019-06-25 18:08:34
  • 基于JSP的校园论坛BBS网站的设计与实现

    1 概述开发校园论坛系统的目的是提供一个供我校学生交流的平台,为我校学生提供交流经验、探讨问题的社区。因此, 校园论坛系统最基本的功能首先是发表主题,其次是其他人员根据主题发表自己的看法。此外,为了记录主题的发表者和主题的回复者信息,系统还需要提供用户注册和登录的功能。只有注册的用户登录后才能够发表和回复主题,浏览者(游客)只能浏览主题信息。根据用户的需求及以上的分析,校园论坛需要具备前台功能和后台功能。
    1.1 系统概述
    网站名称
    校园论坛
    网站功能实现
    为用户提供一个注册、发帖、回复、浏览等交流操作功能。
    用户
    在校学生
    子系统关系图


    1.2 系统目标为了方便校内同学的交流,我们决定要做这么一个校园论坛,而对于论坛这样的数据流量特别大的网络管理系统,必须要满足使用方便、操作灵活等设计需求。所以本系统在设计时应满足以下几个目标:

    临时用户进入,可浏览帖子但不可发帖一个版面能显示所有的帖子具有登录模块,有用户的个人信息用户随时都可以查看自己发表的帖子有用户的消息的时间及时提醒,主要用于提示哪个用户 回复了自己的主题管理员权限可删除任意帖子,具有最大权限的管理功能对用户输入的数据,系统进行严格的数据检验,尽可能 排除人为的错误系统最大限度地实现了易维护性和易操作性系统运行稳定安全可靠
    1.3 文档概述需求分析报告采用面向对象的方法,在文档中主要采用了用例、流程图等表示方法来描述需求。
    1.4 需求概述1.4.1 用户需求对于一个用户,使用论坛进行交流时,首先要注册一个 账户,然后登录后才能进行对帖子的回复,如果不登录,就 只能查看帖子而不能进行回复和发表帖子。用户使用论坛系 统的需求是发表某一个主题相关的帖子,用户在发表帖子后, 如果有人进行回复,就要在首页提醒用户有新消息。用户可以删除自己发表的帖子和评论。对于论坛管理人员来说,需要完成对用户发表的帖子的管理,包括:设置精华帖、置顶 帖子、删除帖子等操作。
    开发校园论坛系统的目的是提供一个供我校学生交流的平台,为我校学生提供交流经验、探讨问题的社区。因此, 校园论坛系统最基本的功能首先是发表主题,其次是其他人员根据主题发表自己的看法。此外,为了记录主题的发表者和主题的回复者信息,系统还需要提供用户注册和登录的功能。只有注册的用户登录后才能够发表和回复主题,浏览者(游客)只能浏览主题信息。根据用户的需求及以上的分析, 校园论坛需要具备前台功能和后台功能。

    系统前台功能:显示用户发表的帖子,查看帖子的内容、发表对帖子的回复、发表对回复的回复、显示用户的头像、用户信息的显示、用户新信息的提醒。系统后台功能:进入后台、帖子管理、用户管理、添加删除用户、系统设置、退出系统、返回首页。
    1.4.2 系统开发环境需求1.4.2.1 开发环境我们一致认为在开发此论坛的时候需要配置以下软件环境:
    服务器端:

    操作系统:Windows 7及以上Web服务器:Tomcat 7.0 集成开发环境:Eclipse 数据库:MySQL
    客户端:

    无特别要求
    1.4.2.2 使用技术
    前端:HTML、CSS、JS、BootStrap后端:Java数据库:MySQL
    1.4.2.3 用户的特点
    本网站的最终用户的特点:所有上网用户在无需培训的情况下,按照网站页面提示即可使用网站的相关服务和功能后台管理和维护人员的教育水平和技术专长:本软件的后台管理和维护人员均为我小组开发成员
    1.5 功能需求经过系统性的研究,我们认为整个论坛大概分为 3 个功能模块,分别为:论坛模块、管理员模块和用户模块。
    1.5.1 前台功能需求在论坛前台中,我们作了如下设计:分未登录前和登录后,登录前,用户进入校园论坛的主页面后,可以查看帖子内容、用户注册、用户登录,登录后,用户可以修改个人信息、查看个新消息、修改头像、查看帖子内容、回复帖子。
    1.5.2 后台功能需求管理用户进入后台后,可以进行帖子管理,包括查看帖子、删除帖子、返回论坛首页和退出系统。
    1.5.3 系统前台流程图 在系统前台流程中,我们做出了如下设置。首先,我们开始点开界面,是我们的论坛主页,不登录可以以临时用户身份浏览,登陆则可以发帖和评论,没有账号的可以注册。

    1.5.4 系统后台流程图在系统的后台流程中,我们做出了如下设置。首先,我们进入登录界面,使用管理员账号和密码进行登录,在管理员界面,我们可以进行用户信息管理,可以查看、删除用户帖子

    1.6 系统用例图
    1.7 系统时序图论坛管理员处理帖子的时序图

    用户发帖评论时序图

    1.8 系统组件图
    1.9 系统E-R图
    1.10 系统配置图
    2 操作指引2.1 项目简介校园论坛所具有的功能包括:用户注册、用户登录、用户信息修改、浏览帖子、发表帖子、收藏帖子、搜索帖子、回复帖子、用户信息管理(查询、增加、删除、修改)。
    从整体上可以分为数据层、数据访问层和业务逻辑层。数据层是系统最底层,它用于存储系统的所有数据。数据访问层建立在数据库之上,应用程序通过该层访问数据库。数据访问层一般封装数据库的选择、添加、更新和删除操作,同时还为业务逻辑层服务,所以数据访问层的设计的好坏关系到整个系统的成败。业务逻辑层包括用户登录、用户注册、 发表帖子等业务逻辑,它一般由Web页面实现。
    系统操作结构

    页面操作结构

    2.2 操作介绍在登录注册界面可以通过登录和注册按钮进行登录和注册操作


    登录完就会进入主界面,在主界面上方有“个人信息”,“我的帖子”、“用户管理”等按钮可以进行相应的操作。界面中间是其他用户发的帖子,可以点击进行浏览和恢复等操作。界面的最下方是发帖模块,只用登录用户才可以进行发 帖操作,游客只有浏览帖子的权限。

    点击个人信息按钮进入个人信息界面可以修改个人的详细信息。

    点击我的帖子进入我的帖子界面可以对自己发的帖子进行删除和查看操作。

    在首页点击其他用户的帖子可以进入帖子的完整内容进行浏览,还可以在最下方的回复模块进行回复。

    如果你是以管理员的身份登录,你还可以进入用户管理模块,进行删除帖子的操作。

    3 业务说明3.1 业务简介本软件系统旨在通过网络论坛,让在校大学生快速地进行交流更为便捷。使得大学生的交流环境和校方教育及管理人员获得广大学生声音更加方便也更为人性化。校园论坛是面向各大高校师生的一个信息交流平台,建立一个基于师生沟通为目的,功能简洁,方便实用和管理的论坛系统显得越来越必要。为达成这一目标,并将大学学习期间所学的数据库设计与实现、网页设计、面向对象程序设计、Web 应用开发等知识综合运用,融会贯通,特开发此项目。
    3.2 业务场景


    触发事件
    用户注册




    执行者
    用户


    工作内容
    1.进行用户的信息注册






    触发事件
    用户登录




    执行者
    用户


    工作内容
    1.用户使用已注册的账号和密码进行登录






    触发事件
    查看已发布的帖子




    执行者
    用户/游客


    工作内容
    1. 对已发布的帖子进行查看






    触发事件
    发帖




    执行者
    用户


    工作内容
    1.用户进行帖子发布






    触发事件
    回帖




    执行者
    用户


    工作内容
    1.用户对已发布的帖子内容进行回复






    触发事件
    论坛出现违规帖子




    执行者
    网站管理员


    工作内容
    1.对违规帖子进行查看,评估 2.对存在违规现象的帖子进行删除,当情况严重时还需要对违规用户进行禁言或封号处理



    4 数据库数据流图,简称 DFD,是 SA 方法中用于表示系统逻辑模型的一种工具,它以图形的方式描绘数据在系统中流动和处理的过程,由于它只反映系统必须完成的逻辑功能,所以它 是一种功能模型。
    4.1 顶层数据流图
    4.2 0 层数据流图
    4.3 具体数据流图4.3.1 登录系统
    4.3.2 注册系统
    4.3.3 发表主题
    4.3.4 回复主题
    4.3.5 论坛管理
    4.4 数据字典4.4.1 数据流




    4.4.2 数据项




    4.5 E-R图E-R 图即实体-联系图(Entity Relationship Diagram),是指提供了表示实体型、属性和联系的方法,用来描述现实世界的概念模型 。 E-R方法是 “实体-联系方法 ”(Entity-Relationship Approach)的简称,它是描述现实世界概念结构模型的有效方法。

    4.6 数据库设计4.6.1 数据库分析数据库的设计,在程序的开发中起着至关重要的作用,它往往决定了在后面的开发中进行怎样的程序编码。一个合理、有限的数据库设计可降低程序的复杂性,使程序开发的过程更为容易。
    本系统是一个中型的用户交流的网站,考虑到用户访问量以及网络连接,本系统采用MySQL 作为数据库。
    MySQL 是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。MySQL 的 SQL 语言是用于访问数据库的最常用标准化语言。
    4.6.2 数据库的逻辑设计根据系统需求,我们就可以创建系统所需要的数据库表了。本系统包含 3 个表,下面是这些表的结构。
    user_info 表结构如表所示:



    字段名
    数据类型
    字段长度
    是否主键
    是否为空
    备注




    user_id
    int
    15


    用户 id


    user_name
    varchar
    50


    用户名


    user_password
    varchar
    50


    密码


    user_sex
    varchar
    2


    性别


    user_face
    varchar
    255


    头像


    user_phone
    varchar
    255


    联系电话


    user_email
    varchar
    200


    电子邮箱


    user_from
    varchar
    200


    来自何处


    isAdmin
    int
    2


    是否为管理员



    forum_info 表结构如表所示:



    字段名
    数据类型
    字段长度
    是否主键
    是否为空
    备注




    Fid
    int
    10


    发帖 id


    Title
    varchar
    255


    帖子标题


    content
    varchar
    255


    帖子内容


    create_time
    datetime



    发帖时间


    user_id
    int
    11


    用户 id



    reply_info 表结构如表所示:



    字段名
    数据类型
    字段长度
    是否主键
    是否为空
    备注




    reply_id
    int
    10


    回帖 id


    reply_content
    varchar
    255


    回帖内容


    reply_time
    datetime



    回帖时间


    user_id
    int
    11


    用户 id


    fid
    int
    11


    发帖 id



    4.6.3 SQL 语句设计(建表语句 )用户信息表(user_info)
    CREATE TABLE `user_info` ( `user_id` int(15) NOT NULL, `user_name` varchar(50) NOT NULL, `user_password` varchar(50) NOT NULL, `user_sex` varchar(2) NOT NULL, `user_face` varchar(255) NOT NULL, `user_phone` varchar(255) NOT NULL, `user_email` varchar(200) NOT NULL, `user_from` varchar(200) NOT NULL, `isAdmin` int(2) DEFAULT NULL, PRIMARY KEY (`user_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8
    主题信息表(forum_info)
    CREATE TABLE `forum_info` ( `fid` int(10) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `content` varchar(255) NOT NULL, `create_time` datetime NOT NULL, `user_id` int(11) NOT NULL, PRIMARY KEY (`fid`)) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8
    回帖信息表(reply_info)
    CREATE TABLE `reply_info` ( `reply_id` int(10) NOT NULL AUTO_INCREMENT, `reply_content` varchar(255) NOT NULL, `reply_time` datetime NOT NULL, `user_id` int(11) NOT NULL, `fid` int(11) NOT NULL, PRIMARY KEY (`reply_id`)) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8
    4.7 MD5 算法加密实现代码
    public class MD5 { public static String generateCode(String str) throws Exception{ MessageDigest md5=MessageDigest.getInstance("MD5"); byte[] srcBytes=str.getBytes(); md5.update(srcBytes); byte[] resultBytes=md5.digest(); String result=new String(resultBytes); return result; }}
    实现结果

    5 系统测试
    测试环境
    操作系统:windows10
    浏览器:Google Chrome

    5.1 测试内容


    用户注册
    输入学号、密码及个人信息,并且通过验证码认证后,完成注册





    用户登录
    输入学号及密码完成登录



    浏览帖子
    不论是否登录,都可以正常浏览已有的帖子



    发帖
    只有登录后,方可发帖



    回复
    只有登录后,方可回复



    个人信息
    查看个人信息(头像、学号、姓名、性别 联系电话、电子邮箱、来自哪里)



    修改个人信息
    对个人信息(头像、学号、姓名、性别、联系电话、电子邮箱、来自哪里)进行修改



    退出登录
    退出已登录状态




    5.2 功能截图校园论坛主页

    在校园论坛主页,可作为游客身份浏览帖子,但只有注册、 登录之后,方可回复跟帖。主页提供直观的注册、登录按钮。
    用户注册

    输入学号、用户名、密码,以及其它个人信息之后,即可完成注册。并且为了网站安全,用户需要通过验证码验证。
    用户登录
    输入学号、密码即可完成登录。

    若勾选“记住密码”,会自动填充学号及密码,方便用户快捷登录校园论坛。
    查看帖子

    登录之后,进入“查看帖子”页面,可浏览已发布的帖子。右上角提供“个人信息”、“ 我的帖子”、“退出论坛”三个按钮。
    发帖

    “查看帖子”页面底部,输入标题及内容,点击”发表”,即可发布自己的帖子。
    阅读、回复帖子

    点击帖子的标题,即可阅读帖子详情,可以回复跟帖。
    个人信息

    点击右上角的“个人信息”按钮,即可查看个人信息,包 括头像、学号、姓名、性别、联系电话、电子邮箱,及来自哪里。
    修改个人信息

    “个人信息”页面,点击“修改资料”按钮,即可进入“修改个人信息”页面。可对个人信息进行修改。
    退出登录

    点击右上角的“退出论坛”按钮,即退出登录状态,回到论坛主页。
    5 评论 175 下载 2018-10-05 22:38:31 下载需要16点积分
  • Palette-based Photo Recoloring论文算法重现

    一、使用方法直接打开index.html从相册中选择合适图片上传,程序将自动计算调色板。待计算完成,可点击调色板编辑变换的目标颜色。编辑完后,点击CONFIRM按钮开始重着色。
    二、算法实现2.1 调色板计算
    将RGB颜色空间均匀分成$16\times 16\times 16$个bins,统计图像中属于各个bin的颜色个数。每个bin的RGB空间均值为其代表色 (注: 原文为Lab空间均值,但求该均值较复杂,故使用了差别不大的RGB均值)。
    对bin进行聚类,使用的方法是改进的K-means算法。
    在聚类前,规定黑色为调色板颜色之一,这样可避免生成的调色板中有很多暗色。
    将聚类得到的调色板颜色按照Lab亮度升序排序。

    2.2 重着色
    单个的颜色变换可以看成颜色在L通道与a、b通道单独变换的组合。
    L通道的变换由对调色板亮度线性插值得到。
    ab通道的变换(左下图)由颜色在Lab空间内平移得到。其中对于超出Lab边界的情况做了特殊处理
    一组颜色变换可以看做是若干个单独颜色变换的组合(右下图),文章给出了使用径向基函数分配权重的方法:

    $$f(x)=\sum_i^k \omega _i(x) f_i(x)$$ $$\omega _i(x)=\sum_j^k\lambda_{ij}\phi(\parallel x-C_j\parallel)$$ $$\phi(r)=e^{-\frac{r^2}{2\sigma_r^2}}$$ <div style="text-align:center"> <img src="img/report/p1.png" height=200>   <img src="img/report/p1.png" height=200> </div>
    三、主要函数及说明


    函数
    位置
    说明




    Color.lab2rgb(InputColor)
    color.js
    将RGB颜色转换为Lab颜色($RGB\to XYZ\to Lab$)


    Color.rgb2lab(InputColor)
    color.js
    将Lab颜色转换为RGB颜色


    Color.labBoundary(pin, pout)
    color.js
    在Lab空间内,求颜色pin与pout连线与Lab边界的交点(二分查找)


    Palette.palette()
    palette.js
    统计图像颜色属于各个bin的颜色个数


    Palette.kmeansFirst()
    palette.js
    聚类,由统计好的bins数据得到调色板


    Palette.kmeans()
    palette.js
    调整上一步聚类得到的颜色,直到调色板不再变化


    Palette.colorTransformSingleL(l)
    palette.js
    根据调色板颜色亮度,确定新图像亮度的变化


    Palette.colorTransformSingleAB(ab1,ab2,L,x)
    palette.js
    在Lab空间某一亮度的层内,根据论文方法,由$ab1\to ab2$这一对颜色变换,对x做a、b通道的颜色变换


    Palette.colorTransform(colors1,colors2)
    palette.js
    综合几组颜色变换的结果,得到新的图片



    Gallery<div style="text-align:center"><img src="img/gallery/p1.png" height=400><img src="img/gallery/p2.png" height=400></div><div style="text-align:center"><img src="img/gallery/p3.png" height=400><img src="img/gallery/p4.png" height=400></div>---<div style="text-align:center"><img src="img/gallery/p5.png" height=400><img src="img/gallery/p6.png" height=400></div><div style="text-align:center"><img src="img/gallery/p7.png" height=400><img src="img/gallery/p8.png" height=400></div>---<div style="text-align:center"><img src="img/gallery/p9.png" height=400><img src="img/gallery/p10.png" height=400></div>
    0 评论 1 下载 2019-06-18 13:31:16
  • 基于JSP的停车场信息管理系统设计与实现

    1 引言1.1 项目背景软件系统的名称是停车场管理系统。我们开发的系统将帮助停车场管理员和物业公司更加智能化的管理停车场,省去很多的人力物力。方便随时查询停车场的情况,也有助于车主方便随时查找附近可以停车的停车场。
    1.1.1用户基本情况介绍
    角色1:停车场管理员
    查看剩余车位数量,状态
    记录车牌号,出入时间,收钱
    查看停车出入记录
    修改停车位类型(临时车位或永久车位,当有业主购买车位的情况下,记录购买的基本信息,停车位的年限等)
    角色2:系统管理员
    增加停车场数量,因为不止有一个停车场,设置停车场的相关信息
    增加停车场管理员数量
    角色3:用户(车主)手机端
    用户可以查看停车场的停车位信息,以及其他停车场的停车位信息(用户除了可以停在自己小区已购买的停车位,还可以停在周围小区的临时收费停车位)

    1.1.2 项目开发目标停车管理系统能够对对进出停车场的车辆进行科学有效的实时管理,通过过网络和服务中心服务器相联,进行数据共享。停车场系统管理软件可方便地完成管理临时车位、长期占有车位、随时查询停车场情况、修改停车位信息、给更多的停车场提供接口等功能。
    自动统计车辆进出数量,在每个入口处设置显示牌显示该区车辆统计。各停车场系统之间应能进行信号传输,方便各个车主查询和物业公司进行管理。
    1.1.3 用户组织结构
    1.1.4 用户相关业务
    停车场管理员
    记录车的出入信息
    查看停车历史记录
    管理车主买车位的信息
    系统管理员
    管理停车场的属性信息
    管理停车场管理员信息
    用户(安卓)
    查看停车位信息

    1.2 业务对象说明及术语定义
    进库、进场:指车辆进入停车场。
    出库、出场:指车辆驶离停车场。
    车主:指拥有车辆、购买停车位的本小区业主,不是指外来临时停靠的司机。
    车位类型:分固定车位和临时车位,固定车位指已经被业主购买的车位,临时车位指没有被业主购买,可供外来车辆临时停车的车位。

    2 任务概述2.1 目标具有操作简单、使用方便、功能先进等特点,停车场使用者可以在最短的时间进入或离开停车场,从而提高停车场的管理水平,取得更高的经济效益和良好的社会效益。它一方面以智能化设备和完善的管理软件来简化人的劳动,实现停车场车辆进入、场内监控以信息化管理;另一方面通过网络化管理实现能够在一个相对广阔的地域内(例如一个城市)了解多个停车场情况,附近停车场的空车位数。
    2.2 运行环境2.2.1 网络及硬件环境一台联网的pc 和一个安卓手机
    2.2.2 支持软件环境该系统为B/S三层结构,它的运行环境分客户端、应用服务器端和数据库服务器端三部分。

    客户端
    操作系统:Windows7或更新版本。 浏览器:IE8以上,其它常见浏览器如FireFox。
    应用服务器端
    操作系统:Windows7或更新版本。
    应用服务器:Tomcat 7或更新版本。
    数据库访问:JDBC。
    数据库服务器端
    操作系统:Windows7或更新版本。 数据库系统:SQL Server 2008 r2
    Android端
    Android4.4版本或以上

    2.3 条件与限制要求用户具有简单的计算机使用知识,系统暂时无法提供收费管理功能
    3 功能需求3.1 总体功能需求停车场管理系统主要有管理车辆进场出场功能、记录查询功能等。停车场车位划分为固定停车位和临时停车位。满足业主拥有固定停车位和周围散客停车的要求。给不同类型的用户赋予不同的权限管理停车场。主要能管理车辆进场入场、查询历史记录、查询当前停车信息(如空余车位量等)。
    3.2 功能划分根据系统的需求分析,将系统设计的功能分为三大模块:车辆进出管理模块、信息查询模块和系统管理模块。

    停车场管理:车辆入场、车辆出场
    车辆进入停车场时,系统管理员记录车辆的车牌号码和自动获取系统时间作为进入时间。车辆离开停车场时,根据车辆车牌号码判断是否为固定车位车辆来决定是否收费。所有进出停车场的信息(包括车牌号、进入时间、离开时间)都记入一个进出记录表以备查询和统计使用。
    信息查询:某时间段的出入场信息,当前在场信息,车辆历史停车记录
    系统的查询功能可以查询包括自由车位空闲数目、自由车位停车情况、固定车位使用情况、固定车位车主信息、自由车位使用率等多种信息。将自由车位空闲数目显示在停车场入口处,可以提示即将进入停车场的车主;如果自由车位已满,更可以给出指示,并不允许继续进行车辆进入自由车位停车场的操作。
    信息维护:用户及停车位续费等
    查询模块包括自由车位空闲数目指示、固定车位停车情况查询、固定车位车主信息查询、自由车位停车情况查询,指定车辆进出记录查询、系统初始化功能。
    系统管理:车位信息
    进出记录表中记录了包括固定车位车辆和自由车位车辆的所有进出信息,每车每次离开停车场时增加一条记录,非常方便日后查询和统计工作的需要。

    将停车场划分为固定车位和自由车位两部分。固定车位又可以称为专用车位或内部车位,它的特点是使用者固定,交费采用包月制或包年制,平时进出停车场时不再交费。对于固定车位的车辆,系统有着详细的信息记录,包括车辆信息和车主信息。自由车位又可以称为公用车位或公共车位,它的特点是使用者不固定,针对临时性散客服务,车辆每次出停车场时,根据停车时间和停车费率交纳停车费用。固定车位的车辆总是停放在自己的车位上,而不停放在自由车位上。不同类型停车场的固定车位和自由车位数目比例是不同的,,系统可以在系统管理功能里对这两类车位的数目进行设定和修改。
    系统包含三类用户:系统管理员、停车场管理员和普通用户。

    系统管理员能够对停车场和停车场管理员实现信息管理,包括开放对更多停车场的接口,管理各个停车场管理员等。
    停车场管理员可以查看剩余停车位信息,查看以前的停车记录,对车辆的入库出库信息进行管理,以及对于车主购买停车位的信息管理,车主购买停车位的信息管理基本包括信息的增删改查。
    普通用户能够通过手机端查看剩余车位信息。

    3.3 功能需求1系统管理员能够对停车场和停车场管理员实现信息管理,包括开放对更多停车场的接口,管理各个停车场管理员等。
    3.3.1 用例描述
    3.3.2 数据概念结构图
    3.3.3 系统业务流程图
    3.4 功能需求2停车场管理员由可以查看剩余停车位信息,查看以前的停车记录,对车辆的入库出库信息进行管理,以及对于车主购买停车位的信息管理,车主购买停车位的信息管理基本包括信息的增删改查。
    3.4.1 用例描述停车场管理员用例图

    3.4.2 数据概念结构图
    3.4.3 系统业务流程图
    3.5 功能需求3普通用户的定位在于私家车主,只需要能够在手机上查看到指定的停车场有没有剩余的停车位信息即可。
    3.5.1 用例描述
    3.5.2 数据概念结构图
    3.5.3 系统业务流程图
    4 性能需求4.1 数据精确度


    数据
    要求




    车牌号
    格式长度正确


    离开、到达时间
    精确到分钟


    手机号
    11位数


    停车场地址
    精确到道路的哪一号



    5 运行需求5.1 安全性权限控制根据不同用户角色,设置相应权限,用户的重要操作都做相应的日志记录以备查看,没有权限的用户禁止使用系统。只有该停车场管理员能对该停车场进行操作。系统管理员才能新增停车场管理员和开放对其他停车场的接口。
    重要数据加密本系统对一些重要的数据按一定的算法进行加密,如用户口令、重要参数等。
    数据备份允许用户进行数据的备份和恢复,以弥补数据的破坏和丢失。
    记录日志本系统应该能够记录系统运行时所发生的所有错误,包括本机错误和网络错误。这些错误记录便于查找错误的原因。日志同时记录用户的关键性操作信息。
    5.2 用户界面
    屏幕尺寸387mm*259mm手机端建议使用5.2寸或以上屏幕
    5.3 接口要求5.3.1 硬件接口
    服务器端建议使用专用服务器
    5.3.2 通信接口
    http协议
    6 系统结构分析6.1 系统静态结构关系分析说明
    其中的类包括:

    普通用户类:具有车牌号属性,完成用户的查询空车位行为。停车场管理员类:具有管理员工号,电话,身份证号,年龄等基本信息,完成查询剩余停车位信息,查看停车记录,记录车辆出入信息,管理车主购买停车位信息等行为。系统管理员类:具有用户名和密码属性,完成停车场信息管理,停车场管理员信息管理行为。车位信息类,停车场信息类,车主购买车位类,车辆进出场信息管理类(车辆进场信息类,车辆出场信息类)。
    6.2 系统体系结构分析说明

    用户查看剩余停车位信息管理包括普通用户查看指定停车场的剩余停车位信息;停车场管理员信息管理包括查看、增加、删除、修改停车场管理员信息的界面类,控制类以及停车场管理员信息实体类。停车场信息管理包括查看、增加、删除、修改停车场信息的界面类,控制类以及停车场信息实体类。车辆出入信息管理包括包含记录车辆的出入场时间,车牌号等信息。车主购买停车位信息管理包括查看、增加、删除、修改车主购买停车位的界面类,控制类以及车主信息以及停车位信息的实体类。
    6.3 系统部署分析说明
    7 系统功能行为分析7.1 系统业务流程说明系统管理员活动图
    系统管理员的主要活动基本为停车场信息管理和停车场管理员的信息管理活动,包括每种信息的查看,增加,删除和修改活动。

    停车场管理员活动图
    停车场管理员由于对系统操作较多,所以活动也较多,包括查看剩余停车位信息,查看以前的停车记录,对车辆的入库出库信息进行管理,以及对于车主购买停车位的信息管理,车主购买停车位的信息管理基本包括信息的增删改查。

    普通用户活动图
    普通用户的定位在于私家车主,只需要能够在手机上查看到指定的停车场有没有剩余的停车位信息即可,所以活动只有一个。

    7.2 系统交互说明因为系统管理员对于停车场信息管理和停车场管理员的管理流程基本相同,所以这里只写明系统管理员对于停车场信息的管理时序图,对于停车场管理员的流程基本相同。
    系统管理员查看停车场信息时序图

    系统管理员删除停车场信息时序图

    系统管理员修改停车场信息时序图

    系统管理员增加停车场信息时序图

    停车场管理员查看剩余停车位信息

    停车场管理员记录车辆入库信息

    停车场管理员记录车辆出库信息

    停车场管理员查看停车记录

    停车场管理员查看车主购买车位信息

    停车场管理员修改车主购买车位信息

    停车场管理员增加车主购买车位信息

    普通用户查看停车场剩余车位信息

    停车场管理员删除车主购买车位信息

    7.3 系统对象状态演化说明系统管理员主要状态图
    系统管理员主要进行停车场管理员和停车场信息的管理操作,所以主要的状态即为对于停车场和停车场管理员的操作状态。

    停车场管理员主要状态图
    停车场管理员在系统当中功能较多,主要功能涉及查看停车场的剩余停车位信息,查看停车的历史记录,对车辆的出入库信息进行管理,以及对车主购买停车位的信息管理,所以主要状态即为查看信息以及对信息进行管理操作。

    普通用户主要状态图

    8 系统展示登陆主界面

    系统管理员登录主界面

    查看管理员信息界面

    查看停车场信息界面

    添加停车场管理员信息界面

    修改停车场管理员信息界面

    查看出入信息界面

    添加停车记录信息界面

    查看停车记录备份界面

    手机端的查询界面

    手机端的显示界面
    3 评论 114 下载 2018-10-05 22:27:42 下载需要18点积分
  • 基于WIN32 API实现黄金矿工游戏单人版

    一、什么是设计文档游戏类型是什么?游戏有哪些功能?相关数学公式是什么?
    描述一个游戏的所有功能,这就是设计文档,也叫需求说明。真正的设计文档,并不是我写的这个样子,应该由策划来写(俗称“案子”)。我写的这篇,有流程图、有分类,条理清晰,基本上和真实代码完全对应,已经接近伪码了。
    二、游戏状态图
    三、游戏功能设计1.开屏

    显示内容:
    程序启动后,显示初始化图片,计时结束,进入菜单界面。
    逻辑处理:
    控制图片从左至右显示。

    2.菜单

    显示内容:
    显示菜单背景图片,显示“开始”按钮。
    逻辑处理:
    检测鼠标移动。当鼠标移动到按钮上,更改按钮图片。
    检测鼠标单击。当按下按钮后,初始化游戏数据,开始“地图加载”动画。

    3. 地图加载动画

    显示内容:
    显示背景图片,进度条,显示当前关卡、目标金钱数量。
    逻辑处理:
    控制进度条移动。

    4. 游戏中

    显示内容:
    显示人物图片,地面背景。
    显示当前金钱数量,目标金钱数量,剩余时间。
    显示叉子。
    显示金子、石头。
    显示炮的数量。
    显示爆炸动画。
    逻辑处理:
    控制叉子摆动,伸出,收回。
    检测按键“下”,按下后,叉子伸出。
    检测叉子是否碰到物品,碰到物品后,叉子收回,物品跟随叉子移动。
    当物品移动到地面位置,清除物品,增加金钱。
    胜负判断。
    道具使用:
    检测按键“上”,按下后,清除当前所抓物品,播放爆炸动画。
    判断玩家属性:如果有“体力”道具,增加叉子收回速度。
    判断玩家属性:如果有“魔法”道具,增加物品价值。

    5.游戏过关

    显示内容:
    显示过关图片。
    逻辑处理:
    计时结束,进入“购买道具”状态。

    6.购买道具

    显示内容:
    道具按钮:炮,体力,魔法。
    “下一关”按钮。
    逻辑处理:
    检测鼠标移动。当鼠标移动到按钮上,更改按钮图片。
    点击“炮”,增加炮的数量。
    点击“体力”,增加叉子收回速度。
    点击“魔法”,设置玩家属性:有魔法道具。
    点击“下一关”,加载地图,进入下一关游戏。

    7.游戏失败

    显示内容:
    显示失败图片。
    逻辑处理:
    计时结束,进入“菜单”状态。

    8.游戏通关

    显示内容:
    显示通关图片。
    逻辑处理:
    计时结束,进入“菜单”状态。

    四、叉子坐标系统叉子所在位置为坐标原点,所在位置垂线为X轴。向左摆动,旋转角度A大于0;向右摆动,旋转角度A小于0。其中,x,y是屏幕坐标系统。叉子坐标系统示意图:

    五、碰撞检测怎样检测叉子碰到了物品?在叉子端口处,设定一个圆形区域。如果这个圆与物品碰撞,则叉子碰到了物品。示意图如下:

    其中,圆心坐标A(50, 0) ,半径14。
    物品的检测范围也是圆形区域,示意图如下:

    六、地图数据物品属性表



    ID
    价值
    名称
    检测半径(像素)
    移动速度(像素)




    0
    500
    金子
    32
    4


    1
    150
    金子
    24
    12


    2
    50
    金子
    16
    20


    3
    15
    石头
    24
    12


    4
    5
    石头
    16
    20


    5
    600
    钻石
    16
    20



    地图物品数据
    目前只制作了3张地图。
    第一关地图,过关金钱数量:700



    ID
    横坐标
    纵坐标




    0
    50
    110


    1
    100
    270


    2
    200
    370


    1
    380
    370


    2
    480
    340


    0
    550
    150


    3
    190
    190


    4
    390
    260


    5
    120
    380



    第二关,过关金钱数量:1000



    ID
    横坐标
    纵坐标




    1
    50
    110


    4
    100
    270


    2
    200
    370


    3
    380
    370


    4
    480
    340


    5
    450
    400


    0
    550
    150


    1
    190
    190


    2
    390
    260



    第三关,过关金钱数量:2000



    ID
    横坐标
    纵坐标




    4
    50
    110


    1
    100
    270


    0
    200
    370


    0
    380
    370


    3
    480
    340


    4
    550
    150


    2
    190
    190


    4
    390
    260


    5
    460
    300



    游戏截图

    2 评论 83 下载 2018-10-04 21:41:13 下载需要6点积分
显示 0 到 15 ,共 15 条
eject