关于游戏的灵感来源
今年元宵节的时候,我玩的小游戏里面有限时任务,可以解锁节日限定物品,于是那几天我玩的很欢乐很积极。端午节到来之前,我想玩一下身份转换,从玩家转换到游戏策划。一个有趣的想法在脑海中逐渐清晰。
假如我是游戏策划
假如我是游戏策划,首先会对自己灵魂三连问:活动内容什么?活动怎么玩?活动奖励是什么?
现有大体的想法,然后再拆分到各个细节中去。
因为游戏中的一些场景搭配、日常活动名称、称号等借鉴了我最近沉迷的游戏《美人传》,所以这次的游戏仅供学习练习,不做任何商业用途。
产品视角
站在产品的角度思考活动设计,我的产品视角是这样的:
一入夏,就盼着假期,过了五一很快就会到端午,一想到端午就不由自主的想到美味的粽子。所以端午的活动就来了,包粽子。众所周知,包粽子需要糯米、粽叶等必备材料,而粽子的内馅有很多种,本次活动中需要的是红枣。所以包粽子的材料就选定了糯米、粽叶、红枣三种。(活动内容是什么)
游戏中有日常收集任务,每个收集任务掉落的材料都是固定的。活动期间一般会增加活动材料限时掉落,所以在活动期间,日常收集时会掉落包粽子需要的材料,不同收集任务掉落不同材料。(活动怎么玩)
粽子积累到一定数量就可以兑换节日限定物品。一般游戏中的节日限定物品都是精心设计的,但是由于时间和精力有限,我这次活动设计的比较简单,不同数量的粽子可以兑换不同的称号,最高称号为“荣宠万千”。(活动奖励是什么)
(^U^)ノ~YO,一切准备就绪,开始干活。
交互设计
大致画了一下设计草图,帮助理清楚布局思路。(第一次画,还有待提高。)
首页
日常任务
端午活动
功能设计
首页
内容
主要包括用户信息、任务入口、活动入口等展示。
称号规则
称号和糯米粽子数量对应如下:
称号 |
糯米粽子数量 |
殿上佳人 |
<50 |
淑仪倾城 |
>=50 && < 100 |
花容初绽 |
>=100 && < 200 |
花成蜜就 |
>=200 && < 300 |
宠冠六宫 |
>=300 && < 400 |
凤仪千载 |
>=400 |
功能实现
首页页面
/** * @description 首页 */importReactfrom'react'; import { useHistory } from'react-router-dom'; importAvatarfrom'@/components/Avatar'; importFlowerClusterfrom'@/components/FlowerCluster'; import { Button } from'antd-mobile'; import'./index.less'; constHome= () => { consthistory=useHistory(); // 页面跳转constgoTo=path=> { history.push(path); }; // 入口展示constentranceContent= () => { return ( <divclassName='home-entrance'><Buttonblockshape='rounded'className='entrance-btn'onClick={() =>goTo('/tasks')}>日常任务</Button><Buttonblockshape='rounded'className='entrance-btn'onClick={() =>goTo('/festival')}>端午活动</Button></div> ); }; return ( <divclassName='home'><divclassName='home-head'><Avatar/></div><divclassName='home-center'></div><divclassName='home-bg'> {/* 门 */} <divclassName='door'><divclassName='door-beam'><divclassName='tiaoliang'></div></div><divclassName='door-frame'><divclassName='door-top'></div><divclassName='door-line door-line-left'></div><divclassName='door-line door-line-right'></div><divclassName='door-line door-line-bottom'></div><divclassName='door-frame'><divclassName='stick-h stick-h1'></div><divclassName='stick-h stick-h2'></div><divclassName='stick-h stick-h3'></div><divclassName='stick-h stick-h4'></div><divclassName='stick-h stick-h5'></div><divclassName='stick-h stick-h6'></div><divclassName='stick-h stick-h7'></div><divclassName='stick-h stick-h8'></div><divclassName='stick-h stick-h9'></div><divclassName='stick-h stick-h10'></div><divclassName='stick-h stick-h11'></div><divclassName='stick-h stick-h12'></div><divclassName='stick-d stick-d1'></div><divclassName='stick-d stick-d2'></div><divclassName='stick-d stick-d3'></div><divclassName='stick-d stick-d4'></div><divclassName='stick-d stick-d5'></div><divclassName='stick-d stick-d6'></div></div><divclassName='door-opening'><divclassName='door-opening-center'>{entranceContent()}</div><divclassName='door-opening-decorate door-opening-decorate1'></div><divclassName='door-opening-decorate door-opening-decorate2'></div><divclassName='door-opening-decorate door-opening-decorate3'></div><divclassName='door-opening-flowers'><FlowerCluster/></div></div></div></div> {/* 地板 */} <divclassName='floor'><divclassName='floor-line floor-line1'></div><divclassName='floor-line floor-line2'></div><divclassName='floor-line floor-line3'></div><divclassName='floor-line floor-line4'></div><divclassName='floor-line floor-line5'></div><divclassName='floor-line floor-line6'></div><divclassName='floor-line floor-line7'></div><divclassName='floor-line floor-line8'></div><divclassName='floor-line floor-line9'></div><divclassName='floor-line floor-line10'></div><divclassName='home-cat'><divclassName='body'></div><divclassName='head'><divclassName='ear ear-left'></div><divclassName='ear ear-right'></div><divclassName='nose'></div><divclassName='whisker whisker-left'></div><divclassName='whisker whisker-right'></div></div><divclassName='tail'><divclassName='tail-line'></div><divclassName='tail-round'></div><divclassName='tail-end'></div></div></div><divclassName='home-table'></div></div></div></div> ); }; exportdefaultHome;
样式
.home { width: 100%; height: 100vh; position: relative; background: #46272d;&-head { width: 100%; height: 60px; background: #f3a29f;position: relative; } &-center { width: 200px; height: 200px; z-index: 99; margin-top: 60px; } &-bg { width: 100%; position: absolute; top: 70px; left: 0; z-index: 10; .door { &-beam { width: 100%; height: 90px; border-top:3pxsolid#9b6d59;background: #825146;position: relative; .tiaoliang { width: 100%; height: 50px; background: #4e2e29;background-image: repeating-linear-gradient(45deg, transparent, transparent13px, #9b6d59 13px, #9b6d59 15px), repeating-linear-gradient(-45deg, transparent, transparent 13px, #9b6d59 13px, #9b6d59 15px);border-top: 5pxsolid#f5a672;border-bottom: 5pxsolid#f5a672;position: absolute; top: 20px; left: 0; } } &-frame { width: 100%; height: 300px; position: relative; overflow: hidden; .door-top { width: 100%; height: 30px; border-top: 4pxsolid#f5a672;border-bottom: 4pxsolid#f5a672;background: #89544c;position: absolute; top: 0; left: 0; z-index: 99; } .door-line { background: #673a35;position: absolute; z-index: 89; &-left{ width: 15px; height: 100%; top: 0; left: 0; border-right: 2pxsolid#815345; } &-right{ width: 15px; height: 100%; top: 0; right: 0; border-left: 2pxsolid#815345; } &-bottom{ width: 100%; height: 15px; bottom: 0; left: 0; z-index: 87; border-top: 2pxsolid#815345; } } .door-frame { width: 100%; height: 100%; position: absolute; top: 0; left: 0; .stick-h { width: 6px; height: 100%; background: #774747;position: absolute; top: 50px; } .stick-h1 { left: 30px; } .stick-h2 { left: 70px; } .stick-h3 { left: 85px; } .stick-h4 { left: 100px; } .stick-h5 { left: 115px; } .stick-h6 { left: 130px; } .stick-h7 { right: 130px; } .stick-h8 { right: 115px; } .stick-h9 { right: 100px; } .stick-h10 { right: 85px; } .stick-h11 { right: 70px; } .stick-h12 { right: 30px; } .stick-d { width: 30px; height: 6px; background: #774747;position: absolute; } .stick-d1 { width: 100%; top: 50px; left: 0; } .stick-d2 { top: 65px; left: 86px; } .stick-d3 { width: 20px; top: 80px; left: 70px; } .stick-d4 { top: 65px; right: 86px; } .stick-d5 { width: 20px; top: 80px; right: 70px; } .stick-d6 { width: 100%; bottom: 30px; left: 0; } } .door-opening { width: 300px; height: 300px; border-radius: 50%; position: absolute; top: 35px; left: 50%; margin-left: -150px; background: #7c5655;overflow: hidden; &-center{ width: 250px; height: 250px; border-radius: 50%; position: absolute; top: 25px; left: 25px; background: #fff; } &-decorate { width: 50px; height: 80px; border-radius: 50%; background: #f3c068;position: absolute; } &-decorate1 { left: -30px; top: 100px; } &-decorate2 { left: 50%; top: -43px; margin-left: -25px; transform: rotate(90deg); } &-decorate3 { right: -30px; top: 100px; } &-flowers { position: absolute; bottom: 55px; right: 43px; .flowercluster { transform: scale(0.85); } } } } } .floor { width: 100%; height: 300px; position: relative; background: #946962;overflow: hidden; &-line { width: 1px; height: 100%; background: linear-gradient( tobottom, #b48e5e 20%, #eebe88 40%, #fce49c 60%, #9f725a 80%, #f7c887 100%);position: absolute; top: 0; } &-line1 { left: 0; transform: rotate(10deg); } &-line2 { left: 10%; transform: rotate(10deg); } &-line3 { left: 23%; transform: rotate(5deg); } &-line4 { left: 34%; transform: rotate(2deg); } &-line5 { left: 45%; } &-line6 { right: 43%; transform: rotate(-2deg); } &-line7 { right: 32%; transform: rotate(-5deg); } &-line8 { right: 20%; transform: rotate(-8deg); } &-line9 { right: 10%; transform: rotate(-10deg); } &-line10 { right: 0; transform: rotate(-10deg); } } } &-cat { width: 200px; height: 60px; position: absolute; top: 95px; right: 10px; .body { width: 110px; height: 50px; background-color: #745341;position: absolute; top: -4px; border-top-left-radius: 90px; border-top-right-radius: 90px; animation: catbody10snoneinfinite; } @keyframescatbody { 5% { transform: scaleY(1); } 10% { transform: scaleY(1.15); } 15% { transform: scaleY(1); } 20% { transform: scaleY(1.25); } 25% { transform: scaleY(1); } 30% { transform: scaleY(1.15); } 40% { transform: scaleY(1); } 50% { transform: scaleY(1.15); } } .head { width: 70px; height: 34px; background-color: #745341;position: absolute; top: 13px; left: -45px; border-top-left-radius: 70px; border-top-right-radius: 70px; } .ear { width: 0; height: 0; position: absolute; left: 5px; top: -4px; border-left: 12pxsolidtransparent; border-right: 12pxsolidtransparent; border-bottom: 20pxsolid#745341;transform: rotate(-30deg); animation: catearleft10sbothinfinite; } .ear-right { top: -11px; left: 21px; animation: catearright10sbothinfinite; } @keyframescatearleft { 0% { transform: rotate(-20deg); } 5% { transform: rotate(-5deg); } 15% { transform: rotate(-15deg); } 25% { transform: rotate(-15deg); } 35% { transform: rotate(-30deg); } 40% { transform: rotate(-30deg); } 45% { transform: rotate(0deg); } 50% { transform: rotate(0deg); } 80% { transform: rotate(-15deg); } 90% { transform: rotate(-5deg); } 100% { transform: rotateZ(-5deg); } } @keyframescatearright { 0% { transform: rotateZ(-15deg); } 15% { transform: rotateZ(-20deg); } 25% { transform: rotateZ(-20deg); } 30% { transform: rotateZ(-30deg); } 34% { transform: rotateZ(-20deg); } 38% { transform: rotateZ(-30deg); } 40% { transform: rotateZ(-20deg); } 42% { transform: rotateZ(-20deg); } 44% { transform: rotateZ(-30deg); } 45% { transform: rotateZ(-20deg); } 50% { transform: rotateZ(-10deg); } 55% { transform: rotateZ(-10deg); } 60% { transform: rotateZ(-20deg); } 61% { transform: rotateZ(-30deg); } 62% { transform: rotateZ(-20deg); } 63% { transform: rotateZ(-20deg); } 64% { transform: rotateZ(-30deg); } 65% { transform: rotateZ(-20deg); } 80% { transform: rotateZ(-20deg); } 90% { transform: rotateZ(-15deg); } 100% { transform: rotateZ(-15deg); } } .nose { width: 5px; height: 5px; background-color: #dc9d90;position: absolute; bottom: 10px; left: 30px; border-radius: 50%; } .whisker { width: 16px; height: 10px; position: absolute; bottom: 5px; left: 7px; transform-origin: right; } .whisker::before, .whisker::after { content: ''; width: 100%; position: absolute; top: 0; border: 1pxsolid#fff;transform-origin: 100%0; transform: rotate(10deg); } .whisker::after { transform: rotate(-20deg); } .whisker-left { animation: catwhiskerleft10sbothinfinite; } .whisker-right { left: 27px; bottom: 12px; transform: rotate(180deg); animation: catwhiskerright10sbothinfinite; } @keyframescatwhiskerleft { 5% { transform: rotate(0); } 10% { transform: rotate(0deg); } 15% { transform: rotate(-5deg); } 20% { transform: rotate(0deg); } 25% { transform: rotate(0deg); } 30% { transform: rotate(10deg); } 40% { transform: rotate(-5deg); } 50% { transform: rotate(10deg); } } @keyframescatwhiskerright { 5% { transform: rotate(180deg); } 10% { transform: rotate(190deg); } 15% { transform: rotate(180deg); } 20% { transform: rotate(175deg); } 25% { transform: rotate(190deg); } 30% { transform: rotate(180deg); } 40% { transform: rotate(185deg); } 50% { transform: rotate(175deg); } } .tail { width: 14px; height: 100px; position: absolute; top: 42px; right: 90px; z-index: 99; } .tail-line { width: 14px; height: 60px; background: #745341;position: absolute; left: 0; top: 0; z-index: 99; } .tail-round { width: 48px; height: 48px; background: #745341;position: absolute; top: 36px; left: -34px; border-radius: 50%; } .tail-round::before { content: ''; width: 20px; height: 20px; background: #946962;position: absolute; top: 14px; left: 14px; border-radius: 50%; } .tail-round::after { content: ''; width: 48px; height: 22px; background: #946962;position: absolute; top: 0; left: 0; } .tail-end { width: 14px; height: 10px; background: #745341;border-radius: 14px14px00; position: absolute; bottom: 39px; left: -34px; z-index: 99; } } &-table { width: 200px; height: 20px; background-color: #e3895e;position: absolute; top: 140px; right: 80px; border-radius: 20px; z-index: 9; } &-entrance { position: absolute; top: 60px; left: 35px; .entrance-btn { width: 180px; line-height: 28px; font-size: 16px; font-weight: 600; color: #fff;border: 0; background-image: linear-gradient(toright, #ed6ea0, #ec8c69, #f7186a, #FBB03B);background-size: 300%100%; box-shadow: 04px15px0#ed6ea0;margin-bottom: 20px; animation: 5sease-in-outentranceinfinite; } } } @keyframesentrance { 0% { background-image: linear-gradient(toright, #ed6ea0, #ec8c69, #f7186a, #FBB03B);background-size: 300%100%; } 100% { background-image: linear-gradient(toright, #FBB03B, #ec8c69, #f7186a, #ed6ea0);background-position: 100%0; } }
头像组件
头像组件主要包括头像图片、称号、花朵点缀三个部分。
/** * @description 头像组件 */importReactfrom'react'; import'./index.less'; importutilfrom'../../utils/util'; constAvatar= () => { constuserInfo=util.getUserInfo() || {}; constgetDesignationByZongziNum= () => { constfestival=userInfo.festival?userInfo.festival : {}; constzongzi=festival.zongzi?festival.zongzi : 0; letname='殿上佳人'; if (zongzi<50) { name='殿上佳人'; } elseif (zongzi<=100) { name='淑仪倾城'; } elseif (zongzi<=200) { name='花容初绽'; } elseif (zongzi<=300) { name='花成蜜就'; } elseif (zongzi<=400) { name='宠冠六宫'; } elseif (zongzi>400) { name='凤仪千载'; } returnname; }; return ( <divclassName='avatar'><imgclassName='avatar-img'src='https://p6-passport.byteacctimg.com/img/user-avatar/c6c1a335a3b48adc43e011dd21bfdc60~300x300.image'alt=''/><divclassName='avatar-nickname'>叶一一</div><divclassName='avatar-designation'><span>{getDesignationByZongziNum()}</span><divclassName='avatar-flower'><divclassName='avatar-flower-leaf avatar-flower-leaf1'></div><divclassName='avatar-flower-leaf avatar-flower-leaf2'></div><divclassName='avatar-flower-leaf avatar-flower-leaf3'></div><divclassName='avatar-flower-leaf avatar-flower-leaf4'></div><divclassName='avatar-flower-leaf avatar-flower-leaf5'></div><divclassName='avatar-flower-circle'></div></div></div></div> ); }; exportdefaultAvatar;
花丛组件
这个是参考的网站是的,参考地址我放到了文章末尾。
/** * @description 花丛组件 */importReactfrom'react'; import'./index.less'; constFlowerCluster= () => { return ( <divclassName='flowercluster'><divclassName='flower-leaves'></div><divclassName='bunch'><divclassName='flower'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='flower'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='flower'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div></div></div> ); }; exportdefaultFlowerCluster;
最终UI
设计为古代的室内,参考的《美人传》小游戏中的UI设计,包括木质的墙壁、门和地板。除此之外还加了一些动画效果增加趣味性:
- 称号上面加了一个花朵做装饰;
- 任务和活动入口上加了光效闪动的效果;
- 地板上的猫咪耳朵和肚子随着呼吸而动;
日常任务
日常任务收集规则
- 每天0点开始进行资源生产,每个小时生产1万资源,不足1个小时的时候不产生,满足1个小时的时候产生;
- 可以进行资源收集,每次收集完成,对应的资源值进行叠加;
- 不同资源收集时,随机掉落不同的活动材料。对应如下:
任务名称 |
活动材料名称 |
活动材料数量 |
开源节流 |
粽叶 |
5~10 |
助宫易物 |
糯米 |
5~10 |
布施济民 |
红枣 |
2~5 |
功能实现
日常页面
/** * @description 日常任务 */importReact, { useState, useEffect } from'react'; importclassnamesfrom'classnames'; importmomentfrom'moment'; importBackfrom'@/components/Back'; importFlowerfrom'@/components/Flower'; importFlowerTreefrom'@/components/FlowerTree'; import { Modal } from'antd-mobile'; import { QuestionCircleFill, KoubeiFill, FireFill, HeartFill } from'antd-mobile-icons'; importutilfrom'../../utils/util'; import'./index.less'; constTasks= () => { constuserInfo=util.getUserInfo() || {}; const [tasksObj, setTasksObj] =useState( userInfo.tasks?userInfo.tasks : { zheng: 0, cai: 0, mei: 0, creatAt: 0, }, ); constlistInit= [ { key: 'zheng', title: '政', name: '开源节流', num: 0, harvestFalg: true, taskKey: 'zongye', icon: <KoubeiFillfontSize={16} color='#fcb887'/>, }, { key: 'cai', title: '才', name: '助宫易物', num: 0, harvestFalg: true, taskKey: 'nuomi', icon: <FireFillfontSize={16} color='#f6f6f6'/>, }, { key: 'mei', title: '魅', name: '布施济民', num: 0, harvestFalg: true, taskKey: 'hongzao', icon: <HeartFillfontSize={16} color='#59ca94'/>, }, ]; const [list, setList] =useState(listInit); // 获取当前内务展示数据constgetNewNum= () => { // 梯龄换算成月constnewData=newDate(); letdiffData=tasksObj.creatAt; if (!tasksObj.creatAt) { // 如果收获时间默认活动开始时间diffData=moment('2022-06-01'); } lethour=moment(newData).diff(moment(diffData), 'hours'); console.log(hour, 'hour'); letnumCurr=hour*1000; constlistInit= [list]; listInit.map(item=> { item.num+=numCurr; }); setList(listInit); }; useEffect(() => { getNewNum(); }, []); // 获取随机数constgetRandomNumber=key=> { constrandomObj= { zheng: [5, 10], cai: [5, 10], mei: [2, 5], }; constrandomItem=randomObj[key]; constm=randomItem[1]; constn=randomItem[0]; letrandomNum=Math.random() * (m-n) +n; randomNum=Math.round(randomNum); console.log(randomNum, 'randomNum'); returnrandomNum; }; // 收获consthandleHarvest=index=> { constnewData=newDate(); letuserInfoInit= { userInfo }; consthandleList= [].concat(list); letitem=handleList[index]; lettasksObjInit= { tasksObj }; tasksObjInit.creatAt=newData; constfestivalObjInit=userInfo.festival?userInfo.festival : { nuomi: 0, zongye: 0, hongzao: 0, zongzi: 0, }; // 收获操作if (item.harvestFalg) { tasksObjInit[item.key] +=item.num; item.num=0; festivalObjInit[item.taskKey] =getRandomNumber(item.key); // 设置缓存userInfoInit.festival=festivalObjInit; userInfoInit.tasks=tasksObjInit; util.saveUserInfo(userInfoInit); setList(list); setTasksObj(tasksObjInit); } item.harvestFalg=!item.harvestFalg; setList(handleList); }; // 顶部提示constheadTip= () => { returnModal.show({ title: '内务', content: ( <divclassName='tasks-modal'><divclassName='tasks-modal-title'>内务打理</div><divclassName='tasks-modal-content mb10'><pclassName='mb10'>内务分为“开源节流”,“助宫易物”,“布施济民”三种类型,分别可以获得铜币、珍品和名望。</p><p>打理内务有一定几率获得包粽子的材料。</p></div><divclassName='tasks-modal-title'>内务奖励</div><divclassName='tasks-modal-content'><pclassName='mb10'>开源节流有一定几率获得粽叶。</p><pclassName='mb10'>助宫易物有一定几率获得糯米。</p><p>布施济民有一定几率获得红枣。</p></div></div> ), showCloseButton: true, }); }; // 将数据除以10000进行展示constgetTaskNumContent=num=> { num=num/10000; returnnum; }; return ( <divclassName='tasks'><Back/><divclassName='tasks-info'> {list.map(item=> { return ( <divclassName='tasks-info-item'key={item.key}><divclassName='tasks-info-item-icon'>{item.icon}</div><span> {getTaskNumContent(tasksObj[item.key])} {tasksObj[item.key] >0?'万' : ''} </span></div> ); })} </div><divclassName='tasks-head'><divclassName='tasks-head-tip'onClick={headTip}><QuestionCircleFillfontSize={28} color='#f69bad'/></div><divclassName='tasks-head-title'>内务打理</div></div><divclassName='tasks-list'> {list.map((item, index) => { return ( <divclassName='tasks-item'key={item.key}><divclassName='tasks-item-top'></div><divclassName='tasks-item-title'>{item.title}</div><divclassName='tasks-item-name'><span>{item.name}</span><divclassName='name-circular name-circular1'></div><divclassName='name-circular name-circular2'></div><divclassName='name-circular name-circular3'></div><divclassName='name-circular name-circular4'></div><divclassName='name-circular name-circular5'></div><divclassName='name-circular name-circular6'></div></div><divclassName='tasks-item-num'>{item.num}</div><divclassName={classnames('tasks-item-btn', { inactive: !item.harvestFalg })} onClick={() =>handleHarvest(index)}><divclassName='btn-flower1'><Flower/></div><divclassName='btn-flower2'><Flower/></div><span>{item.harvestFalg?'收获' : '恢复'}</span></div></div> ); })} </div><divclassName='tasks-footer'></div><divclassName='tasks-tree'><FlowerTree/></div><divclassName='tasks-rule'><divclassName='tasks-rule-title'><span>宫规</span></div><divclassName='tasks-rule-text'>内务收获+5%</div></div></div> ); }; exportdefaultTasks;
返回组件
每个二级、三级页面都会放返回按钮,所以我封装成了组件。
/** * @description 回退按钮组件 */importReactfrom'react'; importPropTypesfrom'prop-types'; import { useHistory } from'react-router-dom'; import'./index.less'; constBack= ({ props }) => { consthistory=useHistory(); const { path } =props; // 点击事件consthandleClick= () => { history.push(path); }; return ( <divclassName='back'onClick={handleClick}><divclassName='back-left'></div><divclassName='back-right'></div></div> ); }; Back.propTypes= { path: PropTypes.string, // 跳转路径}; Back.defaultProps= { path: '/home', }; exportdefaultBack;
花朵组件
有些页面需要花朵装饰,所以我把花朵封装成了组件。
/** * @description 花朵组件 */importReactfrom'react'; import'./index.less'; constFlower= () => { return ( <divclassName='flower'><divclassName='flower-leaf flower-leaf1'></div><divclassName='flower-leaf flower-leaf2'></div><divclassName='flower-leaf flower-leaf3'></div><divclassName='flower-leaf flower-leaf4'></div><divclassName='flower-leaf flower-leaf5'></div><divclassName='flower-circle'></div></div> ); }; exportdefaultFlower;
开满花的树组件
这个是参考的网站是的,参考地址我放到了文章末尾。
/** * @description 开满花的树组件 */importReactfrom'react'; import'./index.less'; constFlowerTree= () => { return ( <divclassName='flowertree'><divclassName='trunk'><divclassName='roots'><divclassName='root'></div><divclassName='root'></div><divclassName='root'></div><divclassName='root'></div><divclassName='root'></div></div></div><divclassName='leaves cherry-blossoms'><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div><divclassName='cherry-blossom'><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div><divclassName='petal'></div></div></div></div> ); }; exportdefaultFlowerTree;
公共方法
有些基础的功能、或者出现频率较高的功能,可以提炼成公共方法。
/** * @description 公共方法 */// 获取用户信息constgetUserInfo= () => { letuserInfo=localStorage.getItem('userInfo'); if (userInfo) { returnJSON.parse(userInfo); } returnnull; }; // 保存用户信息constsaveUserInfo=userInfo=> { if (userInfo) { localStorage.setItem('userInfo', JSON.stringify(userInfo)); } }; /** * 两个是否可以整除 * @param {number} num1 除数 * @param {number} num2 被除数 * @return {boolean} 是否整除的布尔值 */constgetNumDivisibleFlag= (num1, num2) => { letflag=false; // 如果除数小于被除数 则表示不可以被整除if (num1>num2&&num1/num2>1) { flag=true; } returnflag; }; exportdefault { getUserInfo, saveUserInfo, getNumDivisibleFlag };
最终UI
端午活动
活动规则
活动时间
1.2022-5-31 至 2022-6-5,提前预热3天。
2.页面上设置活动倒计时
- 活动结束前,展示距离活动结束还剩多长时间,时间格式为DD天 hh:mm:ss;
- 活动结束后,展示内容为"活动已结束";
兑换规则
食材兑换比例
粽子类型 |
需要材料 |
糯米粽子 |
10 * 糯米 + 2 * 粽叶 + 2 * 红枣 |
食材兑换规则
- 通过页面按钮进行兑换,当食材数量不足时,按钮不可点击,当食材数量充足时可以进行点击。
- 点击兑换按钮唤起兑换弹窗,可以通过加减号进行兑换数量的修改,当达到最大可兑换值时,加号不可点击。
- 确定兑换之后,粽子数量增加,食材数量对应减少。
功能实现
活动页面
/** * @description 端午活动 */importReact, { useEffect, useState } from'react'; importclassnamesfrom'classnames'; importmomentfrom'moment'; importBackfrom'@/components/Back'; import { Modal, Stepper } from'antd-mobile'; import { QuestionCircleFill } from'antd-mobile-icons'; importutilfrom'../../utils/util'; import'./index.less'; constFestival= () => { constuserInfo=util.getUserInfo() || {}; const [festivalObj, setFestivalObj] =useState( userInfo.festival?userInfo.festival : { nuomi: 20, zongye: 10, hongzao: 10, zongzi: 150, }, ); constlist= [ { key: 'nuomi', name: '糯米', }, { key: 'zongye', name: '粽叶', }, { key: 'hongzao', name: '红枣', }, { key: 'zongzi', name: '粽子', }, ]; // 是否可以进行兑换操作的布尔值 true-能 false-不能const [activeFlag, setActiveFlag] =useState(false); const [visible, setVisible] =useState(false); const [count, setCount] =useState(0); // 兑换的粽子数量const [convertNum, setConvertNum] =useState(1); const [countdown, setCountdown] =useState(''); lettimer=null; // 获取当前兑换按钮是否可以点击constgetInactiveFlag=festivalObj=> { letactiveInit=false; letnuomi=festivalObj.nuomi; letzongye=festivalObj.zongye; lethongzao=festivalObj.hongzao; if (nuomi&&zongye&&hongzao) { letnuomiFlag=util.getNumDivisibleFlag(nuomi, 10); letzongyeFlag=util.getNumDivisibleFlag(zongye, 2); lethongzaoFlag=util.getNumDivisibleFlag(hongzao, 2); if (nuomiFlag&&zongyeFlag&&hongzaoFlag) { activeInit=true; } } setActiveFlag(activeInit); }; constgetCountdown= () => { letnowDate=newDate(); // console.log(nowDate, 'nowDate');// 获取的2022-06-05的23:59:59的时间戳letendTime=moment('2022-06-05').endOf('day').format('x'); letcountdownInit=''; // 剩余时间 毫秒letsurplusTime=endTime-nowDate.getTime(); if (surplusTime<=0) { clearTimeout(timer); countdownInit='活动已结束'; setCountdown(countdownInit); } else { // 剩余时间 秒letrunTime=surplusTime/1000; constday=Math.floor(runTime/86400); runTime=runTime%86400; consthour=Math.floor(runTime/3600); runTime=runTime%3600; constminute=Math.floor(runTime/60); runTime=runTime%60; constsecond=Math.floor(runTime); constdayText=day?`${day}天` : ''; countdownInit=`剩余时间:${dayText}${hour}:${minute}:${second}`; setCountdown(countdownInit); timer=setTimeout(getCountdown, 1000); } }; useEffect(() => { getInactiveFlag(festivalObj); getCountdown(); }, []); useEffect(() => { // 清除定时return () => { clearInterval(timer); }; }, []); // 顶部提示constheadTip= () => { returnModal.show({ title: '"粽"得凤仪', content: ( <divclassName='festival-modal'><divclassName='festival-modal-title'>合成粽子</div><divclassName='festival-modal-content mb10'><pclassName='mb10'>10*糯米+2*粽叶+2*红枣可以兑换1个糯米粽子。</p><p>当糯米、粽叶、红枣的比例不是5:1:1时,无法进行兑换。</p></div><divclassName='festival-modal-title'>称号奖励</div><divclassName='festival-modal-content'><pclassName='mb10'>当前粽子数量达到50个可获得称号“淑仪倾城”。</p><pclassName='mb10'>当前粽子数量达到100个可获得称号“花容初绽”。</p><pclassName='mb10'>当前粽子数量达到200个可获得称号“花成蜜就”。</p><pclassName='mb10'>当前粽子数量达到300个可获得称号“宠冠六宫”。</p><pclassName='mb10'>当前粽子数量达到400个可获得称号“凤仪千载”。</p><p>称号自动获取无需额外操作</p></div></div> ), showCloseButton: true, }); }; // 粽子展示constzongziContent= () => { return ( <divclassName='festival-zongzi'><divclassName='festival-zongzi-left'></div><divclassName='festival-zongzi-center'></div><divclassName='festival-zongzi-right'></div></div> ); }; // 兑换确定操作constconvertOnConfirm= () => { setVisible(false); letfestivalObjInit= { festivalObj }; console.log(convertNum, 'convertNum'); festivalObjInit.nuomi-=convertNum*10; festivalObjInit.zongye-=convertNum*2; festivalObjInit.hongzao-=convertNum*2; festivalObjInit.zongzi+=convertNum; console.log(festivalObjInit, 'festivalObjInit'); // 设置缓存letuserInfoInit= { userInfo }; userInfoInit.festival=festivalObjInit; util.saveUserInfo(userInfoInit); setFestivalObj(festivalObjInit); getInactiveFlag(festivalObjInit); }; // 获取可以兑换的数量constgetConvertCount= () => { letnuomi=festivalObj.nuomi; letzongye=festivalObj.zongye; lethongzao=festivalObj.hongzao; letnuomiNum=Math.floor((nuomi*100) / (10*100)); letzongyeNum=Math.floor((zongye*100) / (2*100)); lethongzaoNum=Math.floor((hongzao*100) / (2*100)); returnMath.min(nuomiNum, zongyeNum, hongzaoNum); }; // 兑换操作consthandleConvert= () => { if (!activeFlag) return; constcount=getConvertCount(); setConvertNum(1); setCount(count); setVisible(true); }; return ( <divclassName='festival'><divclassName='festival-content'><Back/><divclassName='festival-head'><divclassName='festival-head-tip'onClick={headTip}><QuestionCircleFillfontSize={28} color='#f69bad'/></div><divclassName='festival-head-title'>"粽"得凤仪</div></div><divclassName='festival-time'>{countdown}</div><divclassName='festival-convert'><divclassName='festival-convert-zongzi'>{zongziContent()}</div><divclassName='festival-convert-zongzi2'>{zongziContent()}</div><divclassName='festival-convert-num'> {list.map(item=> { return ( <divclassName='festival-convert-num-item'key={item.key}> {item.name}: {festivalObj[item.key]} </div> ); })} </div><divclassName='festival-convert-rule'><divclassName='festival-convert-rule-nuomi'></div> x 10<div className='festival-convert-rule-add'></div><divclassName='festival-convert-rule-zongye'></div> x 2<div className='festival-convert-rule-add'></div><divclassName='festival-convert-rule-hongzao'></div> x 2</div><divclassName={classnames('festival-convert-btn', { inactive: !activeFlag })} onClick={handleConvert}>兑换</div></div></div><divclassName='festival-room'><divclassName='festival-room-wall'><divclassName='wall-poetry'><divclassName='wall-poetry-nail'></div><divclassName='wall-poetry-shaft wall-poetry-shaft-top'></div><divclassName='wall-poetry-inner'><divclassName='wall-poetry-title'>浣溪沙·端午</div><divclassName='wall-poetry-author'>宋·苏轼</div><divclassName='wall-poetry-content'>轻汗微微透碧纨,明朝端午浴芳兰。流香涨腻满晴川。彩线轻缠红玉臂,小符斜挂绿云鬟。佳人相见一千年。</div></div><divclassName='wall-poetry-shaft wall-poetry-shaft-bottom'></div></div></div><divclassName='festival-room-floor'><divclassName='floor-line floor-line1'></div><divclassName='floor-line floor-line2'></div><divclassName='floor-line floor-line3'></div><divclassName='floor-line floor-line4'></div><divclassName='floor-line floor-line5'></div><divclassName='floor-line floor-line6'></div><divclassName='floor-line floor-line7'></div><divclassName='floor-line floor-line8'></div><divclassName='floor-line floor-line9'></div><divclassName='floor-line floor-line10'></div></div></div><Modalvisible={visible} content={ <divclassName='festival-modal'><divclassName='festival-modal-text'>最多可以兑换: {count}</div><divclassName='festival-modal-stepper'><Stepperstep={1} value={convertNum} min={1} max={count} onChange={value=> { setConvertNum(value); }} /></div><divclassName='festival-modal-confirm'onClick={() =>convertOnConfirm()}>兑换</div></div> } showCloseButton={true} closeOnActiononClose={() => { setVisible(false); }} /></div> ); }; exportdefaultFestival;
最终UI
活动展示
兑换弹窗展示
总结
本次收获还是挺多的。
- CSS用的别以前熟练了很多,这次的游戏里除了头像图片、一颗树、一簇花,其他的都是我用CSS写出来的,没有用图片素材,实现过程不断收获新的创意。说起来多亏这段时间码上掘金活动,我才能使用CSS实现功能做的如此之快,ღ( ´・ᴗ・` );
- 游戏设计,体验了一把产品/策划的感觉,站在不同的角度去思考需要实现的功能,锻炼逻辑思维,很有收获;
- 核心功能的实现,包括内务收集的计算、食材的随机掉落计算、粽子兑换的计算等多个计算功能,虽然方法可能不是最优,但是在遇到类似的功能实现算是有经验了;
还差一个github的地址,等有时间我把所有代码上传后,补充一下github地址。