精华内容
下载资源
问答
  • 他小心翼翼地走了进去,一下子惊呆了,洞中堆满了财物,还有多得无法计数的金银珠宝,有的散堆在地上,有的盛在皮袋中。突然看见这么多的金银财富,阿里巴巴深信这肯定是一个强盗们数代经营、掠夺所 积累起来的宝窟...

    问题背景:

    有一天,阿里巴巴赶着一头毛驴上山砍柴。砍好柴准备下山时,远处突然出现一股烟尘,弥漫着直向上空飞扬,朝他这儿卷过来,而且越来越近。靠近以后,他才看清原来是一支马队,他们共有四十人,一个个年轻力壮、行动敏捷。一个首领模样的人背负沉重的鞍袋,从丛林中一直来到那个大石头跟前,喃喃地说道:“芝麻,开门吧!”随着那个头目的喊声,大石头前突然出现一道宽阔的门路,于是强盗们鱼贯而入。阿里巴巴待在树上观察他们,直到他们走得无影无踪之后,才从树上下来。他大声喊道:“芝麻,开门吧!”他的喊声刚落,洞门立刻打开了。他小心翼翼地走了进去,一下子惊呆了,洞中堆满了财物,还有多得无法计数的金银珠宝,有的散堆在地上,有的盛在皮袋中。突然看见这么多的金银财富,阿里巴巴深信这肯定是一个强盗们数代经营、掠夺所
    积累起来的宝窟。为了让乡亲们开开眼界,见识一下这些宝物,他想一种宝物只拿一个,如果太重就用锤子凿开,但毛驴的运载能力是有限的,怎么才能用驴子运走最大价值的财宝分给穷人呢? 阿里巴巴陷入沉思中……

    问题分析:

    假设山洞中有 n 种宝物,每种宝物有一定重量 w 和相应的价值 v,毛驴运载能力有限,
    只能运走 m 重量的宝物,一种宝物只能拿一样,宝物可以分割。那么怎么才能使毛驴运走宝物的价值最大呢?
    每次选取单位重量价值最大的宝物,也就是说每次选择性价比(价值/重量)最高的宝物,如果可以达到运载重量 m,那么一定能得到价值最大

    算法设计:

    (1)数据结构及初始化。将 n 种宝物的重量和价值存储在结构体 back(包含重量、价
    值、性价比 3 个成员)中,同时求出每种宝物的性价比也存储在对应的结构体 back中,将其按照性价比从高到低排序。采用 sum 来存储毛驴能够运走的最大价值,初始化为 0。
    (2)根据贪心策略,按照性价比从大到小选取宝物,直到达到毛驴的运载能力。每次选
    择性价比高的物品,判断是否小于 c(毛驴运载能力),如果小于 c,则放入sum(已放入物品的价值)加上当前宝物的价值,c减去放入宝物的重量;如果不小于 c,则取该宝物的一部分 c* proportion[i],c=0,程序结束。c减少到 0,则 sum 得到最大值。

    在性价比排序的基础上,进行贪心算法运算。如果剩余容量比当前宝物的重量大,则可
    以放入,剩余容量减去当前宝物的重量,已放入物品的价值加上当前宝物的价值。如果剩余容量比当前宝物的重量小,表示不可以全部放入,可以切割下来一部分(正好是剩余容量),然后令剩余容量乘以当前物品的单位重量价值,已放入物品的价值加上该价值,即为能放入宝物的最大价值。

    测试数据:

    输入:
    6 19 //宝物数量,驴子的承载重量
    2 8 //第 1 个宝物的重量和价值
    6 1 //第 2 个宝物的重量和价值
    7 9
    4 3
    10 2
    3 4

    输出:
    24.6

    代码如下:

    #include <iostream>
    #include<stdio.h>
    #include<algorithm>
    using namespace std;
    
    struct back{
        double weight;//重量
        double value;//价值
        double proportion;//性价比
    }a[999];
    
    bool beyond(back a,back b){
        return a.proportion >b.proportion;//性价比由大到小排序
    }
    
    int main()
    {
        int number;//珠宝的种类
        double sum=0.0;//总价值
        double c;//驴最大载重量
        cin>>number>>c;
        for(int i=0;i<number;i++){//以次输入数据
            scanf("%lf",&a[i].weight);
            scanf("%lf",&a[i].value);
            a[i].proportion = a[i].value / a[i].weight;
        }
    
        sort(a,a+number,beyond);//按性价比由大到小排序
    
        for(int j=0;j<number;j++){
            if(c>=a[j].weight){//宝物的重量小于或等于驴的载重量
                c-=a[j].weight;//装上该宝物之后,驴的剩余载重量
                sum+=a[j].value;//宝物的价值
            }else{//宝物的重量大于驴的载重量
                sum+= c * a[j].proportion;//分割宝物
                break;
            }
        }
        printf("%.1lf\n",sum);
        cout<<sum;
        return 0;
    }
    
    
    
    
    
    展开全文
  • ASO优化:马甲上架优化方法总结

    千次阅读 2019-01-03 16:18:49
    他们通过定期地上下架操作来维护榜单排名和关键词排名 具体例子 2 、通过大量的机刷评论 + 少量的积分墙冲击大词 top3 App 的排名并不高,但某些大词(热度 top8000 以上)排名在 top5 、 top3 ,因此并...

    一、真实操作案例:我是如何0成本让单个iOS马甲包月新增15+自然用户

    二、最近上架及更新审核遇到的一些问题和解决办法

    三、最近的一些aso操作黑科技

    四、前ASM时代ASO从业者的一些思考

    1

    真实操作案例:我是如何0成本让单个iOS马甲包月新增15+自然用户

    我在174月入职杭州一家现金贷的公司,负责公司的app推广。曾经单个马甲app在一个月内没有进行任何付费优化情况下实现了15w的用户新增。下面我将会分享一些我的操作方法及优化技巧。

    影响浏览量和转化率的因素:元数据metadata

    元数据metadata包含:logo、截图、标题、副标题、描述、关键词、支持网址等

    一个简单的公式:APP下载量=APP浏览量*浏览下载转化率

    因此我们的工作就是围绕提高app浏览量及浏览下载转化率这两点展开

    1logo&截图

    logo&截图是APP给人的第一印象,对转化率影响很大,因此让UI设计一套符合产品特性并具有高转化率的logo和截图十分重要

    logo+截图底色:我建议底色选大红或者大黄等暖色调,这样比较吸引注意力,不建议使用冷色调。

    截图尺寸:建议使用横屏截图(2208*1242)避免使用竖屏截图(1242 * 2208)拼接带来的破碎感

    2、标题:产品名称-后缀文案

    标题作用

    a、实现关键词覆盖

    b、后缀文案影响用户心智,提高浏览下载转化率

    关键词覆盖:这个可以通过关键词组合来命中尽可能多的有效关键词,一个比较极端的做法是一些CP弄一个超级长的标题来覆盖关键词。但现在这种操作手法法几乎不会通过审核,还在线上的长标题app的上次更新时间都是在16年及以前,而且长标题app越靠后的关键词权重比较低。这种操作手法现在基本很少使用。

    后缀文案(slogan)可以影响用户心智,提高浏览下载转化率。而且考虑到标题太长在屏幕显示不全,因此不要有空格,不要用长划线,用短划线隔开

    因此我认为一个好的标题是:产品名字-产品特点(关键词覆盖+介绍产品特点,这个可以进行一定程度的夸张宣传)

    具体例子

    ***-借钱只要1分钟

    ***-门槛低借钱快

    3、副标题:

    当时app store还未改版,没有副标题选项。

    现在的话写副标题我建议从:1)对后缀文案进一步补充 2)关键词覆盖这两方面进行考虑

    4、描述

    (就当时情况而言)我认为不太重要,因为描述在上架后可以修改,并没有锁定,这时候描述并不会对关键词的覆盖和排名产生影响。

    而大部分用户基本在搜索结果页就完成了app的下载,并不会进入app的详情页进行查看。

    PS:现在描述被锁定,只能在更新时进行修改,因此描述对增加关键词覆盖和排名有一定帮助,建议大家用心填写。

    5、关键词

    aso优化的核心内容:围绕关键词,因此关键词的筛选及组合极其重要,重要程度可以说在免费的优化工作占比达到80%甚至更多。

    关键词设置要点:

    a、关键词的组合(本地化选项):中文、澳大利亚、英国

    PS个人测试经验:中文繁体、美国英文、日语、韩语等地区的关键词对中国区关键词覆盖基本无效。(这条可能每个人经验不一样,可以探讨)

    b、为了节省字符不要使用空格逗号等

    关键词前后连接,节省字符,组合完成后放到aso100或者禅大师的aso优化助手将重复的关键词去掉,并添加新的关键词,这样可以覆盖尽可能多的关键词

    关键词前后连接例子:

    滴水贷、贷上花、花无缺

    关键词组合结果:滴水贷上花无缺

    关键词去重具体例子2

    现金白卡、萌虎白卡、小猪白卡

    关键词去重结果:小猪萌虎现金白卡

    c、关键词库的积累和筛选

    关键词库的积累:

    这是个长期性的工作,需要大家日常积累并维护自己的关键词库

    我尝试过使用一些搜索引擎工具如站长工具获取以及百度推广的后台获取关键词,后面发现这种效果并不好,因为发现网页端和应用市场端搜索习惯不尽相同,两者关键词的热度搜索量差距很大。

    目前我积累关键词库主要通过下面几个方式:

    1)竞品app获取关键词:大词的搜索结果、竞品app联想推荐

    25000热度以上关键词获取

    3)排名top榜单

    4)每日上架app

    因为我是乙方,合作伙伴行业各不相同,因此我会对获取的关键词按照不同行业进行分类:如理财、贷款、股票、电商、小说、区块链等,并用数据库软件(如mysql等)来更方便地管理我的关键词库。

    关键词的筛选

    针对某个行业,大概会积累几千个到上万个关键词,单个app的话一次上架或者迭代只能挑选100-200多个关键词进行适当组合,因此需要挑选高价值的关键词进行组合

    因此需要设计一个合适的计算关键词的价值度的公式,公式主要考虑这几个因素:关键词长度、关键词热度、搜索结果数

    其他因素

    1、对2款竞品APP蹭量

    2、一次被动地漏洞营销

    1、对2款竞品蹭量

    当时某竞品app**万卡在站外做了大量的品牌及效果推广但他们app体验价差(评分较低)且几乎没做过aso优化。我挖掘发现了他的高价值度品牌关键词(8000度热度,个位数的搜索结果数)及相似品牌关键词(5000多热度)(同音字关键词、字形相似的关键词),通过上述操作手法将我的马甲app排名优化的十分靠前。

    后面他们向苹果投诉我们,被我怼回去了,因为我确实没有机刷或者做积分墙,纯粹的自然优化,

    再后来他们没办法,临时做了一个马甲包,然后马甲包机刷品牌关键词将马甲包的关键词挤上去

    最开始排名情况:

    我的马甲包A

    我的马甲包B

    **万卡主包

    中期排名情况

    我的马甲包A

    **万卡马甲包

    我的马甲包B

    **万卡主包

    后期排名情况

    **万卡马甲包

    我的马甲包A

    我的马甲包B

    **万卡主包

    另一款品牌关键词(热度6000多的app,搜索结果数也很少,*夫贷)不知道什么原因暂时下架了,我们当时在他们的主关键词排名第二,他们app下架后我们排名第一,因此也蹭到了他们的一波流量

    2、一次被动地漏洞营销

    不知道你们是否还记得,178月的时候,KFCappbug了半价优惠券事件,后面被认是一次漏洞营销事件(相关链接:http://www.sohu.com/a/168624059_114778)。

    其实我们在他们之前也实现了一次漏洞营销,当然这个漏洞并不是我们主动放出的而是被动的。

    当时我们的马甲app有好几个,但没有做用户身份的一致性校验,使用不同的电话注册后可以使用同一套身份证进行申请并下款,这个漏洞在一些口子群、论坛迅速传播,有很多老哥们来薅我们的羊毛。后面我们的技术及时修复了这个漏洞。但复查的时候发现最多的一套身份资料成功借款几十次。

    上述多种因素叠加的影响导致那款马甲app在没有付费优化及推广情况下实现了月新增15w用户的效果。

    2

    最近上架及更新审核遇到的一些问题和解决办法

    首先建议大家先看看App Store 审核指南

    https://developer.apple.com/cn/app-store/review/guidelines/

    很多被拒的原因解释都可以在上面的官方链接找到,可以针对性地进行回复或者修改。

    a1.1.6元数据被拒及2.3被拒

    解决办法:

    去掉所有敏感信息、标题、副标题、描述、关键词等

    截图使用真实截图

    首次上架审核通过迅速发一版,换成转化率高的元数据

    b1.1.6二进制文件被拒,开关走起

    c2.1逐条回复即可

    没问题最好

    有问题也要信誓旦旦,装无辜误伤

    d3.1.1看你切支付的技术

    e3.2.1或者5.2.1

    资质问题,金融类app都会遇到的问题

    逐步升级:金融账号->小贷账号->银行账号,

    银行账号也是概率过包

    现在套壳比较多:比如理财套成资讯、贷款套成记账或者手机回收。

    f4.2最低功能,app太简单,改一改,碰上其他审核员,基本上能过

    g4.3,重复应用

    最开始:同账号下只需将马甲app名称和UI换颜色换一下即可提交并审核通过

    后面:需要用不同账号提交

    再后来:批量修改类名变量名的代码混淆脚本+修改资源的md5+添加垃圾代码

    最新情况:大概在5月初的时候苹果升级算法,包括一些上线的app被复查4.3下架,以上方法基本失效了,需要重写代码逻辑才能通过。

    5.1数据隐私

    欧盟GDPR法案《通用数据保护条例生效》,对跨国公司影响很大,几十亿美元的罚款,一些公司都更新了他们的用户条款

    建议:除非权限确实需要,不用申请那么多的权限。并且在申请权限的plist做出合理的解释,让审核人员认为获取这个权限是合理的。

    3

    最近的一些aso操作黑科技

    1、定时上下架

    观察下架app(链接:https://old.qimai.cn/rank/offline)你会发现有些app又重新上架了

    他们通过定期地上下架操作来维护榜单排名和关键词排名

    具体例子

    2、通过大量的机刷评论+少量的积分墙冲击大词top3

    App的排名并不高,但某些大词(热度top8000以上)排名在top5top3,因此并不是大量积分墙堆量,观察发现,每天刷几千到几万的评论。

    3、上热搜的一个操作思路

    干预型上热搜(基本失效):大量的关键词搜索下载机刷怼上去,现在基本失效了,大概单次费用在3w左右

    预测型上热搜:分析最近上热搜的关键词的热度变化情况,预测哪些词会成为热词,提前押宝,通过两种方法

    a 提前积分墙关键词排名刷上去进行布局,

    b 快速马甲包改名字,直接蹭量。

    实际案例:**日记被下架后同行app通过改名字迅速蹭了一波流量。

    这样带量效果会很不错,而且成本会相对很低。

    4

    ASM时代ASO从业者的一些思考

    ASM即将在中国地区登录,因此苹果必然会打击马甲、干预型aso的这些行为,让大家通过付费asm来获客从而获得更多的营收。就像苹果打击切支付的行为,淘宝打击刷单的行为

    乙方:提前布局,提供asm增值服务,

    ASOer:去有预算的甲方,后面aso很多时候只能锦上添花,aso的操作空间相比之前会小很多,这就要求大家不要仅仅局限于aso,而是全局观,从产品整体层面的营销来思考问题。

    这样哪里有流量,哪里有搜索框的在哪里,我们就在哪里。

    希望大家不仅仅只是作为一个ASOer,而是发展成为一个全面增长黑客。

    课后精彩问答合集

    Q:机刷评论顶大词,具体是怎么操作的呢?

    A:可以先把你行业的大词搜索一下结果,看下有没有同行这样操作的案例,然后你可以直接把他们的评论直接拿过来复用就可以了,评论按天的话至少要每天五千条以上,维持七天一个周期,然后再观察效果,而且和你本身的包也有关系,如果之前被惩罚过的话也是打不上去的。

    Q:理财类的APP要上架有什么办法吗?

    A:理财类的现在用银行账号有一定的几率可以上去的,另外一个目前用资讯类的套壳也可以,比如做成一个理财助手,里面是一些咨询,然后通过版本迭代更新更成正常的包,这其中的话开关肯定是必须的

    Q:股票APP遇到4.3的问题可以怎么过包?要注意那些点呢?

    A4.3问题还是重复应用,你股票的包可能上了好几个马甲,苹果更新算法之后会判你4.3,机审要把你们的代码逻辑重新写一遍,UI也要做大的变动,之前用脚本混淆和批量修改的MD5值的方法已经失效了。

    Q:贷超上线需要哪些资质呢?迭代审核会放松吗?

    A:可以把贷超伪装成记账或者手机回收这类的APP,等审核通过后再关上开关。迭代审核方面,假如每次版本是1.1更新,你的开关打开,然后假如现在正常版本是1.0,开关关上是正常显示的,如果担心不通过,可以分几步走,每次只小部分迭代一些内容,这样过审率高一些。

    Q:现在预判型热搜可以尝试吗?

    A:可以的,但是预判型热搜主要也看行业,之前是可以通过机刷大量关键词搜索,现在是自然量大量关键词搜索,如果是冷门行业基本上是没有上热门的可能,所以对冷门行业来说是没什么用的。

    Q:落榜的关键词或者排名很靠后的关键词一般怎么做上去

    A:落榜关键词现在做积分墙可以,但是对排名有要求,排名200以后的积分墙是不做的,可以尝试打联想关键词,再加一些评论,将关键词带到200名以内,产品可以更新设置尽量更新一下,将词的覆盖带上去,然后再通过积分墙把目标关键词刷上去。

    Q:提审的游戏包给了2.1,有发邮件逐条解释,但是苹果审核团那边没回复,还有机会吗?

    A:可以通过品宣的方式带入一些自然量,听了让苹果觉得你有正向的流入慢慢恢复。如果一个包废掉了,可以上一个马甲包就尽量上,不要在一棵树上吊死,对自己的主包尽量少进行违规操作。

    Q:请问被苹果下架的包,因为4.3等问题多次重新提交依然上不去,如果把这个转让给其他账号会影响之前的权重吗?

    A:现在是APP提交之后,它就会把APP的代码特征提取,后面就算用其他账号提交,UI和产品名字都改以后还是会匹配到出现4.3的问题

    Q:小公司有必要做马甲包吗?

    A:有些大厂也做马甲包的,像之前网易新闻被下架之后,它的量就是用马甲包冲上去的,你们可以观察一下网易新闻的马甲包,在网易新闻主包下架时的排名情况,马甲包就是一个备份作用,有备无患,而且马甲包其实是一个获客成本非常低的方式。

    Q:请问一下,比如说游戏的关键词,整个关键词库里,很多关键词都带有字结尾,是不是可以在其中一套关键词,只出现一个侠字就可以了

    A:是的,就是关键词里面设置,比如之前你在标题覆盖过了,或者你在其他的地区本地化设置里覆盖了,后面的话就不用再重复覆盖了,你可以覆盖其他的关键词。

    Q:比如仙侠,武侠,奇侠,仙剑奇侠,侠客。是不是可以变成:仙武剑侠客呢?

    A:可以的,但有时候苹果分词不一定会给分出来,这就需要你多实践,可以迭代分词覆盖上去,如果这个词很重要就不要用重叠方法,用前后连接的方式进行关键词组合。

    Q:重叠覆盖关键词是不是可以提高关键词覆盖权重呢

    A:我常用的是进行关键词覆盖,提高排名可以通过积分墙方式,如果说重叠关键词可以提高关键词权重的话,你可以把关键词里面三个都设成行业的最高热度

    Q:在本地化关键词的时候听说用逗号隔开,会提高关键词的权重,是这样吗?

    A:这个不是提高权重,是苹果有时候分词没有把关键词分出来,逗号是为了强制苹果分词,我一般不会加空格,然后挑选高价值的关键词尽可能地多覆盖,然后提高整体的权重而不是提高单个关键词排名。你可以去搜索引擎搜一下分词算法,基本上中文的算法就那几种,苹果很多时候是用其中一种或某几种算法调它的权重,很多时候你看一下他算法大概的原理就会明白,再根据自己的实际经验就可以总结出规律。要理论与实践相结合。

    展开全文
  • 一、前言 发现就喜欢研究这些插件,为什么呢,因为方便快捷啊。基本不用研究源代码怎么实现的,只要会有就行了。但是,光这样也不行,还是要多去看看底层代码是怎么实现的,还有人家的框架是怎么搭的。...

    推荐阅读

    一、前言

    发现就喜欢研究这些插件,为什么呢,因为方便快捷啊。基本不用研究源代码怎么实现的,只要会有就行了。但是,光这样也不行,还是要多去看看底层代码是怎么实现的,还有人家的框架是怎么搭的。
    要不说Unity3D入门容易,提升难呢,因为提升全是靠苦功夫,去研究底层代码。算了,不絮叨了

    二、参考文章

    Unity3D 装备系统学习Inventory Pro 2.1.2 总结
    Unity3D 装备系统学习Inventory Pro 2.1.2 基础篇

    三、正文

    先上一张效果图
    这里写图片描述
    下载链接:
    https://github.com/764424567/Unity-plugin/tree/master/Menu/Unity3D-S-Inventory
    无效了记得跟博主说一声哈

    1、总体结构

    这里写图片描述

    物品相关

    • 非UI相关InventoryItem 物品体系类,具体如装备,消耗品,商店物品等

    • UI相关InventoryUIItemWrapper 物品体系

    • UIItem的UI包装的Item继承体系

    • ItemCollection这样的类,因为简单的增、删和改肯定是逃不了,复杂的如交换,容器间的交换等操作

    UI窗口相关

    • UIWindow体系的窗口类,具体有角色,银行,技能,店铺等窗口

    • InventoryUIDialog系统下的对话框类,具体有确认框,买卖,通用提示

    • 特殊窗口(非继承体系窗口),如上下菜单,通知窗口等

    管理相关类

    • 配置管理

    • InvertoryManager

    • ItemManger

    • 数据库操作

    其它

    应该是一些辅助类,有UI部分的,事件辅助,定义接口等等吧,这部分还没有深入去阅读应该也是挺复杂的

    剩下部分

    一些第三方插件,Unity3D特性及Edit扩展等等

    具体类图

    这里写图片描述
    这里写图片描述

    2、使用教程

    示范项目

    在你的Assets/Demos/Scenes 你会发现这些演示场景,这些演示场景会包含所有特性,一定要仔细的看一下哦
    这里写图片描述

    建立一个新的项目

    • 选择1(自动)
      打开设置向导,它可以发现在Tools/Inventory Pro/Setup wizard
      对于错误“没有管理者发现的对象”;单击确定按钮和一个管理对象命名为"_managers"将自动添加到你的场景里。
    • 选择2(手动)
      创建一个空的游戏对象将inventorymanager组成,你可以找到在 库存/经理/ inventorymanager你会得到几个管理器组件包括inventorymanager,inventorysettingsmanager和更多的,我们不需要现在。

    数据库建立

    itemmanager包含所有项目的项目数据库,你可以创建管理每个场景,并使用不同的数据库,每一个场景。

    1. 去你的项目窗口中,找到一个地方,你想创建项目数据库。

    2. 右键单击(或点击创建,或者去创建/ 库存亲 /项目数据库)来创建一个新的项目数据库。

    3. 一旦创建的数据库将在你的项目中选定。

    4. 将项目拖到itemmanager的试题数据库槽。

    现在也让我们创建 语言数据库 ,做完全相同的操作,并将它分配给inventorymanager的 lang 。

    配置设置

    首先打开主编辑器可以发现在 工具/库存亲/主编。当你打开编辑会给你一个错误消息的第一时间(见下图)。

    这是因为库存亲需要知道它应该保存新项目。单击“设置”按钮,选择一个文件夹 文件夹里面你的项目。

    现在让我们打开整合经理再次看到我们必须配置。

    第一个选项卡“UI”包含用于渲染UI的基本要素。为了做到这一点需要至少2个项目的 项目按钮预置 Item Button Prefab和 UIROOT。

    这个项目按钮预置是的预置 /槽包含的项目。根是包含所有用户界面窗口的UI画布。

    几个演示器可以用来快速上手。你可以在库存/演示/资产/用户界面/ ui_prefabs / ui_item_pfb找到默认的包装

    并新建一个UGUI,把UI Canvas 赋值给 GUI ROOT。

    对象编辑

    1. 项目编辑

    主编辑器,包括项目编辑器可以打开通过 Tools / Inventory Pro / Main editor

    1.1 创建项目Creating items

    首先,单击“创建项目”按钮,一旦我们做了一个对象选择器将显示。

    在对象选择器,我们可以选择的物品类型。每一项类型具有不同的行为或目的,例如当消耗品被使用时将减少1个的堆栈大小,而当武器装备被装备时出现人物在场景中得到呈现。

    你可以使用键盘来选择类型来创建,使用向上和向下箭头,选择一个你想要的并回车确认。

    让我们抓住一个consummableinventoryitem (可消耗品)现在,并配置。

    接下来你会被提示步骤2。在这里你可以选择一个模型,以此为基础的新项目。例如,你已经有了一个预置,例如一把剑,它拥有的纹理图片、可以使用碰撞器和自定义组件,那么只需拖动物品到“Drag object here”字段位置上,或者选择使用“Select model”按钮。

    假设你没有一个预先定义的模型,只是想创建一个新的对象选择“No model”,或“2D sprite with trigger“用于2D游戏。

    一旦创建的项目将显示一个按键在列表的底部,点击并生成物品。

    一旦项目(预制)是创造了你可以修改它通过改变纹理,模型,添加自定义组件,等。

    1.2 类别Categories

    使用类别编辑器你可以定义项目的具体类别,例如食物,药水,剑,斧,等你可以定义每一类的冷却时间,这样你可以使用消耗品,同一类别中的所有其他项目也将在冷却。就是说,你可以重写每个项目的冷却时间在项目编辑器”选项卡。

    1.3 性能Properties

    使编辑更强大的性能也增加,属性允许您创建自定义的“变量”。一旦创建,你可以指定的属性项目编辑器内的任何项目,它提供一个值。 属性也可以通过 自定义代码。

    值的字符串格式: 格式允许您使用该属性的值在UI格式。 使用{ NR }符号来定义自己的格式。

    基准值: 基值的初始值的属性。例如,你可能有5的强度默认和允许它从那里成长。

    1.4 稀有性编辑 Rarity editor

    最后但并非最不重要的你可以定义你的项目多种多样,各有它的颜色,在工具提示中显示的用户界面元素。

    定义物品的稀有程度分级,在UI中显示的颜色区别,还有物品掉落时出现在场景中的预置显示(这里默认是一个袋子)

    ——

    2.装备编辑 Equipment editor

    设备系统非常灵活,可用于几乎任何装备/附件系统。人物属性是通过选择一个或多个项目类型的定义,可以使用编辑器选择哪些数据将被计算,最终显示。

    除了统计也有装备的类型,这些可以被定义在“装备类型编辑器选项卡 Equip type editor tab ”。装备类型可以限制避免某些组合。例如,当装备单手剑,我确信双手的剑和斧子和匕首双手不兼容。当单手剑装备,装备一把单手匕首,匕首将被装备。

    ——

    3 货币编辑Currency editor

    货币 编辑器允许你定义,可以用在你的项目,制定蓝图,供应商的货币,等。

    每一种货币可以包含一组转换。这些转换允许你将一个美元兑欧元。

    自动转换的定义是否可用于汽车。货币之间转换。例如,许多游戏使用系统的金,银和铜。当跑出来的铜系统可以转换到铜银货币。基本上重新填充它从一个更高的货币,可以转换成。

    自动转换可以在编辑器底部的定义。

    自动转换最大:允许你转换到一个更值钱的货币一旦你达到定义的最大值。例如,你不能拥有超过100的铜,所以一旦系统发现你有100以上的铜将被转换为1银。

    自动转换分数: 当“让分数”(在顶部)不启用的分数也需要转换,或丢弃。例如,当你有1.1银(这是不允许的)系统会把它降到1银10铜。

    4 制作编辑

    制作经理类,允许你创建“锻造”任何时候,无论是烹饪,锻造或皮。

    让我们创建一个新的 category,并开始创造一些蓝图。

    #### 制定蓝图 Crafting editor

    1. 默认情况下,结果项目名称–项目成功后,工艺–将作为蓝图的名称,但你当然可以,像往常一样,配置。

    2. 机会因素表明工艺成功的可能性有多大,0.5种工艺的机会有50%的机会,和1的机会的因素有100%的成功机会。

    1. 加速因子是成对的制作时间,在许多游戏(哇)制作一项变快时,创建一个完整的批一次。例如,当制作10烂苹果,第一项需要5秒,第二,1.1(10%)更快,这归结为5 /(1.1 ^ N)。

    3. 所需物品说明很多项目所需的工艺给定的项目,这是从布局上分,可以在底部的定义。

    一旦你定义的蓝图,你可以创建一个标准或布局的基础工艺窗口允许用户做他的 事。

    语言编辑器 Language editor

    语言编辑器允许你定义一个特定的动作发生在显示时的库存支持信息。

    目前只有1个语言数据库的支持,为多语言数据库支持将来会增加。

    如果你喜欢一个特定的消息不出现只是让它空着。

    设置编辑

    搜索

    请注意,您还可以使用搜索栏搜索名称的变量设置/你想改变。

    3、Demo解析

    GettingStarted.unity

    这里写图片描述
    首先说一下Demo1的功能,其实很简单主要是建立起来Inventory Pro的运行环境,首先项目的Demo是3d的所以创建项目时,选择是3D工程。运行环境中,使用标准插件库建立一个第三方视角跟随的角色,角色可以在Panel中自由的移动跑跳;然后才是Inventroy Pro的基础配置,主要是引入Setting,在Setting中进行一些基础的配置。具体的运行后的界面如下图所示
    这里写图片描述

    设置一个角色

    第一步在Scene中添加一个Panel,然后把它设置大点,不然角色会掉下去
    这里写图片描述
    第二步,找到图中的角色prefab然后直接拖到场景中,reset一下即可
    这里写图片描述
    第三方视角相机跟随

    第三方视角相机跟随,也是按照标准过程进行

    1,删除原来的MainCarmar摄像机

    2,从Asset中拖拽我们需要的Prefab到场景中来
    这里写图片描述
    第三步,设置相机的Target为我们的控制角色,这里拖拽即可
    这里写图片描述

    最后是装备系统配置创建

    基础环境创建好了,下面我们需要创建下装备系统的自身的基础环境了,涉及到了Srcript,Manage文件夹中的四大基础类

    装备系统配置类,

    装备系统管理类

    Item管理类(工厂可能不准确,欢迎指正)

    装备数据Asset类

    Demo1中其实要实现的就两步

    第一步,创建空游戏对象,配置InventorySetting类

    第二步,初始化游戏Item数据Asset

    4、实例

    Demo1:使用插件来实现一个装备窗口

    功能点:

    • 1、实现了两个窗口,通过点击键盘I来,打开或者关闭窗口也就是Toggle功能

    • 2、装备窗口中的物品栏空格数量动态生成可控,可以在属性窗口手动配置

    • 3、窗口具有拖拽功能

    • 4、窗口物品具有拖拽,及窗口间拖拽

    • 5、可以在窗口使用物品的功能,物品有消耗扇形显示功能

    具体效果图如下所示:
    这里写图片描述

    插件使用

    1、具体在UGUI 中的Canvas中创建一个InventoryWindow

    2、在InventoryWindow下创建空GameObject并命名Container,赋予Grid LayOut 插件

    3、给InventoryWindow添加InventoryUI组件,插件将自动添加WindowUI也就是通用窗口辅助插件

    4、添加拖拽功能组件DraggableWindow,这样窗口就有了拖拽功能了

    至此简单的点击I键可以打开和关闭的装备窗口做好了

    总结

    最后总结下实现通用窗口的三个类,分别是WindowHelper文件夹下的,UIWindow,UIWindowPage和DraggableWindow
    这里写图片描述
    1、DraggableWindow有就是拖拽窗口的组件,这里还是比较赞的,也是插件编程的简单例子,这里学过UGui的同学都知道要实现拖拽功能实现IBeginDragHandler和IDargHandler接口即可,原理很简单, 源码如下

    using UnityEngine;
    using System.Collections;
    using UnityEngine.EventSystems;
     
    namespace Devdog.InventorySystem
    {
        [AddComponentMenu("InventorySystem/UI Helpers/DraggableWindow")]
        public partial class DraggableWindow : MonoBehaviour, IBeginDragHandler, IDragHandler
        {
            public float dragSpeed = 1.0f;
     
            private Vector2 dragOffset;
     
     
            public void OnBeginDrag(PointerEventData eventData)
            {
                if (InventorySettingsManager.instance.isUIWorldSpace)
                    dragOffset = transform.position - eventData.worldPosition;           
                else
                    dragOffset = new Vector2(transform.position.x, transform.position.y) - eventData.position;
            }
     
            void IDragHandler.OnDrag(PointerEventData eventData)
            {
                transform.position = new Vector3(eventData.position.x + dragOffset.x * dragSpeed, eventData.position.y + dragOffset.y * dragSpeed, 0.0f);
            }
        }
    }
    

    2、UIWindow这个类是窗口的公共类,先上类图主要功能点在类图上标注了,这里就不废话了,主要就是控制的窗口的显示关闭,及组合动画效果比较难的是实现了类似组合窗口的功能(这部分有后有机会再仔细分析)
    这里写图片描述
    源码就不全上了,上点有亮点的部分如下:

    public virtual void Hide()
    {
        if (isVisible == false)
            return;
     
        isVisible = false;
     
        if (OnHide != null)
            OnHide();
     
        if (hideAudioClip != null)
            InventoryUIUtility.AudioPlayOneShot(hideAudioClip);
     
        if (hideAnimation != null)
        {
            animator.enabled = true;
            animator.Play(hideAnimation.name);
     
            if (hideCoroutine != null)
            {
                StopCoroutine(hideCoroutine);                   
            }
     
            hideCoroutine = _Hide(hideAnimation);
            StartCoroutine(hideCoroutine);
        }
        else
        {
            animator.enabled = false;
            SetChildrenActive(false);
        }
    }
     
     
    /// <summary>
    /// Hides object after animation is completed.
    /// </summary>
    /// <param name="animation"></param>
    /// <returns></returns>
    protected virtual IEnumerator _Hide(AnimationClip animation)
    {
        yield return new WaitForSeconds(animation.length + 0.1f);
     
        // Maybe it got visible in the time we played the animation?
        if (isVisible == false)
        {
            SetChildrenActive(false);
            animator.enabled = false;
        }
    }
    

    以上部分是通过协程实现的具有延时效果的动画关闭窗口的代码,有代表意义。

    3、UIWindowPage类,该类是UIWindow的子类,在UIWindow有一个Page的集合用于组合显示UIWindowPage,这块Demo1中没有涉及到该功能这里就不仔细分析了,等后面的例子中出现了再研究,亮点代码如下:

    /// <summary>
    /// Container that olds the items, if any.
    /// </summary>
    public RectTransform itemContainer;
    public UIWindow windowParent { get; set; }
     
    public override void Awake()
    {
        base.Awake();
     
        windowParent = transform.parent.GetComponentInParent<UIWindow>();
        if (windowParent == null)
            Debug.LogWarning("No UIWindow found in parents", gameObject);
     
        // Register our page with the window parent
        windowParent.AddPage(this);
    }
     
    public override void Show()
    {
        if(isEnabled == false)
        {
            Debug.LogWarning("Trying to show a disabled UIWindowPage");
            return;
        }
     
        base.Show();
     
        windowParent.NotifyPageShown(this);
    }
    

    这里UIWindow和UIWindowPage 本身是继承关系,然又彼此引用,代码可读性有些差了,作者这里通过Page类中Awake和Show来做父类的初始化和调用,也是一种方法,我觉得还行(请高手拍砖)。

    总体来说目前的UIWinow和UIWindowPage更像是容器Panel或者Group不像是窗口,等以后的Demo中有复杂的再学习吧。

    4、如何通过键盘唤起窗口

    这个比较简单用到了U3D的输入输出模块,关键代码如下:

    /// <summary>
    /// Keys to toggle this window
    /// </summary>
    public KeyCode[] keyCombination;
     
    public virtual void Update()
    {
        if (keyCombination.Length == 0)
            return;
     
        bool allDown = true;
        foreach (var key in keyCombination)
        {
            if (Input.GetKeyDown(key) == false)
            {
                allDown = false;
            }
        }
     
        if (allDown)
            Toggle();
     
    }
    

    Demo2:通用窗口的具体实现

    功能点:

    • 1、实现了两个窗口,通过点击键盘I来,打开或者关闭窗口也就是Toggle功能

    • 2、装备窗口中的物品栏空格数量动态生成可控,可以在属性窗口手动配置

    • 3、窗口具有拖拽功能

    • 4、窗口物品具有拖拽,及窗口间拖拽

    • 5、可以在窗口使用物品的功能,物品有消耗扇形显示功能

    • 6、通用窗口的类体系结构

    具体的插件使用和功能已经在上篇中说明了这里就不多说了

    1、本篇重点分析 6通用窗口的类体系结构,类组织和类图如下所示:
    这里写图片描述
    类的继承体系结构这里就说了,在第一篇有可以自行查阅
    这里写图片描述
    类的引用关系、核心字段和方法已经在类图中标记的很清楚,用简单的几句话说明下,装备窗口中的每个格子是由一个空格子具有背景的UIItem和InventoryItemBase Model组成的,而整个装备窗口是一个InventoryUI,该类继承了ItemCollectionBase类,也就是说它是具有一组UIItem的装备集合窗口,添加上UIWindow组件、DraggableWindow就具有了普通窗口的拖拽移动和显示关闭的功能了。

    2、装备窗口中的物品栏空格数量动态生成可控,可以在属性窗口手动配置

    如何实现动态装备窗口主要有两个核心技术:一个是UI中的自适应排列,也就是Grid layout Group组件;一个是U3D的prefab实例化技术

    动态初始化Cell数据核心代码如下

    protected virtual void FillUI()
            {
                if (manuallyDefineCollection == false)
                {
                    items = new InventoryUIItemWrapperBase[initialCollectionSize];
    
                    // Fill the container on startup, can add / remove later on
                    for (uint i = 0; i < initialCollectionSize; i++)
                    {
                        items[i] = CreateUIItem<InventoryUIItemWrapper>(i, itemButtonPrefab != null ? itemButtonPrefab : InventorySettingsManager.instance.itemButtonPrefab);
                    }
                }
                else
                {
                    for (uint i = 0; i < items.Length; i++)
                    {
                        items[i].itemCollection = this;
                        items[i].index = i;
                    }
                }
            }
    
            protected T CreateUIItem<T>(uint i, GameObject prefab) where T : InventoryUIItemWrapperBase
            {
                T item = GameObject.Instantiate<GameObject>(prefab).GetComponent<T>();
                item.transform.SetParent(container);
                item.transform.localPosition = new Vector3(item.transform.localPosition.x, item.transform.localPosition.y, 0.0f);
                item.itemCollection = this;
                item.transform.localScale = Vector3.one;
                item.index = i;
            
                return item;
            }
    

    是不是很简单 initailCollectionSize是InventoryUI基类的一个共有field也就是说这个装备格子的数量,这个可以根据自己设计的装备窗口手动设置,然后根据这个循环调用CreateUIItem泛型方法通过,GameObject.Instantiate动态实例化预设装备格子对象,并设置Parent和位置

    Demo3:通用窗口的具体实现可拖拽功能

    窗口间物品的拖拽

    自己在学习的过程中,虽然读了源码过了几天来写教程,还是有点不清楚,不能亲车熟路(这也许就是读与写的区别吧),给自己提出了几个问题,在重新去翻代码之前先给自己提出几个问题:

    • 1、拖拽的事件发起者应该是那个类?

    • 2、拖拽的事件的Drag是如何确定下方的UI元素的?

    • 3、拖拽后的逻辑操作,应该由哪个类来承接?

    • 4、窗口与窗口之间的拖拽,既有Drag又有Drop,如何更加合理的解决这个问题?

    • 5、窗口间物品拖拽的以及同窗口物品拖拽的逻辑流程是什么?

    A1 拖拽的事件发起者应该是那个类?:拖拽的发起者必须是UI元素这点是必须的,目前涉及的UI元素有两种一种是窗口容器,一种数据装备格元素,显然装备格元素更有优势,因为少了一层定位逻辑判断,Drag事件直接发起,还可以做Move的相关逻辑,这里Inventory Pro2也确实是怎么做的(这里初次接触UGUI的同学可能要去学习下相关的事件系统,这里就不多说了),这里InventoryUIItemWrapper就是装备格的基类,这里继承了UGUI的相关UI事件,IBeginDragHandler, IEndDragHandler, IDragHandler, IPointerUpHandler, IPointerDownHandler, IPointerEnterHandler, IPointerExitHandler,这么多接口也预示着代码并不简单
    这里写图片描述
    这些就是接口实现函数,把他们都弄明白了也就明白了如何实现拖拽
    这里写图片描述
    A5 窗口间物品拖拽的以及同窗口物品拖拽的逻辑流程是什么?:先回答这个问题比较合理,在过去的winform的拖拽中并没有这么多接口可以实现,但是我相信拖拽操作的本身逻辑应该是不变的,也就是几个步骤,

    • 1)在物品上点击鼠标左键(记录鼠标点击的元素)->

    • 2)在鼠标不up,且move事件中确认了拖拽开始(Drag事件) –>

    • 3) mouse Move事件中获得鼠标下的元素->

    • 4)mouse up 事件触发Drop,判断鼠标位置及鼠标下元素是否可以drop如果可以进行Drop逻辑至此,这个拖拽操作结束

    技术原型就是这么简单。下面看看Uintiy3d ugui Inventory Pro是如何实现的,又读了一遍代码,深深的有体会到了一把,“原理很简单,现实很残酷”,这还是在ugui为我们做了一些封装的情况下,这里其实涉及的函数其实有5个

    • OnPointerEnter:确定了点击了那个UI元素,对应1)

    • OnBeginDrag:开始拖拽,对应2)

    • OnDrag:拖拽中,对应3)

    • OnEndDrag:结束拖拽,对应4)

    • OnPointExit:清空选中元素,恢复默认值

    具体代码比较多这里不再展开说了,这里庆幸的是,Inventory Pro对拖拽的逻辑进行了封装,在InventoryUIItemWrapper中接口实现函数中,主要做的参数舒适化,主要关于UI的逻辑代码封装在了InventoryUIUtility类中,

    以下是主要接口实现函数的代码

    public virtual void OnBeginDrag(PointerEventData eventData)
            {
                if (itemCollection == null)
                    return;
    
                if (item != null && eventData.button == PointerEventData.InputButton.Left && itemCollection.canDragInCollection)
                {
                    // Create a copy
                    var copy = GameObject.Instantiate<InventoryUIItemWrapper>(this);
                    copy.index = index;
                    copy.itemCollection = itemCollection;
    
                    var copyComp = copy.GetComponent<RectTransform>();
                    copyComp.SetParent(InventorySettingsManager.instance.guiRoot);
                    copyComp.transform.localPosition = new Vector3(copyComp.transform.localPosition.x, copyComp.transform.localPosition.y, 0.0f);
                    copyComp.sizeDelta = GetComponent<RectTransform>().sizeDelta;
    
                    InventoryUIUtility.BeginDrag(copy, (uint)copy.index, itemCollection, eventData); // Make sure they're the same size, copy doesn't handle this.
                }
            }
    
            public virtual void OnDrag(PointerEventData eventData)
            {
                if (item != null && itemCollection != null && itemCollection.canDragInCollection) // Can only drag existing item
                    InventoryUIUtility.Drag(this, index, itemCollection, eventData);
            }
    
            public virtual void OnEndDrag(PointerEventData eventData)
            {
                if (item != null && itemCollection != null && itemCollection.canDragInCollection)
                {
                    var lookup = InventoryUIUtility.EndDrag(this, index, itemCollection, eventData);
    
                    // Didn't end on a button or used wrong key.
                    if (lookup == null)
                        return;
    
                    if (lookup.endOnButton)
                    {
                        // Place on a slot
                        lookup.startItemCollection.SwapOrMerge((uint)lookup.startIndex, lookup.endItemCollection, (uint)lookup.endIndex);
                    }
                    else if (lookup.startItemCollection.useReferences)
                    {
                        lookup.startItemCollection.SetItem((uint)lookup.startIndex, null);
                        lookup.startItemCollection[lookup.startIndex].Repaint();
                    }
                    else if(InventoryUIUtility.clickedUIElement == false)
                    {
                        TriggerDrop();
                    }
                }
            }
    

    以下是InventoryUIUtility类封装的静态函数代码

    public static InventoryUIDragLookup BeginDrag(InventoryUIItemWrapper toDrag, uint startIndex, ItemCollectionBase collection, PointerEventData eventData)
            {
                if (draggingItem != null)
                {
                    Debug.LogWarning("Item still attached to cursor, can only drag one item at a time", draggingItem.gameObject);
                    return null; // Can only drag one item at a time
                }
    
                if (eventData.button != PointerEventData.InputButton.Left)
                    return null;
    
    
                draggingItem = toDrag;
                //draggingButtonCollection = collection;
    
                // Canvas group allows object to ignore raycasts.
                CanvasGroup group = draggingItem.gameObject.GetComponent<CanvasGroup>();
                if(group == null)
                    group = draggingItem.gameObject.AddComponent<CanvasGroup>();
    
                group.blocksRaycasts = false; // Allows rays to go through so we can hover over the empty slots.
                group.interactable = false;
    
                var lookup = new InventoryUIDragLookup();
                lookup.startIndex = (int)startIndex;
                lookup.startItemCollection = collection;
    
                return lookup;
            }
    
            public static void Drag(InventoryUIItemWrapper toDrag, uint startSlot, ItemCollectionBase handler, PointerEventData eventData)
            {
                if(eventData.button == PointerEventData.InputButton.Left)
                    draggingItem.transform.position = new Vector3(eventData.position.x, eventData.position.y, 0.0f);
            }
    
            public static InventoryUIDragLookup EndDrag(InventoryUIItemWrapper toDrag, uint startSlot, ItemCollectionBase handler, PointerEventData eventData)
            {
                if(eventData.button == PointerEventData.InputButton.Left)
                {
                    var lookup = new InventoryUIDragLookup();
                    lookup.startIndex = (int)draggingItem.index;
                    lookup.startItemCollection = draggingItem.itemCollection;
    
                    if (hoveringItem != null)
                    {
                        lookup.endIndex = (int)hoveringItem.index;
                        lookup.endItemCollection = hoveringItem.itemCollection;
                    }
    
                    Object.Destroy(draggingItem.gameObject); // No longer need it
    
                    draggingItem = null;
                    //draggingButtonCollection = null;
    
                    return lookup;
                }
    
                return null;
            }
    
            /// <summary>
            /// When the cursor enters an item
            /// </summary>
            public static void EnterItem(InventoryUIItemWrapper item, uint slot, ItemCollectionBase handler, PointerEventData eventData)
            {
                hoveringItem = item;
                //hoveringItemCollection = handler;
            }
    
            /// <summary>
            /// When the cursor exits an item
            /// </summary>
            /// <param name="item"></param>
            /// <param name="slot">The slot is the IButtonHandler index not the inventory index.</param>
            /// <param name="handler"></param>
            /// <param name="eventData"></param>
            public static void ExitItem(InventoryUIItemWrapper item, uint slot, ItemCollectionBase handler, PointerEventData eventData)
            {
                hoveringItem = null;
                //hoveringItemCollection = null;
            }
    

    A2 拖拽的事件的Drag是如何确定下方的UI元素的?

    这里DragDrop中的元素由 OnPointerEnter 来触发获得(有点像Mouse Move事件),具体保存在InventoryUIUtility类的一个静态变量中

    public static InventoryUIItemWrapper hoveringItem { get; private set; }
    

    A3 拖拽后的逻辑操作,应该由哪个类来承接?

    也就是Drop的操作由谁来完成,首先回忆下职责InventoryUIItemWrapper类负责事件的触发,InventoryUIUtility类负责UI相关的逻辑(选中,射线,坐标系统)

    再看一遍OnDragEnd函数,具体的Drop逻辑是有Drop的后查找的lookup集合类(装备格集合基类ItemCollectionBase)来处理的,具体又有交换\合并两个逻辑,触发代码如下:

    if (lookup.endOnButton)
    {
         // Place on a slot
         lookup.startItemCollection.SwapOrMerge((uint)lookup.startIndex, lookup.endItemCollection, (uint)lookup.endIndex);
    }
    

    当然还有一种复杂的逻辑,就是扔掉物品的操作,这个是有具体的Item装备模型类(InventoryItemBase)来处理,核心代码在TriggerDrop方法中来调用,具体如下:

    public override void TriggerDrop(bool useRaycast = true)
            {
                if (item == null || itemCollection.canDropFromCollection == false)
                    return;
    
                if(item.isDroppable == false)
                {
                    InventoryManager.instance.lang.itemCannotBeDropped.Show(item.name, item.description);
                    return;
                }
    
                Vector3 dropPosition = InventorySettingsManager.instance.playerObject.transform.position;
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                RaycastHit hit;
                if (Physics.Raycast(ray, out hit, InventorySettingsManager.instance.maxDropDistance,
                    InventorySettingsManager.instance.layersWhenDropping))
                {
                    dropPosition = hit.point;
                }
                else
                {
                    return; // Couldn't drop item
                }
    
                var s = InventorySettingsManager.instance;
                if (useRaycast && s.showConfirmationDialogWhenDroppingItem && s.showConfirmationDialogMinRarity.ID <= item.rarity.ID)
                {
                    // Not on a button, drop it
                    var tempItem = item; // Capture list stuff
                    var msg = InventoryManager.instance.lang.confirmationDialogDrop;
                    s.confirmationDialog.ShowDialog(msg.title, msg.message, s.defaultDialogPositiveButtonText, s.defaultDialogNegativeButtonText, item,
                        (dialog) =>
                        {
                            ItemCollectionBase startCollection = tempItem.itemCollection;
                            uint startIndex = tempItem.index;
    
                            var d = tempItem.Drop(dropPosition);
                            if (d != null)
                            {
                                startCollection[startIndex].Repaint();
                            }
                        },
                        (dialog) =>
                        {
                            //Debug.Log("No clicked");
                        });
                }
                else
                {
                    var d = item.Drop(dropPosition);
                    if (d != null)
                    {
                        Repaint();
                    }
                }
            }
    

    A4 窗口与窗口之间的拖拽,既有Drag又有Drop,如何更加合理的解决这个问题?

    这个问题比较绕,其实也涉及到了问题2,其实无论怎么拖拽也就是两个东西,一个是被拖拽的物体from,一个是要放的地方to,这里其实都是窗口中的格子,只要有了这两个格子类也就确定了from和to的容器,比较特殊的一种情况也就是 from和to两个容器相等,也就是同窗口拖拽了,具体这些对象InventoryUIUtilty类中都做了封装,还是很赞的具体代码如下:

    public class InventoryUIDragLookup
            {
                public int startIndex = -1;
                public ItemCollectionBase startItemCollection;
    
                public int endIndex = -1;
                public ItemCollectionBase endItemCollection;
    
                public bool endOnButton
                {
                    get
                    {
                        return endItemCollection != null;
                    }
                }
            }
    
    
            #region Variables 
    
            private static InventoryUIItemWrapper draggingItem;
            public static InventoryUIItemWrapper hoveringItem { get; private set; }
            public static bool isDraggingItem
            {
                get
                {
                    return draggingItem != null;
                }
            }
    
            public static bool clickedUIElement
            {
                get
                {
                    return EventSystem.current.IsPointerOverGameObject();
                }
            }
    
    
            public static bool isFocusedOnInput
            {
                get
                {
                    if (EventSystem.current.currentSelectedGameObject != null)
                        if (EventSystem.current.currentSelectedGameObject.GetComponent<UnityEngine.UI.InputField>() != null)
                            return true;
    
                    return false;
                }
            }
    
            #endregion
    

    复杂的物品拖拽逻辑总结完毕,再次向我们印证了,从helloworld到现实是多么的困难,实际的情况可能更复杂比如要加入动画效果,要做网络延时验证,数据同步等等吧

    Demo4:通用窗口的具体实现物品有消耗扇形显示功能

    功能点:

    • 1、实现了两个窗口,通过点击键盘I来,打开或者关闭窗口也就是Toggle功能

    • 2、装备窗口中的物品栏空格数量动态生成可控,可以在属性窗口手动配置

    • 3、窗口具有拖拽功能

    • 4、窗口物品具有拖拽,及窗口间拖拽

    • 5、可以在窗口使用物品的功能,物品有消耗扇形显示功能

    • 6、通用窗口的类体系结构

    这里开动之前给自己提几个问题:

    • 1、UGui原生实现使用物品扇形消耗效果(即冷却实现)是如何实现的?

    先看看最终效果
    这里写图片描述

    如上图,我的技能不是像LOL一样每个技能对应一个按键,而是,先选择一个技能,然后按下某一个键释放 [类似于Button Group]
    技能后方有一个灰色的蒙版,有蒙版的技能即为当前选中的技能。
    每个技能CD可以在脚本中自定义。
    倒计时蒙版与CD会同步。
    怎么实现先选择一个技能然后按下键来释放技能不是本教程的目标。
    本教程只简单说明怎么实现技能CD。
    我们在实现效果之前先简单了解一下UGUI的Image组件。

    这里写图片描述
    重点介绍一下Image Type 【显示模式】

    Image组件中,Image Type(显示方式)有Simple、Sliced、Tiled、Filled四种。
    下面一一介绍。

    *Simple【普通】
    *此显示模式下,Sprite将直接显示在控件中。如果大小不一致,将通过拉伸来填充控件。
    *如果preserve Aspect勾选,图片将保持长宽比
    *
    *Sliced【切片】
    此显示模式下,Sprite将被视为9个切片[33]组成,图片控件只显示中间切片的边缘。
    *如果Fill Center勾选,将显示完整切片
    *
    *Tiled【平铺】
    *此显示模式下,sprite尺寸不变
    *[自行类比Windows桌面壁纸填充方式中的平铺]
    *
    *Filled【填充】
    *显示模式类似于Simple,但是可以有多个选项来选择展示“从无到有”的变化
    *填充方式由Fill-Method属性决定。
    *本帖不一一说明各个显示模式。如果你感兴趣,请自行查找资料。

    在脚本中,我们需要几个重要的变量来关联各个物体。

    比如你需要一个这样的结构体或者类:

    [System.Serializable]
    public struct skillSprite
    {
        public Sprite skill_On;
        public Sprite skill_Off;
        public float skillTimer;
        public Text timerText;
        public Image timerMask;
        public GameObject tipPanel;
        [HideInInspector]
        public bool isOn;   //冷却时间到
        [HideInInspector]
        public bool isOpen; //拥有此技能
        [HideInInspector]
        public bool isPressDown;   //是否使用了技能
    }
    

    要实现技能效果,我们会在技能面板中使用Filled模式来实现倒计时效果。

    我们在Hierarchy面板中建立这样的父子关系
    这里写图片描述
    控制代码如下:

    if(skillSprites[index].isOpen)  //如果已经获得该技能执行
    {
                //Debug.Log("index = " + index);
     
                if (skillSprites[index].isPressDown)    //当技能被激活时执行
                {
                    skillSprites[index].isOn = false;   //冷却时间重置
                    skillSprites[index].timerMask.enabled = true;   //CD蒙版激活
                    skillSprites[index].skillTimer -= Time.deltaTime;   //CD时间开始倒计时
                    //startTimer[index]为该技能的冷却时间
                    //因为在Image组件fillAmount的值范围是[0,1]的float值,所以这里做一个计算转化为百分比
                    skillSprites[index].timerMask.fillAmount -= (Time.deltaTime / startTimer[index]);   //更新蒙版状态  
                    skillSprites[index].timerText.text = skillSprites[index].skillTimer.ToString("F1"); //更新时间显示
                }
    }
    

    这样你可以继续完善脚本逻辑来实现效果。
    剩下的部分比较简单啦。我就不在这里再做说明了。
    这里写图片描述
    这里写图片描述
    OK,搞定

    • 2、装备格子是如何与1所提到的方法接合在一起的?

    这里回忆下装备格子是用InventoryUItemWrapper这个UI类实现的,所以答案就在这个类里,但使用这个类并非易事,它是用ItemCollectionBase类(集合容器)动态生成的,在前篇讲过这里再温习一下,这里我们发现如果没有值得话,它取的InventorySettingsManager.itemButtonPrefab,也就是个装备格子预设

    protected virtual void FillUI()
            {
                if (manuallyDefineCollection == false)
                {
                    items = new InventoryUIItemWrapperBase[initialCollectionSize];
    
                    // Fill the container on startup, can add / remove later on
                    for (uint i = 0; i < initialCollectionSize; i++)
                    {
                        items[i] = CreateUIItem<InventoryUIItemWrapper>(i, itemButtonPrefab != null ? itemButtonPrefab : InventorySettingsManager.instance.itemButtonPrefab);
                    }
                }
                else
                {
                    for (uint i = 0; i < items.Length; i++)
                    {
                        items[i].itemCollection = this;
                        items[i].index = i;
                    }
                }
            }
    
            protected T CreateUIItem<T>(uint i, GameObject prefab) where T : InventoryUIItemWrapperBase
            {
                T item = GameObject.Instantiate<GameObject>(prefab).GetComponent<T>();
                item.transform.SetParent(container);
                item.transform.localPosition = new Vector3(item.transform.localPosition.x, item.transform.localPosition.y, 0.0f);
                item.itemCollection = this;
                item.transform.localScale = Vector3.one;
                item.index = i;
            
                return item;
            }
    

    回忆一下这个配置是一个必填配置,必须在Setting中进行设置,
    这里写图片描述
    找到该预设,找到真相
    这里写图片描述
    这里写图片描述
    看的出来这个预设绑定了InventoryUIItemWrapper类,其公共的Field也以此列了出来分别是Amout Text物品数量,Item Name物品名称(None),Icon(Image)这就是物品的图标了比如苹果,Cooldown Image 就是图片表面的遮罩层,用来做雷达效果的。

    • 3、装备格子是如何与苹果等可以吃的动态物品接合在一起的?

    从上面的图片我们也看的出来,其实默认的Icon应该是装备格子的背景(黑底),它是如何变成苹果梨,或者刀剑的呢?这里比较简单的线索就是通过拾取地上的包裹,然后在背包里多了一个物品(这个过程不表述了比较复杂,留在以后专门分析),顺藤摸瓜,最终还是要加入到背包里面,这样我们去看看ItemCollectionBase类中的AddItem方法,这里这个方法本身很复杂主要是有类似20个血瓶1打东西放置的时候需要重新计算格子什么的比较麻烦,核心的函数是SetItem

    /// <summary>
            /// This function can be overridden to add custom behavior whenever an object is placed in the inventory.
            /// This happens when 2 items are swapped, items are merged, anytime an object is put in a slot.
            /// <b>Does not handle repainting</b>
            /// </summary>
            /// <param name="slot"></param>
            /// <param name="item"></param>
            /// <returns>Returns true if the item was set, false if not.</returns>
            public virtual bool SetItem(uint slot, InventoryItemBase item)
            {
                if (CanSetItem(slot, item) == false)
                    return false;
    
                // _item ugly work around, but no other way to keep it safe...
                items[slot].item = item;
                return true;
            }
    

    这里我们还记得InventoryUIItemWrapper是Item的包装,所以这里设置了其的Item为新增的Item,顺着这个思路剩下相关的就是绘制部分了,再去看看InventoryUIItemWrapper的绘制部分看看是如何显示的以及冷却效果是如何实现的

    public override void Repaint()
            {
                if (item != null)
                {
                    if (amountText != null)
                    {
                        // Only show when we have more then 1 item.
                        if (item.currentStackSize > 1)
                            amountText.text = item.currentStackSize.ToString();
                        else
                            amountText.text = string.Empty;
                    }
    
                    if (itemName != null)
                        itemName.text = item.name;
    
                    if(icon != null)
                        icon.sprite = item.icon;
                }
                else
                {
                    if (amountText != null)
                        amountText.text = string.Empty;
    
                    if (itemName != null)
                        itemName.text = string.Empty;
    
                    if(icon != null)
                        icon.sprite = startIcon != null ? startIcon : InventorySettingsManager.instance.defaultSlotIcon;
                }
    
                //RepaintCooldown(); // Already called by update loop
            }
    

    icon.sprite = item.icon; 这一行代码我们看到了,其实icon这个Image Field 对应Item.icon,而且是一个sprite对象(原来sprite是Image的一个部分),再看下Update Loop的代码,只是在每一帧调用了
    RepaintCooldown(),也就是执行冷却刷新,具体代码如下:

    public virtual void RepaintCooldown()
            {
                if (cooldownImage == null)
                    return;
    
                if (item != null)
                {
                    if(item.isInCooldown)
                    {
                        cooldownImage.fillAmount = 1.0f - item.cooldownFactor;
                        return;
                    }
                }
    
                // To avoid GC
                if (cooldownImage.fillAmount != 0.0f)
                    cooldownImage.fillAmount = 0.0f;
            }
    

    当然这里有控制cooldown的逻辑,isInCooldown是一个属性逻辑都在里面,包括同类型物品使用的冷却控制(有点复杂,这里先不表了)

    • 4、物品是如何被触发使用的?

    触发使用一定是在点击的时候触发的,接合UGui的事件机制,Inventory Pro 分别在OnPointerDown和OnPointerUP中进行了实现,有一些触控相关的判断见一下源码

    public virtual void OnPointerDown(PointerEventData eventData)
            {
                if (itemCollection == null)
                    return;
    
                pointerDownOnUIElement = InventoryUIUtility.clickedUIElement;
                if (pointerDownOnUIElement == false)
                    return;
    
                if (InventorySettingsManager.instance.useContextMenu && (eventData.button == InventorySettingsManager.instance.triggerContextMenuButton || Application.isMobilePlatform))
                {
                    if (item != null)
                        TriggerContextMenu();
    
                    return;
                }
    
                if (InventorySettingsManager.instance.mobileUnstackItemKey == MobileUIActions.SingleTap)
                {
                    TriggerUnstack();
                    return;
                }
                else if(InventorySettingsManager.instance.mobileUseItemButton == MobileUIActions.SingleTap)
                {
                    TriggerUse();
                    return;
                }
    
                if (item != null && pressing == false && Time.timeSinceLevelLoad - InventorySettingsManager.instance.mobileDoubleTapTime < lastDownTime)
                {
                    // Did double tap
                    if (InventorySettingsManager.instance.mobileUnstackItemKey == MobileUIActions.DoubleTap)
                    {
                        TriggerUnstack();
                        return;
                    }
                    else if(InventorySettingsManager.instance.mobileUseItemButton == MobileUIActions.DoubleTap)
                    {
                        TriggerUse();
                        return;
                    }
                }
    
                lastDownTime = Time.timeSinceLevelLoad;
                pressing = true;
            }
    

    物品的具体使用在TriggerUse() 实现

    public override void TriggerUse()
            {
                if (item == null)
                    return;
    
                if (itemCollection.canUseFromCollection == false)
                    return;
    
                int used = item.Use();
                if (used >= 0)
                {
                    Repaint();
                }
            }
    

    这里最终物品的使用时调用的Item Model中的Use()来实现的,这个应该是一种基类方法,需要特定的子类Item来实现,具体实现还要和容器有一定关系,比较复杂本文不表,有机会日后再展开,反正使用后调用了Repaint()方法也就是在使用完毕物品后装备格子进行了重绘刷新操作。

    Demo5:Inventory Pro 装备拾取的实现

    效果图

    1、运行相关的例子,场景中出现4个矩形,这4个矩形是用来模拟物品掉落的包裹,移动Player靠近物品
    这里写图片描述
    2、使用鼠标点击物品正方体,点击I键打开包裹,这里看到3个掉落包裹正方体已经点没有了,相应的背包里多出三个苹果,至此例子演示完毕
    这里写图片描述

    插件使用

    使用Inventory Pro进行装备的拾取,有很简单的例子

    1、点击菜单Tool,InventorySystem,Main editor 打开自定义Editor
    这里写图片描述
    2、在Main manager对话框中点击Items editor选项卡,选中Item editor 点击Create Item 绿色按钮,会弹出左侧的选择子对话框,其中列出了可以创建的Item种类,第一项就是消耗品,当然还有很多别的东西,这里先试验最简单的消耗品
    这里写图片描述
    3、点击创建消耗品以后会出来一个Item的详情界面,这里需要添加Item的Name,Icon,Behavier里有类型,冷却时间,价格,是否可以叠放,是否可以丢弃等等属性吧,同时Editor会创建出相应的物品prefab,直接拖拽到场景中即可
    这里写图片描述

    问题

    1、地上的包裹(Item容器)是如何产生的?

    2、地上的包裹是如何对应Item model的,且之间的关系是什么?

    3、拾取的过程是怎么样的?

    答案

    1、地上的包裹(Item容器)是如何产生的?

    A1、这里例子里面是通过Unity3d Editor扩展进行添加地上的包裹的且是Cude,其中使用了动态生成预设的技术,这里涉及到很多关于Editor扩展的知识,基本与本章主题无关,Inventory Pro的作者也只是通过例子进行了展示(我想如果是实际游戏可能需要动态生成,比如生长点,怪物掉落什么的),这里列出一个动态生成Prefab的核心代码,也就是类似选择生成消耗品后点击的事件的处理函数,代码如下:

    protected override void CreateNewItem()
            {
                var picker = EditorWindow.GetWindow<InventoryItemTypePicker>(true);
                picker.Show(InventoryEditorUtil.selectedDatabase);
    
                picker.OnPickObject += (type) =>
                {
                    string prefabPath = EditorPrefs.GetString("InventorySystem_ItemPrefabPath") + "/item_" + System.DateTime.Now.ToFileTimeUtc() + "_PFB.prefab";
    
                    var obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
                    var prefab = PrefabUtility.CreatePrefab(prefabPath, obj);
    
                    AssetDatabase.SetLabels(prefab, new string[] { "InventoryItemPrefab" });
    
                    var comp = (InventoryItemBase)prefab.AddComponent(type);
                    comp.ID = crudList.Count == 0 ? 0 : crudList[crudList.Count - 1].ID + 1;
                    Object.DestroyImmediate(obj);
    
                    AddItem(comp, true);
                };
            }
    

    2、地方的包裹是如何对应Item model的,且之间的关系是什么?
    我们看到1中的代码创建好刚体,碰撞后的正方体后

    var comp = (InventoryItemBase)prefab.AddComponent(type);

    通过AddComponent添加了用户选择的Type,通过类型转换(InventoryItemBase)我们知道这个类型是Item类型。

    然后我们再通过Unity3d 可视化环境来验证一下
    这里写图片描述
    通过选中预设我们看到其绑定的相关插件有碰撞和刚体属性,最后绑定的脚本是Consumable Inventory Item (Script),这样也验证了上面代码加入的Type,也就是在类选择所对应的InventoryItemBase 类型
    这里写图片描述
    这里需要注意的是Object Triggerer Item脚本是如何绑定到对象上的,这块是一个小技巧,我找了半天才发现的

    /// <summary>
    /// The base item of all the inventory items, contains some default behaviour for items, which can (almost) all be overriden.
    /// </summary>
    [RequireComponent(typeof(ObjectTriggererItem))]
    public partial class InventoryItemBase : MonoBehaviour
    

    用RequireComponent这个特性就可以不用手动进行相关插件的绑定了。
    这里写图片描述
    上图是Consumable Inventory Item(script)的public field详情了,是不是似曾相识,就是和Item Editor工具创建Prefab的时候是一样的。

    到此第二个问题回答完毕。

    3、拾取的过程是怎么样的?
    在正式回答前,我先yy下,既然拾取必须是双向,而且应该是触发的(当点击鼠标触发),然后Play接收到触发的调用进行拾取,最后清理空背包(立方体消失),下面去看看代码是否是这么实现的。

    触发器这里在2的答案里已经有了,看了下做法出奇的简单,可能这就是框架的力量吧,下面贴出源码

    /// <summary>
        /// Used to trigger item pickup, modify the settings in ShowObjectTriggerer.
        /// </summary>
        [AddComponentMenu("InventorySystem/Triggers/Object triggerer item")]
        [RequireComponent(typeof(InventoryItemBase))]
        [RequireComponent(typeof(Rigidbody))]
        public partial class ObjectTriggererItem : MonoBehaviour
        {
    
            public InventoryItemBase item { get; protected set; }
    
            public bool inRange
            {
                get
                {
                    return Vector3.Distance(InventorySettingsManager.instance.playerObject.transform.position, transform.position) < InventorySettingsManager.instance.useObjectDistance;
                }
            }
    
    
            public void Awake()
            {
                item = GetComponent<InventoryItemBase>();            
            }
    
            public virtual void OnMouseDown()
            {
                if (ShowObjectTriggerer.instance != null && ShowObjectTriggerer.instance.itemTriggerMouseClick && InventoryUIUtility.clickedUIElement == false)
                {
                    if (inRange)
                        Use();
                    else
                    {
                        InventoryManager.instance.lang.itemCannotBePickedUpToFarAway.Show(item.name, item.description);
                    }
                }
            }
    
            protected virtual void Use()
            {
                item.PickupItem();
            }
        }
    

    这里我们很容易看到了OnMouseDown触发的物品拾取逻辑,直接调用绑定的InventoryBaseItem的PickupItem()拾取函数即可,这里再次重温下自动绑定的威力

    [RequireComponent(typeof(InventoryItemBase))]

    当需要使用的时候记得在Awake()函数中设置相关属性即可

    public void Awake()
    {
        item = GetComponent<InventoryItemBase>();            
    }
    
    /// <summary>
            /// Pickups the item and stores it in the Inventory.
            /// </summary>
            /// <returns>Returns 0 if item was stored, -1 if not, -2 for some other unknown reason.</returns>
            public virtual bool PickupItem(bool addToInventory = true)
            {
                if(addToInventory)
                    return InventoryManager.AddItem(this);
            
                return true;
            }
    

    上面是InventoryItemBase中的PickupItem方法,只用调用InventoryMannager类的静态方法AddItem方法即可,就是这么简单

    /// <summary>
            /// Add an item to an inventory.
            /// </summary>
            /// <param name="item">The item to add</param>
            /// <returns></returns>
            public static bool AddItem(InventoryItemBase item, bool repaint = true)
            {
                if (CanAddItem(item) == false)
                {
                    instance.lang.collectionFull.Show(item.name, item.description, instance.inventory.collectionName);
                    return false;
                }
    
                 All items fit in 1 collection
                //if (item.currentStackSize <= item.maxStackSize)
                //    return best.collection.AddItem(item, repaint);
    
                // Not all items fit in 1 collection, divide them, grab best collection after each iteration
                // Keep going until stack is divided over collections.
                while (item.currentStackSize > 0)
                {
                    var bestCollection = instance.GetBestLootCollectionForItem(item, false).collection;
                    uint canStoreInCollection = bestCollection.CanAddItemCount(item);
    
                    var copy = GameObject.Instantiate<InventoryItemBase>(item);
                    copy.currentStackSize = (uint)Mathf.Min(Mathf.Min(item.currentStackSize, item.maxStackSize), canStoreInCollection);
                    bestCollection.AddItem(copy);
    
                    item.currentStackSize -= copy.currentStackSize;
                    //item.currentStackSize = (uint)Mathf.Max(item.currentStackSize, 0); // Make sure it's positive
                }
    
                Destroy(item.gameObject); // Item is divided over collections, no longer need it.
    
                return true;
            }
    

    InventoryMannager中的AddItem,代码看似简单,却有四个逻辑,是否可以放置,选择最佳容器,叠放逻辑,销毁。

    Demo6:Inventory Pro 实现UGUI通用对话框

    所谓通用对话框,如果是自己实现的话有以下几点需要解决,窗体显示控制,窗体UI布局,窗体文字显示,窗体事件回调,窗体显示动画控制,窗体显示声音控制,窗体与其他窗体的关系,功能虽然小涉及的方面和知识却不少,自己做真的很不容易,所以别再自己造轮子了。
    这里写图片描述

    插件实现的效果

    简单的确认对话框提示

    当扔物品的时候会提示是否确认对话框

    这里写图片描述
    稍微复杂一些的购买物品对话框

    当购买物品时会显示出一个购买的物品,物品数量金额的对话框
    这里写图片描述
    简单确认对话框的使用

    1、使用UGUI来设计一个自己使用的对话框,基本几个元素Title,description ,two buttons;

    2、给对话框绑定Draggable Window(Script)使其具有拖拽功能

    3、添加Animator,定义对话框显示的时候具有动画效果

    4、添加UI Windows(script)使其具有打开关闭,声音,动画的效果

    5、Confirmation Dialog(script)使其具有事件回调,model对话框的属性,文字绑定等对话框固有的属性

    至此简单的对话框就做好了,这里我们充分见识了绑定技术、组件技术、UI解耦和框架的强大威力
    这里写图片描述
    这里写图片描述
    复杂对话框的使用

    这里只要知道Item Int Val Dialog(scirpt)其实是ConfirmDialog类的一个子类,剩下的东西就很自然了,这里不详细展开了。
    这里写图片描述

    分析

    功能需求确定了,如何实现这些功能可能就需要用到一些模式,以及一些经验了,先看一下类图
    这里写图片描述

    根据前一节的脑图,类图我们逐个分析,InventoryUIDialogBase 是一个抽象类,也是与UI进行绑定的主体,其没有一个无用的属性,这里重点关注几个字段和属性,UIWindow类是通用的窗口显示和动画控制组件,InventoryMessage是字符串Message的封装类。

    1)窗体UI布局

    UI布局是通过Unity3d UGUI拖拽的方式设计上去的,这个很简单,首先做到了UI分离

    2)窗体文字显示

    窗体文字的显示首先是通过后台与UI做的绑定,这里使用Unity3d的组件设计时绑定技术(这里做过WPF的同学有是否有印象MVVM中的绑定),这里关键是文字信息,实际发现其实Dialog类并不关心显示的什么string,而是Inventory Pro提供的(类图中的Message类)一层封装后得到的结果,这里为什么要单独拿出来实际是为了做国际化以及一些文字性的扩展,比如颜色,字体显示的方案。

    InventoryLangDataBase类对于所有的消息体文字进行了集中处理,而且本身也是Asset,这里有两种好处一种就是可以集中管理,一种就是为国际化文字。
    这里写图片描述
    因为Unity3d UGUI可以做文字颜色和字体的格式化操作,这里完全可以扩展添加有颜色和字体大小的文字重载
    这里写图片描述

    3)窗体显示控制,窗体显示动画控制,窗体显示声音控制

    窗体显示的控制,完全利用Unity3d平台的组件化功能,通过UIWindow专门拿出来控制,这里看到UIWinow类是必须加载Animator动画类的
    这里写图片描述
    窗体的动画控制,由主体DialogBase进行设计时的动画效果绑定,由UIWindow类在控制显示和关闭时进行动画的Play,这里还用到了协程
    这里写图片描述
    窗体显示声音控制,由全局类静态方法 InventoryUIUtility.AudioPlayOneShot 来播放即可

    4)窗体与其他窗体的关系

    这个功能类似于网页中的遮罩或者winform里的模态(ModelDialog)对话框,这里没有现成的东西可以使用只能自己写了,这里如何关闭UGUI的事件处理主要是通过CanvasGroup这个插件来控制

    这里写图片描述

    5) 窗体事件回调

    窗体中的事件回调交给了Dialog子类来处理,具体是在重载的ShowDialog方法中添加了委托的事件回调函数,然后通过代码绑定的方式(这里是onClick.AddListener,而不是UI手动可视化绑定)进行了按钮事件的绑定,这里有很大的灵活性。我比较喜欢这种通过代码定义显示委托的方式,来完成事件的回调(c++系可能叫做函数指针),同比匿名委托,泛型委托(Action或者Func),Lambda表达式,代码可读性更强

    这里写图片描述

    其它

    这里留了一个小疑问,对话框的触发显示是如何实现的,我们的(MessageBox.Show)在哪里呢?

    看过前面的文章的同学应该知道,Inevntory Pro有一个全局setting类,需要进行一些配置,其中就需要窗体元素与SettingManger脚本进行绑定,而SettingManger是一个单列全局类
    这里写图片描述
    最后是如何显示对话框的代码了,看到ShowDialog方法了吗,两个按钮的事件回调函数 Lambda表达式特别显眼
    这里写图片描述

    写在最后

    分析总结完毕后有一些想法

    1、好的框架使开发变得的easy,扩展很方便,通过以上的分析和例子看的出来很容易就能扩展出来一些简单的类似Confirm对话框,而且是对修改封闭,对新增开放的;

    2、一个司空见惯的小功能,如果做好了完全可以覆盖到Unity3d的许多知识,剩下的只是不断进行这样的重复,重建你的神经网络即可,总有一天Unity3d的技术就这样印在你的大脑之中;

    3、如果你真的看懂了本文,分析一下其实所有的UI系统都是相通的只是API和使用的技术不同而已,只是有些API封装的死,有些封装的松散一些。换句话说如果你自己在某种UI体系中完成一种自己的实现,换到另一个UI体系一样可以实现的;

    4、微软体系如Winform过渡的封装是否是好事情?有些时候是好事情,有些时候就未必。根据手上的资源合理的选择技术才是根本;

    5、关于使用轮子和造轮子的纠结,这也是一组矛盾,不造轮子就不能深刻的体会技术,造轮子需要大量的时间可造出来未必有已经造好的轮子设计的好,你会选择哪一种呢?

    核心代码

    UIWindow

    using System;
    using UnityEngine;
    using System.Collections;
    using UnityEngine.EventSystems;
    using System.Collections.Generic;
    
    namespace Devdog.InventorySystem
    {
        /// <summary>
        /// Any window that you want to hide or show through key combination or a helper (UIShowWindow for example)
        /// </summary>
        [RequireComponent(typeof(Animator))]
        [AddComponentMenu("InventorySystem/UI Helpers/UIWindow")]
        public partial class UIWindow : MonoBehaviour
        {
    
            public delegate void WindowShow();
            public delegate void WindowHide();
    
    
            #region Variables 
    
            /// <summary>
            /// Should the window be hidden when the game starts?
            /// </summary>
            [Header("Behavior")]
            public bool hideOnStart = true;
    
            /// <summary>
            /// Keys to toggle this window
            /// </summary>
            public KeyCode[] keyCombination;
    
            /// <summary>
            /// The animation played when showing the window, if null the item will be shown without animation.
            /// </summary>
            [Header("Audio & Visuals")]
            public AnimationClip showAnimation;
    
            /// <summary>
            /// The animation played when hiding the window, if null the item will be hidden without animation. 
            /// </summary>
            public AnimationClip hideAnimation;
    
            public AudioClip showAudioClip;
            public AudioClip hideAudioClip;
    
    
            /// <summary>
            /// The animator in case the user wants to play an animation.
            /// </summary>
            public Animator animator { get; set; }
            protected RectTransform rectTransform { get; set; }
    
            [NonSerialized]
            private bool _isVisible = false;
            /// <summary>
            /// Is the window visible or not? Used for toggling.
            /// </summary>
            public bool isVisible
            {
                get
                {
                    return _isVisible;
                }
                protected set
                {
                    _isVisible = value;
                }
            }
    
            private IEnumerator showCoroutine;
            private IEnumerator hideCoroutine;
    
    
            /// <summary>
            /// All the pages of this window
            /// </summary>
            [HideInInspector]
            private List<UIWindowPage> pages = new List<UIWindowPage>();
    
            public UIWindowPage defaultPage
            {
                get;
                private set;
            }
    
            #endregion
    
            #region Events
    
            /// <summary>
            /// Event is fired when the window is hidden.
            /// </summary>
            public event WindowHide OnHide;
    
            /// <summary>
            /// Event is fired when the window becomes visible.
            /// </summary>
            public event WindowShow OnShow;
    
            #endregion
    
    
            public void AddPage(UIWindowPage page)
            {
                pages.Add(page);
    
                if (page.isDefaultPage)
                    defaultPage = page;
            }
    
            public void RemovePage(UIWindowPage page)
            {
                pages.Remove(page);
            }
    
    
            public virtual void Awake()
            {
                animator = GetComponent<Animator>();
                if (animator == null)
                    animator = gameObject.AddComponent<Animator>();
    
                rectTransform = GetComponent<RectTransform>();
    
                if (hideOnStart)
                    HideFirst();
                else
                {
                    isVisible = true;
                }
            }
    
            public virtual void Update()
            {
                if (keyCombination.Length == 0)
                    return;
    
                bool allDown = true;
                foreach (var key in keyCombination)
                {
                    if (Input.GetKeyDown(key) == false)
                    {
                        allDown = false;
                    }
                }
    
                if (allDown)
                    Toggle();
     
            }
    
            #region Usefull UI reflection functions 
    
            /// <summary>
            /// One of our children pages has been shown
            /// </summary>
            public void NotifyPageShown(UIWindowPage page)
            {
                foreach (var item in pages)
                {
                    if (item.isVisible && item != page)
                        item.Hide();
                }
            }
    
            protected virtual void SetChildrenActive(bool active)
            {
                foreach (Transform t in transform)
                {
                    t.gameObject.SetActive(active);
                }
    
                var img = gameObject.GetComponent<UnityEngine.UI.Image>();
                if(img != null)
                    img.enabled = active;
            }
    
            public virtual void Toggle()
            {
                if (isVisible)
                    Hide();
                else
                    Show();
            }
    
            public virtual void Show()
            {
                if (isVisible)
                    return;
    
                isVisible = true;
                animator.enabled = true;
    
                SetChildrenActive(true);
                if (showAnimation != null)
                {
                    animator.Play(showAnimation.name);
                    if (showCoroutine != null)
                    {
                        StopCoroutine(showCoroutine);
                        
                    }
    
                    showCoroutine = _Show(showAnimation); 
                    StartCoroutine(showCoroutine);
                }
    
                // Show pages
                foreach (var page in pages)
                {
                    if (page.isDefaultPage)
                        page.Show();
                    else if (page.isVisible)
                        page.Hide();
                }
    
                if (showAudioClip != null)
                    InventoryUIUtility.AudioPlayOneShot(showAudioClip);
    
                if (OnShow != null)
                    OnShow();
            }
    
    
            public virtual void HideFirst()
            {
                isVisible = false;
                animator.enabled = false;
    
                SetChildrenActive(false);
                rectTransform.anchoredPosition = Vector2.zero;
            }
    
            public virtual void Hide()
            {
                if (isVisible == false)
                    return;
    
                isVisible = false;
    
                if (OnHide != null)
                    OnHide();
    
                if (hideAudioClip != null)
                    InventoryUIUtility.AudioPlayOneShot(hideAudioClip);
    
                if (hideAnimation != null)
                {
                    animator.enabled = true;
                    animator.Play(hideAnimation.name);
    
                    if (hideCoroutine != null)
                    {
                        StopCoroutine(hideCoroutine);                    
                    }
    
                    hideCoroutine = _Hide(hideAnimation);
                    StartCoroutine(hideCoroutine);
                }
                else
                {
                    animator.enabled = false;
                    SetChildrenActive(false);
                }
            }
    
    
            /// <summary>
            /// Hides object after animation is completed.
            /// </summary>
            /// <param name="animation"></param>
            /// <returns></returns>
            protected virtual IEnumerator _Hide(AnimationClip animation)
            {
                yield return new WaitForSeconds(animation.length + 0.1f);
    
                // Maybe it got visible in the time we played the animation?
                if (isVisible == false)
                {
                    SetChildrenActive(false);
                    animator.enabled = false;
                }
            }
    
    
            /// <summary>
            /// Hides object after animation is completed.
            /// </summary>
            /// <param name="animation"></param>
            /// <returns></returns>
            protected virtual IEnumerator _Show(AnimationClip animation)
            {
                yield return new WaitForSeconds(animation.length + 0.1f);
    
                if (isVisible)
                    animator.enabled = false;
            }
    
            #endregion
        }
    }
    

    InventoryUIDialogBase

    using UnityEngine;
    using System.Collections;
    using Devdog.InventorySystem.Dialogs;
    using UnityEngine.UI;
    
    namespace Devdog.InventorySystem.Dialogs
    {
    
        public delegate void InventoryUIDialogCallback(InventoryUIDialogBase dialog);
    
        /// <summary>
        /// The abstract base class used to create all dialogs. If you want to create your own dialog, extend from this class.
        /// </summary>
        [RequireComponent(typeof(Animator))]
        [RequireComponent(typeof(UIWindow))]
        public abstract partial class InventoryUIDialogBase : MonoBehaviour
        {
            [Header("UI")]
            public Text titleText;
            public Text descriptionText;
    
            public UnityEngine.UI.Button yesButton;
            public UnityEngine.UI.Button noButton;
    
            /// <summary>
            /// The item that should be selected by default when the dialog opens.
            /// </summary>
            [Header("Behavior")]
            public Selectable selectOnOpenDialog;
    
            /// <summary>
            /// Disables the items defined in InventorySettingsManager.disabledWhileDialogActive if set to true.
            /// </summary>
            public bool disableElementsWhileActive = true;
        
            protected CanvasGroup canvasGroup { get; set; }
            protected Animator animator { get; set; }
            public UIWindow window { get; protected set; }
    
            public virtual void Awake()
            {
                canvasGroup = GetComponent<CanvasGroup>();
                if (canvasGroup == null)
                    canvasGroup = gameObject.AddComponent<CanvasGroup>();
    
                animator = GetComponent<Animator>();
                window = GetComponent<UIWindow>();
    
    
                window.OnShow += () =>
                {
                    SetEnabledWhileActive(false); // Disable other UI elements
    
                    if (selectOnOpenDialog != null)
                        selectOnOpenDialog.Select();
                };
                window.OnHide += () =>
                {
                    SetEnabledWhileActive(true); // Enable other UI elements
                };
            }
    
            public void Toggle()
            {
                window.Toggle();
                if(window.isVisible)
                    SetEnabledWhileActive(false); // Disable other UI elements
                else
                    SetEnabledWhileActive(true); // Enable other UI elements
            }
    
            /// <summary>
            /// Disables elements of the UI when a dialog is active. Useful to block user actions while presented with a dialog.
            /// </summary>
            /// <param name="enabled">Should the items be disabled?</param>
            protected virtual void SetEnabledWhileActive(bool enabled)
            {
                if (disableElementsWhileActive == false)
                    return;
    
                foreach (var item in InventorySettingsManager.instance.disabledWhileDialogActive)
                {
                    var group = item.gameObject.GetComponent<CanvasGroup>();
                    if (group == null)
                        group = item.gameObject.AddComponent<CanvasGroup>();
    
                    group.blocksRaycasts = enabled;
                    group.interactable = enabled;
                }
            }
        }
    }
    

    ConfirmationDialog

    using UnityEngine;
    using System.Collections;
    using UnityEngine.UI;
    
    namespace Devdog.InventorySystem.Dialogs
    {
        public partial class ConfirmationDialog : InventoryUIDialogBase
        {
        
            /// <summary>
            /// Show this dialog.
            /// <b>Don't forget to call dialog.Hide(); when you want to hide it, this is not done auto. just in case you want to animate it instead of hide it.</b>
            /// </summary>
            /// <param name="title">Title of the dialog.</param>
            /// <param name="description">The description of the dialog.</param>
            /// <param name="yes">The name of the yes button.</param>
            /// <param name="no">The name of the no button.</param>
            /// <param name="yesCallback"></param>
            /// <param name="noCallback"></param>
            public virtual void ShowDialog(string title, string description, string yes, string no, InventoryUIDialogCallback yesCallback, InventoryUIDialogCallback noCallback)
            {
                SetEnabledWhileActive(false);
    
                window.Show(); // Have to show it first, otherwise we can't use the elements, as they're disabled.
    
                titleText.text = title;
                descriptionText.text = description;
                yesButton.GetComponentInChildren<Text>().text = yes;
                noButton.GetComponentInChildren<Text>().text = no;
    
                yesButton.onClick.RemoveAllListeners();
                yesButton.onClick.AddListener(() =>
                {
                    if (window.isVisible == false)
                        return;
    
                    SetEnabledWhileActive(true);
                    yesCallback(this);
                    window.Hide();
                });
    
                noButton.onClick.RemoveAllListeners();
                noButton.onClick.AddListener(() =>
                {
                    if (window.isVisible == false)
                        return;
    
                    SetEnabledWhileActive(true);
                    noCallback(this);
                    window.Hide();
                });
            }
    
            /// <summary>
            /// Show the dialog.
            /// <b>Don't forget to call dialog.Hide(); when you want to hide it, this is not done auto. just in case you want to animate it instead of hide it.</b>
            /// </summary>
            /// <param name="title">The title of the dialog. Note that {0} is the item ID and {1} is the item name.</param>
            /// <param name="description">The description of the dialog. Note that {0} is the item ID and {1} is the item name.</param>
            /// <param name="yes">The name of the yes button.</param>
            /// <param name="no">The name of the no button.</param>
            /// <param name="item">
            /// You can add an item, if you're confirming something for that item. This allows you to use {0} for the title and {1} for the description inside the title and description variables of the dialog.
            /// An example:
            /// 
            /// ShowDialog("Are you sure you want to drop {0}?", "{0} sure seems valuable..", ...etc..);
            /// This will show the item name at location {0} and the description at location {1}.
            /// </param>
            /// <param name="yesCallback"></param>
            /// <param name="noCallback"></param>
            public virtual void ShowDialog(string title, string description, string yes, string no, InventoryItemBase item, InventoryUIDialogCallback yesCallback, InventoryUIDialogCallback noCallback)
            {
                ShowDialog(string.Format(string.Format(title, item.name, item.description)), string.Format(description, item.name, item.description), yes, no, yesCallback, noCallback);
            }
        }
    }
    

    5、其他补充

    前言

    Inventory Pro插件中的基于预设(Prefab)的泛型对象池实现
    本文讲的就是基于UI的局部(从某个Scene,到某个Dialog)对象池,范围不同,需要的对象池就有不同的要求。

    ####演示

    左下角信息提示窗口NoticeUI中的信息提示比较多时,具有滚动条和超出自动隐藏的功能,就是通过对象池技术实现,从而提高性能和效率
    这里写图片描述
    下面代码是在NoticeUI中 设置对象池的相关代码
    这里写图片描述

    分析

    老规矩先上类图,不过其实没有什么好分析的,就是对象池的标准接口,取Get和回收这里是Destroy。还有出现两个类一个是泛型的,一个是非泛型的,至于为什么是结构体而不是类,这块我也没有深究。
    这里写图片描述
    下面我们仔细分析下这个UI对象池的类,首先看下类的

    声明和构造函数

    这里写图片描述
    1、类注释写的很清楚,告诉你这个对象池支持的对象是GameObjects,限制了必须是Unity3D的类型,该类的功能仅为了提高速度。

    它并不完善,建议使用小规模的数据。明白了这点很重要,也就是我在背景里说的每种场景使用的对象池是不一样的,一定不能混用和滥用。

    2、泛型结构体的声明,这里我们T限定了类型是UnityEngine.Component也就是Unity3D的组件基类,所以为什么注释里面说了只适合GameObjects了,剩下的就是new()关键字约束,确保类是可以new的。IPoolableObject是面向接口的一种实现约束(框架层考虑就是不一样),实际是强制要求在代码级别做好对象池类的管理。

    3、构造函数是用了缺省值StatSize64很贴心,下面三句初始化代码,创建了一个_PoolParent GameObject,并挂到了全局的组件树上,我觉得的这点特别重要,这使得在Edit下可以看见池对象的动态创建和状态,特别是让组件树特别清晰,点个赞,第三句,也就是保存了泛型对象

    poolParent = new GameObject("_PoolParent").transform;
    poolParent.SetParent(InventoryManager.instance.collectionObjectsParent);
    this.baseObject = baseObject;
    

    泛型池对象的初始化

    池对象初始化创建
    这里写图片描述
    泛型池对象的初始化其实是调用了GameObject.Instantiate的泛型方法重载,也就是深克隆。这也就是解释了为什么可以完成对于预设(Prefab)的对象池了,然后设置其父对象,挂接到组件树上,最后调用gameObject.SetActivie(false)让UI对象不可见,其实是一个开关。

    池对象的获取和销毁

    • 池对象的获取
      这里写图片描述
      有了创建时候的开关,获取UI池对象就可以通过这个开关来判断对象是否已经使用了,然后如果active是false的,通过设置成true打开就完成了池对象的创建,这里createWhenNoneLeft参数默认是true,用来解决当构造函数预设池对象都被使用后如何扩展的问题,这里其实很简单了当所有预设都用完了直接再创建一个即可,多亏了c#的动态数组List,不然还要像c++那样动态调整数组的大小。

    • 池对象的销毁
      这里写图片描述
      对象的销毁其实是回收,这里对象最简单的一步就是SetActive(false)也就是把UI开关给关上,让对象不可见。其实重要的是调用IPoolObject接口的Reset函数实现,进行一些善后工作,让然也要通过 transform.SetParent()进行组件树的重置。所有对象的销毁和回收就很简单了只需要循环遍历销毁即可。

    核心代码

    using System;
    using System.Collections.Generic;
    using Devdog.InventorySystem;
    using Devdog.InventorySystem.Models;
    using UnityEngine;
    
    namespace Devdog.InventorySystem
    {
        /// <summary>
        /// Only supports GameObjects and no unique types, just to speed some things up.
        /// It's not ideal and I advice you to only use it on small collections.
        /// </summary>
        public struct InventoryPool<T> where T : UnityEngine.Component, IPoolableObject, new()
        {
            private List<T> itemPool;
            private Transform poolParent;
            private T baseObject;
    
            public InventoryPool(T baseObject, int startSize = 64)
            {
                poolParent = new GameObject("_PoolParent").transform;
                poolParent.SetParent(InventoryManager.instance.collectionObjectsParent);
                this.baseObject = baseObject;
    
                itemPool = new List<T>(startSize);
                for (int i = 0; i < startSize; i++)
                {
                    Instantiate();
                }
            }
    
    
            /// <summary>
            /// Create an object inside the pool
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="obj"></param>
            public T Instantiate()
            {
                var a = GameObject.Instantiate<T>(baseObject);
                a.transform.SetParent(poolParent);
                a.gameObject.SetActive(false); // Start disabled
    
                itemPool.Add(a);
                return a;
            }
        
    
            /// <summary>
            /// Get an object from the pool
            /// </summary>
            public T Get(bool createWhenNoneLeft = true)
            {
                foreach (var item in itemPool)
                {
                    if(item.gameObject.activeSelf == false)
                    {
                        item.gameObject.SetActive(true);
                        return item;
                    }
                }
    
                if (createWhenNoneLeft)
                {
                    Debug.Log("New object created, considering increasing the pool size if this is logged often");
                    return Instantiate();
                }
    
                return null;
            }
        
    
            /// <summary>
            /// Mark an object as inactive so it can be recycled.
            /// </summary>
            /// <param name="item"></param>
            public void Destroy(T item)
            {
                item.Reset(); // Resets the item state
                item.transform.SetParent(poolParent);
                item.gameObject.SetActive(false); // Up for reuse
            }
    
            public void DestroyAll()
            {
                foreach (var item in itemPool)
                    Destroy(item);
            }
        }
    
        /// <summary>
        /// InventoryPool only good for gameObjects
        /// </summary>
        public struct InventoryPool
        {
            private List<GameObject> itemPool;
            private Transform poolParent;
            private GameObject baseObject;
    
            public InventoryPool(GameObject baseObject, int startSize = 64)
            {
                poolParent = new GameObject("_PoolParent").transform;
                poolParent.SetParent(InventoryManager.instance.collectionObjectsParent);
                this.baseObject = baseObject;
    
                itemPool = new List<GameObject>(startSize);
                for (int i = 0; i < startSize; i++)
                {
                    Instantiate();
                }
            }
    
    
            /// <summary>
            /// Create an object inside the pool
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="obj"></param>
            public GameObject Instantiate()
            {
                GameObject a = null;
                if (baseObject != null)
                    a = GameObject.Instantiate<GameObject>(baseObject);
                else
                    a = new GameObject();
    
                a.transform.SetParent(poolParent);
                a.gameObject.SetActive(false); // Start disabled
    
                itemPool.Add(a);
                return a;
            }
    
    
            /// <summary>
            /// Get an object from the pool
            /// </summary>
            public GameObject Get(bool createWhenNoneLeft = true)
            {
                foreach (var item in itemPool)
                {
                    if (item.gameObject.activeInHierarchy == false)
                    {
                        item.gameObject.SetActive(true);
                        return item;
                    }
                }
    
                if (createWhenNoneLeft)
                {
                    Debug.Log("New object created, considering increasing the pool size if this is logged often");
                    return Instantiate();
                }
    
                return null;
            }
    
    
            /// <summary>
            /// Mark an object as inactive so it can be recycled.
            /// </summary>
            /// <param name="item"></param>
            public void Destroy(GameObject item)
            {
                item.transform.SetParent(poolParent);
                item.gameObject.SetActive(false); // Up for reuse
            }
    
    
            public void DestroyAll()
            {
                foreach (var item in itemPool)
                    Destroy(item);
            }
        }
    }
    

    6、Inventory Pro学习总结

    背景

    游戏中的UI系统或者叫做GUI窗口系统主要有:主要装备窗口(背包,角色窗口也是一种特殊窗口)、确实提示窗口(如购买确认)、信息提示窗口(一遍没有按钮,ContexntMenu)和特殊窗口(聊天记录或者技能树),前篇已经介绍分析了Inventory Pro确认提示窗口的设计和实现方式,这篇主要讲一下信息提示窗口的实现。本以为提示窗口是比较简单的,毕竟没有按钮事件交互的问题,但是分析了下源代码还是让我有些惊讶,插件作者在提示窗口中考虑到了性能问题,由于本人一直在PC端开发程序没有移动端的经验,所以在移动端对于性能优化还是比较关注的。

    插件效果及使用

    左下角即为信息提示窗口NoticeUI,当信息提示比较多时,具有滚动条和超出自动隐藏的功能,是通过对象池技术实现,提高性能和效率
    这里写图片描述
    通过拖拽的方式创建好UI界面,红框中我们看到了组件树的结构和类型
    这里写图片描述
    在NoticeUI上绑定NoticeUI脚本,设置好每一行显示的预设NoticeMessageUI,ScrollRect等相关属性,基本就已经完成了关于信息提示窗口的实现了
    这里写图片描述

    源代码分析

    这里写图片描述

    类图分析

    经过这段时间的学习,我真的慢慢爱上了VS的类图分析了,希望新手同学也能习惯这点。VS的类图很强大能自动生成关联关系和继承接口等信息,是特别舒心的类图工具。

    A、先看下Message模型(Model)类,InventoryNoticeMessage继承了InventoryMessage,继承后拥有的字段有,消息,标题,颜色,消失延时,时间看到这些字段我们大致也可以猜到信息提示窗口有哪些功能了吧(其实是可以扩展的),这里需要重点关注下Show方法(后面源码分析再表述)

    B、NoticeUI和NoticeMessageUI都是MonoBehavior的子类,也就是说他们都是组件,分析其字段有具有ScrollRect和Text说明他们都是需要和UI进行绑定的。这里特变关注下VS使用双箭头表示组合关联,所以NoticeUI组合关联NoticeMessageUI,而继承了IPoolableObject接口顾名思义它具有入对象池的能力,也就是可以加入对象池,我们也看到了NoticeUI有一个InventoryPool,我们大概可以猜到NoticeUI中List和InevntoryPool猥琐的关系。

    调用流程分析

    调用的流程其实可以画一个流程图,这里只是简单的描述一下

    • 1、InventoryNoticeMessage.Show()
    • 2、 全局NoticeUI.AddMessage()
    • 3、InventoryPool池对象NoticeMessageUI.SetMessage()
    • 4、NoticMessageUI通过setAictive(true)进行显示
    • 5、NoticeUI.Update,通过循环调用NoticMessageUI的showTime.deltaTime做控制隐藏

    1、InventoryNoticeMessage.Show()
    这里写图片描述
    通过以上代码我们看的出来其实notice也是一个全局的UI,所以才可以通过单例来访问,应该是有固定区域的。

    2、 全局NoticeUI.AddMessage()
    这里写图片描述
    NoticeUI中的AddMessage就比较复杂了,主要要处理几个事情A、事件触发;B、滚动处理;C、对象池获取NoticeMessageUI并激活显示D、List和InventoryPool好基友的处理(对象池的回收及引用数组的移除)

    3、InventoryPool池对象NoticeMessageUI.SetMessage()
    这里写图片描述
    SetMessage() 就像它的方法名一样好像什么也没有做的样子,只是设置了一些简单字段的内容以及显示时间,实际的显示激活却是在4对象池获取的时候置位的。

    4、NoticMessageUI通过setAictive(true)进行显示
    这里写图片描述
    对象池的使用和回收是通过池对象的activeSelf属性来确定的,这个开关有一箭双雕的意思,既通过它来控制对象池的使用和回收,又用于控制UI对象的演示与否。

    5、NoticeUI.Update,通过循环调用NoticMessageUI的showTime.deltaTime控制隐藏
    这里写图片描述
    通过显示时间来控制信息的隐藏
    这里写图片描述
    隐藏函数使用了动画效果,由于动画是有显示时间的,所以通过一个字段isHiding做为状态判断。

    核心源码

    NoticeUI

    using UnityEngine;
    using UnityEngine.EventSystems;
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Devdog.InventorySystem.Models;
    using Devdog.InventorySystem.UI.Models;
    using UnityEngine.UI;
    
    namespace Devdog.InventorySystem
    {
        /// <summary>
        /// How long a message should last.
        /// Parse to int to get time in seconds.
        /// </summary>
        public enum NoticeDuration
        {
            Short = 2,
            Medium = 4,
            Long = 6,
            ExtraLong = 8
        }
    
    
        [AddComponentMenu("InventorySystem/Windows/Notice")]
        public partial class NoticeUI : MonoBehaviour
        {
            #region Events
    
            /// <summary>
            /// Note that it also fired when message == null or empty, even though the system won't process the message.
            /// This is because someone might want to implement their own system and just use the event as a link to connect the 2 systems.
            /// </summary>
            /// <param name="title"></param>
            /// <param name="message"></param>
            /// <param name="duration"></param>
            /// <param name="parameters"></param>
            public delegate void NewMessage(InventoryNoticeMessage message, params System.Object[] parameters);
            public event NewMessage OnNewMessage;
    
            #endregion
    
            [Header("General")]
            public NoticeMessageUI noticeRowPrefab;
    
            [InventoryRequired]
            public RectTransform container;
    
            public ScrollRect scrollRect;
            public AudioClip onNewMessageAudioClip;
    
    
            /// <summary>
            /// When more messages come in the last items will be removed.
            /// </summary>
            [Header("Messages")]
            public int maxMessages = 50;
    
            /// <summary>
            /// Remove the item after the show time has passed, if false, the item will continue to exist.
            /// </summary>
            public bool destroyAfterShowTime = true;
        
            /// <summary>
            /// All show times are multiplied by this value, if you want to increase all times, use this value.
            /// </summary>
            public float showTimeFactor = 1.0f;
        
            [NonSerialized]
            protected List<NoticeMessageUI> messages = new List<NoticeMessageUI>(8);
            private InventoryPool<NoticeMessageUI> pool;
    
    
            public virtual void Awake()
            {
                pool = new InventoryPool<NoticeMessageUI>(noticeRowPrefab, maxMessages);
            }
    
            public virtual void Update()
            {
                if (destroyAfterShowTime == false)
                    return;
    
                foreach (var message in messages)
                {
                    message.showTime -= Time.deltaTime;
                    if (message.showTime < 0.0f)
                    {
                        message.Hide();
                    }
                }
            }
    
            public virtual void AddMessage(string message, NoticeDuration duration = NoticeDuration.Medium)
            {
                AddMessage(message, duration);
            }
    
            public virtual void AddMessage(string message, NoticeDuration duration, params System.Object[] parameters)
            {
                AddMessage(string.Empty, message, duration, parameters);
            }
    
            public virtual void AddMessage(string title, string message, NoticeDuration duration, params System.Object[] parameters)
            {
                AddMessage(new InventoryNoticeMessage(title, message, duration));
            }
    
            public virtual void AddMessage(InventoryNoticeMessage message)
            {
                // Fire even if we do the nullcheck, just incase other people want to use their own implementation.
                if (OnNewMessage != null)
                    OnNewMessage(message, message.parameters);
    
                if (string.IsNullOrEmpty(message.message))
                    return;
    
                bool scrollbarAtBottom = false;
                if (scrollRect != null && scrollRect.verticalScrollbar != null && scrollRect.verticalScrollbar.value < 0.05f)
                    scrollbarAtBottom = true;
    
                // Incase we don't actually want to display anything and just port the data to some other class through events.
                if (noticeRowPrefab != null)
                {
                    var item = pool.Get();
                    //var item = GameObject.Instantiate<NoticeMessageUI>(noticeRowPrefab);
                    item.transform.SetParent(container);
                    item.transform.SetSiblingIndex(0); // Move to the top of the list
                    item.SetMessage(message);
    
                    if (onNewMessageAudioClip != null)
                        InventoryUIUtility.AudioPlayOneShot(onNewMessageAudioClip);
    
                    messages.Add(item);
                }
            
                if (messages.Count > maxMessages)
                {
                    StartCoroutine(DestroyAfter(messages[0], messages[0].hideAnimation.length));
                    messages[0].Hide();
                    messages.RemoveAt(0);
                }
    
                if (scrollbarAtBottom)
                    scrollRect.verticalNormalizedPosition = 0.0f;
            }
    
            protected virtual IEnumerator DestroyAfter(NoticeMessageUI item, float time)
            {
                yield return new WaitForSeconds(time);
                pool.Destroy(item);
            }
        }
    }
    

    NoticeMessageUI

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Devdog.InventorySystem.Models;
    using UnityEngine;
    using UnityEngine.UI;
    
    namespace Devdog.InventorySystem.UI.Models
    {
        /// <summary>
        /// A single message inside the message displayer
        /// </summary>
        [RequireComponent(typeof(Animator))]
        public partial class NoticeMessageUI : MonoBehaviour, IPoolableObject
        {
            public UnityEngine.UI.Text title;
            public UnityEngine.UI.Text message;
            public UnityEngine.UI.Text time;
    
            public AnimationClip showAnimation;
            public AnimationClip hideAnimation;
    
            [HideInInspector]
            public float showTime = 4.0f;
    
            public DateTime dateTime { get; private set; }
    
            [NonSerialized]
            protected Animator animator;
            [NonSerialized]
            protected bool isHiding = false; // In the process of hiding
    
            public virtual void Awake()
            {
                animator = GetComponent<Animator>();
    
                if (showAnimation != null)
                    animator.Play(showAnimation.name);
            }
    
            public virtual void SetMessage(InventoryNoticeMessage message)
            {
                this.showTime = (int)message.duration;
                this.dateTime = message.time;
    
                if (string.IsNullOrEmpty(message.title) == false)
                {
                    if (this.title != null)
                    {
                        this.title.text = string.Format(message.title, message.parameters);
                        this.title.color = message.color;
                    }
                }
                else
                    title.gameObject.SetActive(false);
    
    
                this.message.text = string.Format(message.message, message.parameters);
                this.message.color = message.color;
    
                if (this.time != null)
                {
                    this.time.text = dateTime.ToShortTimeString();
                    this.time.color = message.color;
                }
            }
    
            public virtual void Hide()
            {
                // Already hiding
                if (isHiding)
                    return;
    
                isHiding = true;
    
                if (hideAnimation != null)
                    animator.Play(hideAnimation.name);
            }
    
            public void Reset()
            {
                isHiding = false;
            }
        }
    }
    

    InventoryNoticeMessage

    using System;
    using System.Collections.Generic;
    using System.Threading;
    using UnityEngine;
    
    namespace Devdog.InventorySystem.Models
    {
        [System.Serializable]
        public partial class InventoryNoticeMessage : InventoryMessage
        {
            public DateTime time;
    
            public Color color = Color.white;
            public NoticeDuration duration = NoticeDuration.Medium;
    
    
            /// <summary>
            /// Required for PlayMaker...
            /// </summary>
            public InventoryNoticeMessage()
            { }
    
            public InventoryNoticeMessage(string title, string message, NoticeDuration duration, params System.Object[] parameters)
                : this(title, message, duration, Color.white, DateTime.Now, parameters)
            { }
    
            public InventoryNoticeMessage(string title, string message, NoticeDuration duration, Color color, params System.Object[] parameters)
                : this(title, message, duration, color, DateTime.Now, parameters)
            { }
    
            public InventoryNoticeMessage(string title, string message, NoticeDuration duration, Color color, DateTime time, params System.Object[] parameters)
            {
                this.title = title;
                this.message = message;
                this.color = color;
                this.time = time;
                this.parameters = parameters;
            }
    
            public override void Show(params System.Object[] param)
            {
                base.Show(param);
    
                this.time = DateTime.Now;
    
                if (InventoryManager.instance.notice != null)
                    InventoryManager.instance.notice.AddMessage(this);
            }
        }
    }
    
    展开全文
  • 背包问题解题思路

    千次阅读 2015-10-18 17:41:55
    可以这样理解:初始化的F数组事实上就是在没有任何物品可以入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被”恰好装满”,其他容量的背包均没有合法的...
    背包解题思路
    背包问题大部分都是类似模样的状态转移方程dp[j]=dp[j]>(dp[j-t]+v)?dp[j]:(dp[j-t]+v);
    普通类型的背包问题只需注意以下情况:
    一、对于物品数量:
    1、	01背包:每件物品只有一件:
    这表示物品不可以重复取,于是我们里面的循环可以写成for(j=v;j>=c;j--)这样从最大背包重量到只能装下一个当前物品的最小重量,由于是从后向前推的,设v表示背包的体积,c表示物体的体积而前面并没有装任何当前物品,所以就可以做到每个物品至多装下一件。(外层循环是物品件数)
    2、	多重背包:
    每个物品有有限件(可能是1件或多件,可以多选但不能超过最大件数)
    解这类题有两种方法,可行的一种方法是直接把每个物品的N件看成完全相同的N种物品,完成这个操作只需要在外层循环和内层循环之间加上一层while循环即可,就像这样:while(c--)//c即为当前物品的数量
    {  
    for(j=n;j>=p;j--)/*由于物品被当成多种来对待,所以“每件”不能重复选择*/
    {  
    dp[j]=max(dp[j],dp[j-p]+h); //状态转移方程,max()为取最大值函数
    }  
    }
    
    3、	完全背包:每个物品有无限件
    这表示每件物品都可以随便取,直到背包装不下为止,于是和单纯的只去一件相对,我们里面的循环可以写成for(j=c;j<=v;j++)这样,从只能装下一个当前物品的最小重量到最大背包重量,由于前面较小的背包重量可能已经装过当前物品了,所以后面的就可以装下重复的物品。
    4、	分组背包:物品分为不同的组,每组最多可以取一件(有的题是每组最少取一件)
    这种题的解题技巧也是加一层循环,不过不像多重背包,多加的这一层循环作用为在某一组中找出最优解,在这不好说,会在下面题中说明。
    
    二、对于是否需要装满的要求:
    技巧就是不需要装满的情况就把数组初始化为0,需要装满就把数组初始化为负无穷。
    我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现问题是在初始化的时候有所不同。
    如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其他F[1..V]均设为负无穷,这样就可以保证最终得到的F[V]是一种恰好装满背包的最优解。
    如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0….V]全部设为0。
    这是为什么呢?可以这样理解:初始化的F数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被”恰好装满”,其他容量的背包均没有合法的解,属于未定义的状态,应该被赋值为负无穷了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解”什么都不装“,这个解的价值为0,所以初始化时状态的值也就全部为0了。
    这个小技巧完全可以推广到其他类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。剩下的就是一些背包问题的变形以及扩展,那些题就需要随机应变了,下面会有例题。下面上例题:
    采药问题
    题目描述
    医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,
    每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。
    如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”  如果你是辰辰,你能完成这个任务吗?
    输入
    输入的第一行有两个整数T(1  < =  T  < =  1000)和M(1  < =  M  < =  100),用一个空格隔开,
    T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整数,
    分别表示采摘某株草药的时间和这株草药的价值。
    输出
    输出包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
    样例输入
    70 3
    71 100
    69 1
    1 2
    样例输出
    3
    提示
    对于30%的数据,M  < =  10;对于全部的数据,M  < =  100。
    
    代码:
    #include<stdio.h>
    #include<string.h>
    int dp[1001];
    int main()
    {
    	int time,m,i,j,t,v;
    	scanf("%d%d",&time,&m);
    	memset(dp,0,sizeof(dp));
    	for(i=1;i<=m;i++)
    	{
    		scanf("%d%d",&t,&v);
    		for(j=time;j>=t;j--)
    			dp[j]=dp[j]>(dp[j-t]+v)?dp[j]:(dp[j-t]+v);
    	}
    	printf("%d\n",dp[time]);
    	return 0;
    }
    
    NYOJ 289 苹果
    题目信息:
    苹果
    时间限制:3000 ms  |  内存限制:65535 KB 
    难度:3
    描述 
    ctest有n个苹果,要将它放入容量为v的背包。给出第i个苹果的大小和价钱,求出能放入背包的苹果的总价钱最大值。
    输入
    有多组测试数据,每组测试数据第一行为2个正整数,分别代表苹果的个数n和背包的容量v,n、v同时为0时结束测试,此时不输出。接下来的n行,每行2个正整数,用空格隔开,分别代表苹果的大小c和价钱w。所有输入数字的范围大于等于0,小于等于1000。
    输出
    对每组测试数据输出一个整数,代表能放入背包的苹果的总价值。
    样例输入
    3 3
    1 1
    2 1
    3 1
    0 0
    样例输出
    2
    代码:
    #include <stdio.h>
    #include <string.h>
    int main()
    {
    	int n,v,c,w,i,j;
    	int dp[1005];
    	while(scanf("%d%d",&n,&v)!=EOF&&!(n==0&&v==0))
    	{
    		memset(dp,0,sizeof(dp));
    		for(i=0;i<n;i++)
    		{
    			scanf("%d%d",&c,&w);
    			for(j=v;j>=c;j--)
    				dp[j]=dp[j]>(dp[j-c]+w)?dp[j]:(dp[j-c]+w);
    		}
    		printf("%d\n",dp[v]);
    	}
    	return 0;
    }
    
    NYOJ 311 完全背包
    题目信息:
    完全背包
    时间限制:3000 ms  |  内存限制:65535 KB 
    难度:4
    描述 
    直接说题意,完全背包定义有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是c,价值是w。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。本题要求是背包恰好装满背包时,求出最大价值总和是多少。如果不能恰好装满背包,输出NO
    输入
    第一行: N 表示有多少组测试数据(N<7)。 
    接下来每组测试数据的第一行有两个整数M,V。 M表示物品种类的数目,V表示背包的总容量。(0<M<=2000,0<V<=50000)
    接下来的M行每行有两个整数c,w分别表示每种物品的重量和价值(0<c<100000,0<w<100000)
    输出
    对应每组测试数据输出结果(如果能恰好装满背包,输出装满背包时背包内物品的最大价值总和。 如果不能恰好装满背包,输出NO)
    样例输入
    2
    1 5
    2 2
    2 5
    2 2
    5 1
    样例输出
    NO
    1
    解题思路: 
    每种物品无限用,所以循环从前向后递推。此题要求必须装满,所以初始化时把数组初始化为负无穷大,最后用来筛选排除掉还是负数的(由于只有op[0]=0,op数组的其它值都是负无穷大,所以只有剩余空间为0时op[j]最后的值op[j]=op[j]>op[j-s[i].c]+s[i].w?op[j]:op[j-s[i].c]+s[i].w;才可能为正数,如果还为负数则表示不能装满)。
    
    代码:
    #include <stdio.h>
    #include <string.h>
    int op[50005];
    struct node{
    	int c;//重量 
    	int w;//价值 
    }s[2002];
    int main()
    {
    	int N,M,V,i,j,t;
    	scanf("%d",&N);
    	while(N--)
    	{
    		memset(op,-0x3f,sizeof(op));
    		op[0]=0;
    		scanf("%d%d",&M,&V);
    		for(i=0;i<M;i++)
    			scanf("%d%d",&s[i].c,&s[i].w);
    		for(i=0;i<M;i++)
    		{
    			for(j=s[i].c;j<=V;j++)
    			{
    				op[j]=op[j]>op[j-s[i].c]+s[i].w?op[j]:op[j-s[i].c]+s[i].w;	
    			}
    		}
    		if(op[V]<0)
    			printf("NO\n");
    		else
    			printf("%d\n",op[V]);
    	}
    	return 0;
    }
    
    HDU 2191 多重背包问题
    题目信息:
    Problem Description
    急!灾区的食物依然短缺!
    为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾区,现在假设你一共有资金n元,而市场有m种大米,每种大米都是袋装产品,其价格不等,并且只能整袋购买。
    请问:你用有限的资金最多能采购多少公斤粮食呢?
    Input
    输入数据首先包含一个正整数C,表示有C组测试用例,每组测试用例的第一行是两个整数n和 m(1<=n<=100, 1<=m<=100),分别表示经费的金额和大米的种类,然后是m行数据,每行包含3个数p,h和c(1<=p<=20,1& amp;lt;=h<=200,1<=c<=20),分别表示每袋的价格、每袋的重量以及对应种类大米的袋数。
    
    Output
    对于每组测试数据,请输出能够购买大米的最多重量,你可以假设经费买不光所有的大米,并且经费你可以不用完。每个实例的输出占一行。
     
    Sample Input
    1
    8 2
    2 100 4
    4 100 2
     
    Sample Output
    400
    解题思路: 
    多重背包即每种物品有有限多个可选择的背包问题,其解法和普通的背包问题基本一样,只需把每种物品看成多件即可。
    代码部分:
    #include <stdio.h>
    #include <string.h>
    int max(int a,int b)
    {
    	return a>b?a:b;
    }
    int main()
    {
    	int t,m,n,i,j,k,p,h,c;
    	int dp[500];
    	scanf("%d",&t);
    	while(t--)
    	{
    		scanf("%d %d",&n,&m);
    		memset(dp,0,sizeof(dp));
    		for(i=0;i<m;i++)
    		{
    			scanf("%d %d %d",&p,&h,&c);
    			while(c--)
    			{
    				for(j=n;j>=p;j--)
    				{
    					dp[j]=max(dp[j],dp[j-p]+h);
    				}
    			}
    		}
    		printf("%d\n",dp[n]);
    	}
    	return 0;
    }
    
    
    
    HDU 1712 分组背包问题
    题目信息:
    ACboy needs your help
    Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
    Total Submission(s): 5146    Accepted Submission(s): 2780
    Problem Description
    ACboy has N courses this term, and he plans to spend at most M days on study.Of course,the profit he will gain from different course depending on the days he spend on it.How to arrange the M days for the N courses to maximize the profit?
     
    Input
    The input consists of multiple data sets. A data set starts with a line containing two positive integers N and M, N is the number of courses, M is the days ACboy has.
    Next follow a matrix A[i][j], (1<=i<=N<=100,1<=j<=M<=100).A[i][j] indicates if ACboy spend j days on ith course he will get profit of value A[i][j].
    N = 0 and M = 0 ends the input.
     
    Output
    For each data set, your program should output a line which contains the number of the max profit ACboy will gain.
    
    Sample Input
    2 2
    1 2
    1 3
    2 2
    2 1
    2 1
    2 3
    3 2 1
    3 2 1
    0 0
    
    Sample Output
    3
    4
    6
    题目大意:
    一个学生用M天的时间复习N门课程,每门课程花费不同的天数,有不同的收获。问如何安排这M天,使得收获最大。
    解题思路:
    分组背包即物品分为多组,每一组的各个物品最多只能取一个。解题方法即在循环最里面再嵌套一个循环,查找每组中价值最大的物品。
    代码部分:
    #include <stdio.h>
    #include <string.h>
    int main()
    {
    	int N,M,i,j,k;
    	int dp[105],value[105];
    	while(scanf("%d %d",&N,&M)&&M||N)
    	{
    		memset(dp,0,sizeof(dp));
    		memset(value,0,sizeof(value));
    		for(i=1;i<=N;i++)/*外面的这层循环为组数,在这一题中为课程数*/
    		{
    			for(j=1;j<=M;j++)/*这里面输入的是每一组物品的信息,这一题中为当前课程花费多少天数可得到的收获,如输入1 3 2表示当前课程学1天收获为1,学两天收获3,学3天收获2*/
    			{
    				scanf("%d",&value[j]);
    			}
    			for(j=M;j>0;j--)/*天数从大到小循环,保证不会出现某一门课程既总共复习了一天,又总共复习了两天这样的情况发生*/
    			{
    				for(k=j;k>0;k--)/*当前这一组物品在当前天数情况下可选取的最优解*/
    					dp[j]=dp[j]>dp[j-k]+value[k]?dp[j]:dp[j-k]+value[k];
    			}
    		}
    		printf("%d\n",dp[M]);
    	}
    	return 0;
    }
    
    
    
    下面就是一些背包问题的扩展及变形,需要把思路转换一下:
    NYOJ 720 项目安排
    题目信息:
    项目安排
    时间限制:3000 ms  |  内存限制:65535 KB 
    难度:4
    描述 
    小 明每天都在开源社区上做项目,假设每天他都有很多项目可以选,其中每个项目都有一个开始时间和截止时间,假设做完每个项目后,拿到报酬都是不同的。由于小 明马上就要硕士毕业了,面临着买房、买车、给女友买各种包包的鸭梨,但是他的钱包却空空如也,他需要足够的money来充实钱包。万能的网友麻烦你来帮帮 小明,如何在最短时间内安排自己手中的项目才能保证赚钱最多(注意:做项目的时候,项目不能并行,即两个项目之间不能有时间重叠,但是一个项目刚结束,就 可以立即做另一个项目,即项目起止时间点可以重叠)。 
    输入
    输入可能包含多个测试样例。
    对于每个测试案例,输入的第一行是一个整数n(1<=n<=5000):代表小明手中的项目个数。
    接下来共有n行,每行有3个整数st、ed、val,分别表示项目的开始、截至时间和项目的报酬,相邻两数之间用空格隔开。
    st、ed、value取值均在32位有符号整数(int)的范围内,输入数据保证所有数据的value总和也在int范围内。
    输出
    对应每个测试案例,输出小明可以获得的最大报酬。
    样例输入
    3
    1 3 6
    4 8 9
    2 5 16
    4
    1 14 10
    5 20 15
    15 20 8
    18 22 12
    样例输出
    16
    22
    提示
    上传时数据加强,项目起始时间和终止时间可能相同(其他oj可能无此情况)
    解题思路: 
    乍一看这题有点贪心算法的味道,其实不是。这仍然是一道动态规划的背包问题,而状态转移方程需要去找到结束时间在当前项目开始时间之前的项目,那时 的最大报酬加上当前项目的报酬之和去和上一个项目的报酬相比较取最大值。完成这样的操作则必须需要排序,因为这里需要去找到结束时间在当前项目开始时间之前的项 目,所以需要按项目结束时间从小到大排序,相同则按项目开始时间从小到大排序。
    代码部分:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    int dp[5005];
    struct Project
    {
    	int st;
    	int ed;
    	int value;
    }item[5005];
    int find(int k)
    {
    	int head=1,arrow,tail=k;
    	while(tail>head)
    	{
    		arrow=(head+tail)/2;
    		if(item[arrow].ed<=item[k].st)
    			head=arrow+1;
    		else
    			tail=arrow;
    	}
    	return tail-1;
    }
    int cmp(const void *a,const void *b)
    {
    	Project *c=(Project *)a;
    	Project *d=(Project *)b;
    	if(c->ed!=d->ed)
    		return c->ed-d->ed;
    	return c->st-d->st;
    }
    int main()
    {
    	int n,i;
    	while(~scanf("%d",&n))
    	{
    		for(i=1;i<=n;i++)
    			scanf("%d%d%d",&item[i].st,&item[i].ed,&item[i].value);
    		qsort(&item[1],n,sizeof(item[1]),cmp);
    		memset(dp,0,sizeof(dp));
    		for(i=1;i<=n;i++)
    			dp[i]=dp[i-1]>(dp[find(i)]+item[i].value)?dp[i-1]:(dp[find(i)]+item[i].value);
    		printf("%d\n",dp[n]);
    	}
    	return 0;
    }
    
    
    
    NYOJ 613 免费馅饼
    题目信息:
    免费馅饼
    时间限制:1000 ms  |  内存限制:65535 KB 
    难度:3
    描述 
    都说天上不会掉馅饼,但有一天gameboy正走在回家的小 径上,忽然天上掉下大把大把的馅饼。说来gameboy的人品实在是太好了,这馅饼别处都不掉,就掉落在他身旁的10米范围内。馅饼如果掉在了地上当然就 不能吃了,所以gameboy马上卸下身上的背包去接。但由于小径两侧都不能站人,所以他只能在小径上接。由于gameboy平时老呆在房间里玩游戏,虽 然在游戏中是个身手敏捷的高手,但在现实中运动神经特别迟钝,每秒种只有在移动不超过一米的范围内接住坠落的馅饼。现在给这条小径如图标上坐标:
     
    
    为了使问题简化,假设在接下来的一段时间里,馅饼都掉落在0-10这11个位置。开始时gameboy站在5这个位置,因此在第一秒,他只能接到4,5,6这三个位置中其中一个位置上的馅饼。问gameboy最多可能接到多少个馅饼?(假设他的背包可以容纳无穷多个馅饼) 
    输入
    输入数据有多组。每组数据的第一行为以正整数 n(0<n<100000),表示有n个馅饼掉在这条小径上。在结下来的n行中,每行有两个整数 x,T(0<T<100000),表示在第T秒有一个馅饼掉在x点上。同一秒钟在同一点上可能掉下多个馅饼。n=0时输入结束。
    输出
    每一组输入数据对应一行输出。输出一个整数m,表示gameboy最多可能接到m个馅饼。
    提示:本题的输入数据量比较大,建议用scanf读入,用cin可能会超时。
    样例输入
    6
    5 1
    4 1
    6 1
    7 2
    7 2
    8 3
    0
    样例输出
    4
    解题思路: 
    动态规划的背包问题的变形,把馅饼从上向下落想象成从下向上落,只需要建立数组输入每个馅饼的位置(坐标),从下向上每层循环,每个dp[i][j]找dp[i+1][j-1]、dp[i+1][j]、dp[i+1][j+1]中的最大值就行了,只需要注意边界的问题就可以了,我处理边界的方法是加一圈边,即数组左边留一列初始化为0(防止j-1溢出),右边、下边也多一列(一行)初始化为0,这个定义数组时定大点就OK啦,然后一层层往上加,最后输出最上层数组最中间的值,就是一开始站的位置,那里的值就是能接的馅饼最大数量。
    代码部分:
    #include <stdio.h>
    #include <string.h>
    int dp[100005][13];
    int Max(int a,int b,int c)
    {
    	if(a<b)
    		a=b;
    	if(a<c)
    		a=c;
    	return a;
    }
    int main()
    {
    	int n,x,t,i,j,maxtime;
    	while(scanf("%d",&n)!=EOF&&n!=0)
    	{
    		memset(dp,0,sizeof(dp));
    		maxtime=0;
    		for(i=0;i<n;i++)
    		{
    			scanf("%d%d",&x,&t);
    			dp[t][x+1]++;/*某个位置有多少个馅饼*/
    			if(t>maxtime)/*找最大行数*/
    				maxtime=t;
    		}
    		for(i=maxtime-1;i>=0;i--)
    {
    			for(j=1;j<=11;j++)
    {
    				dp[i][j]=Max(dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1])+dp[i][j];
    }
    		printf("%d\n",dp[0][6]);
    }
    	}
    	return 0;
    }
    
    
    NYOJ 860 又见01背包
    题目信息:
    又见01背包
    时间限制:1000 ms  |  内存限制:65535 KB 
    难度:3
    描述 
        有n个重量和价值分别为wi 和 vi 的 物品,从这些物品中选择总重量不超过 W 
    的物品,求所有挑选方案中物品价值总和的最大值。
      1 <= n <=100
      1 <= wi <= 10^7
      1 <= vi <= 100
      1 <= W <= 10^9
    输入
    多组测试数据。
    每组测试数据第一行输入,n 和 W ,接下来有n行,每行输入两个数,代表第i个物品的wi 和 vi。
    输出
    满足题意的最大价值,每组测试数据占一行。
    样例输入
    4 5
    2 3
    1 2
    3 4
    2 2
    样例输出
    7
    
    解题思路:
    题目为普通的01背包问题,只是数据有点大额,开不了那么大的数组。但是价值不算大,所以我们可以换个思路,从价值着手做,求价值一定时的最小物品重量,然后从大到小找第一个满足背包大小的状态。代码部分:
    #include <stdio.h>
    #include <string.h>
    long long dp[10005],w[105],t;
    int v[105],n,i,j,sumv;
    int main()
    {
    	while(scanf("%d%lld",&n,&t)!=EOF)
    	{
    		memset(dp,0x3f,sizeof(dp));
    		dp[0]=0;
    		sumv=0;
    		for(i=1;i<=n;i++)
    		{
    			scanf("%lld%d",&w[i],&v[i]);
    			sumv+=v[i];/*所有物品的价值总和,用来当做“背包重量”循环用*/
    		}
    		for(i=1;i<=n;i++)
    			for(j=sumv;j>=v[i];j--)
    				dp[j]=dp[j]<(dp[j-v[i]]+w[i])?dp[j]:(dp[j-v[i]]+w[i]);/*这里因为需要寻找当前价值的最小背包重量,所以需要在其中找小的*/
    		for(i=sumv;i>=0;i--)
    			if(dp[i]<=t)/*价值从大到小、找到第一个能被背包装下的重量*/
    				break;
    		printf("%d\n",i);
    	}
    	return 0;
    }
    

    展开全文
  • 在主类FirstPersonController中,运动之类的都到FixedUpdate()中,检测跳跃按钮是否按下的在Update()方法里,通过GetInput()方法获取到输入,判断好是走还是跑,然后在FixedUpdate()里通过m_CharacterController....
  • unity官方资源CarController理解

    千次阅读 2018-04-14 13:17:33
    最近要写汽车的控制,发现比人物控制难很多,涉及到汽车的很多知识,自己写了一点不忍直视,各种bug,然后被告知untiy官方资源里有控制脚本,就去学习了一下,然后借鉴了网上的一些教程,在这里表示衷心感谢,为了...
  • 不过假设我们自己写了一个jar外部,想到maven中央仓库并使得别人和我们自己使用的时候更加方便,应该怎么做呢? 一,去sonatype网站注册账户并申请工单 进入issues.sonatype.org,注册一个账户,然后点击...
  • Python--安装pyecharts,anaconda环境下如何正确安装神奇可视化pyecharts[安装的那些事儿]更多更多安装教程及软件获取请加关注留言哟~~,若需私信联系博主请加:一、安装步骤1.按键盘上的 windows+R2.输入cmd后...
  • 狼从房子上扑下来,巨猿把它架住,用手掰住它的牙齿,然后把它按在地上,然后再扑过去。」冯骥说,「听起来不错是吧?好,手一掰住牙齿就穿模了。我们没法让主角站在一个稳定的位置,所以牙齿会从手指里穿出来。」 ...
  • 原文链接: http://www.leleblog.top/daily/more?id=4 对springboot的接触时间不短了,却一直没有自己真正动手做一个小项目,这次... 目前我所知道的两种打包部署的方式,一种是jar运行,一种是war放在外部t...
  • //不在地上时,作用一个重力影响 } m_CollisionFlags = m_CharacterController.Move(m_MoveDir*Time.fixedDeltaTime); //移动! ProgressStepCycle(speed); //处理脚步声音 UpdateCameraPosition(speed); //...
  • SteamVR Unity工具

    千次阅读 2016-12-02 17:46:59
    原文: [AR/VR教程] SteamVR Unity工具(三):控制器交互  可交互对象(VRTK_InteractableObject) 可交互对象脚本被添加到需要用(如控制器)来交互的任何游戏对象上。 可用脚本参数如下 Touch ...
  • 网页数据抓入门教程上 ​ 最近几年python火了,很多小伙伴业内的业外的都想要学一学,但是我在网上搜教程,发现知识都很散,没有一条系统的主线可以串联,导致很多小伙伴在自学的时候往往需要浪费很多不必要的时间...
  • 我们使用Pub管理工具 来获取Dart。在Pub上,可以找到公开可用的。或者从本地文件系统或其他的位置,比如Git仓库,加载可用的。无论是从什么途径加载的, Pub 都会进行版本依赖管理,从而帮助我们获得版本...
  • content内部就是我们应该物体的地方。 为了直观的演示,我将content下的文字item删除了,换成了单色精灵(变成红色),然后重命名item,同理: 我们加入,蓝色,绿色,粉色吧 好了,我们会发现...
  • 这几天又把14年给做了,这是day1的第三...如果小鸟一不小心撞到了水管或者掉在地上的话,便宣告失败。 为了简化问题,我们对游戏规则进行了简化和改编: 游戏界面是一个长为 n,高为 m 的二维平面,其中有k 个管
  • 百度的春晚战事

    千次阅读 多人点赞 2019-02-28 20:20:51
    拼尽全力,就是抢,也要在两周内抢来1万台新的服务器,到南京数据中心。 这些服务器都会并入百度云统一调配。张家军发现,虽然百度云平时默不作声,但是这么多年对于技术的极致追求,越是在艰难的时刻越能体现...
  • 程序员鱼皮 表情网站项目学习

    千次阅读 2021-11-20 22:14:12
    并且现在我们可以可视化地在任何电脑上对数据库进行管理 现成的云服务会比本地的数据库方便很多 搭建基础demo,整合框架 jar:直接运行 war:配合Tomcat等服务器进行访问 Lombok:团队中最好不要使用 ​ 原因:...
  • 在烟花绽放里消失

    2017-04-01 16:48:07
    “马上,马上,再等等我,”,小满喝完最后一口牛奶,然后迅速拿起沙发上的文具向门口走去,“妈,我要考试去了。” “慢点别急,路上小心点,加油,祝你考个好成绩啊!” 小满因为昨晚一直都在准备考试,所以...
  • Libgdx播放Spine动画(2)-功能

    千次阅读 2016-03-11 12:14:35
    如果使用混合动画,只需要设置混合时间,在播放第二个动画的时候,会在混合时间内将第一个动画的状态逐渐恢复到第二个动画的初始状态,也就是手是慢慢下来再去摸脸的(虽然也很快,但不是瞬间下来的),所以会使...
  • ArcGIS Earth 1.11版本发布啦!

    千次阅读 2020-10-22 14:05:03
    文章目录ArcGIS Earth简介制作五彩地图 随心三维展示切换日光效果 感受流年穿梭应用实例:地上地下 无缝衔接数据交互 快人一步支持平板 全面协作下载 转自Esri北京研发中心公众号(已获得授权),以下为原文的版权...
  • hstack()函数按新的x轴连接,倒也勉强称地上“按行连接”; 这里给出一个猜测: hastack()函数会将多维数组都视为二维数组,最高两维分别视为y轴与x轴,并按”行“连接。 接下来,我们拿四维数组的情况验证...
  • 非科班进大厂必备算法,没压中找我拿红包

    千次阅读 多人点赞 2020-09-14 21:35:48
    小孙同学去图书馆借书,一次性了借了40本书,出图书馆的时候报警了,不知道哪一本书没有消磁,然后把书放在地上,准备一本本尝试。 女生的操作被旁边的阿姨看见了,阿姨说你这样操作多慢啊,我来教你。于是将树分为...
  • 那妇人被按压在地上 那妇人被按压在地上,只叫道:“好汉饶我!”那里敢挣扎。只见门前 一人挑一担柴歇在门首。望见武松按倒那妇人在地上,那人大踏步跑将进来, 叫道:“好汉息怒!且饶恕了,小人自有话说。”武松...
  • Android,与北岛的诗

    千次阅读 2016-03-04 13:29:07
    剧毒的杀虫剂 诞生了 7 我死的那年十岁 那抛向空中的球再也没 落到地上 你是唯一的目击者 十岁,我知道 然后我登上 那辆运载野牛的火车 被列入过期的提货单里 供人们阅读...
  • 程序员人事面试题 1、谈谈你自己的情况?    【解答思路】:建议大家用2分钟得自我介绍,面试官较喜欢的自我介绍1、有亮点,每一小段都有一个亮点,而不是平铺直叙2、有互动,每一小段都会和面试...
  • 使用solidworks插件获取urdf

    千次阅读 热门讨论 2017-10-22 20:47:24
    由于gazebo是一个物理仿真环境,所以现在可以看到在solidworks下建模的机械臂像个咸鱼一样躺在地上了 ps.gazebo占用内存过大,如果出现未响应的情况,关掉重启节点一般就好了   最后,一下生成的urdf...
  • 农村包围城市的『快手』

    千次阅读 2017-05-14 16:12:18
    研究和体验快手这款app真的很有意思,让我想起了小时候一起在泥土地上奔跑的玩伴,让我看到了在故乡生活的那群同样渴望那个被关注的那群人,外表粗犷内心却很质朴。 1 市场分析 1.1 市场规模 ...
  • 服务重生+前台服务+双进程守护,六神装+复活甲在手,依旧被华为按在地上摩擦。 直到最后,武林中流传着这样一套拳谱,伤敌一千自损八百,名曰七伤拳:无声音乐保活大法; 也就是在服务中循环播放一段无声的音乐,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,565
精华内容 4,226
关键字:

包放地上