「前端游戏开发体验」我用react实现网页游戏的全过程(包括规则设计)

简介: 用技术实现梦想,用梦想打开创意之门。游戏开发体验挺新奇的,我用react实现网页游戏的全过程(包括规则设计)。

关于游戏的灵感来源

今年元宵节的时候,我玩的小游戏里面有限时任务,可以解锁节日限定物品,于是那几天我玩的很欢乐很积极。端午节到来之前,我想玩一下身份转换,从玩家转换到游戏策划。一个有趣的想法在脑海中逐渐清晰。

假如我是游戏策划

假如我是游戏策划,首先会对自己灵魂三连问:活动内容什么?活动怎么玩?活动奖励是什么?

现有大体的想法,然后再拆分到各个细节中去。

因为游戏中的一些场景搭配、日常活动名称、称号等借鉴了我最近沉迷的游戏《美人传》,所以这次的游戏仅供学习练习,不做任何商业用途。

产品视角

站在产品的角度思考活动设计,我的产品视角是这样的:

一入夏,就盼着假期,过了五一很快就会到端午,一想到端午就不由自主的想到美味的粽子。所以端午的活动就来了,包粽子。众所周知,包粽子需要糯米、粽叶等必备材料,而粽子的内馅有很多种,本次活动中需要的是红枣。所以包粽子的材料就选定了糯米、粽叶、红枣三种。(活动内容是什么)

游戏中有日常收集任务,每个收集任务掉落的材料都是固定的。活动期间一般会增加活动材料限时掉落,所以在活动期间,日常收集时会掉落包粽子需要的材料,不同收集任务掉落不同材料。(活动怎么玩)

粽子积累到一定数量就可以兑换节日限定物品。一般游戏中的节日限定物品都是精心设计的,但是由于时间和精力有限,我这次活动设计的比较简单,不同数量的粽子可以兑换不同的称号,最高称号为“荣宠万千”。(活动奖励是什么)

(^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 * 红枣

食材兑换规则

  1. 通过页面按钮进行兑换,当食材数量不足时,按钮不可点击,当食材数量充足时可以进行点击。
  2. 点击兑换按钮唤起兑换弹窗,可以通过加减号进行兑换数量的修改,当达到最大可兑换值时,加号不可点击。
  3. 确定兑换之后,粽子数量增加,食材数量对应减少。

功能实现

活动页面

/** * @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

活动展示

兑换弹窗展示

总结

本次收获还是挺多的。

  1. CSS用的别以前熟练了很多,这次的游戏里除了头像图片、一颗树、一簇花,其他的都是我用CSS写出来的,没有用图片素材,实现过程不断收获新的创意。说起来多亏这段时间码上掘金活动,我才能使用CSS实现功能做的如此之快,ღ( ´・ᴗ・` );
  2. 游戏设计,体验了一把产品/策划的感觉,站在不同的角度去思考需要实现的功能,锻炼逻辑思维,很有收获;
  3. 核心功能的实现,包括内务收集的计算、食材的随机掉落计算、粽子兑换的计算等多个计算功能,虽然方法可能不是最优,但是在遇到类似的功能实现算是有经验了;

还差一个github的地址,等有时间我把所有代码上传后,补充一下github地址。

参考文章

目录
相关文章
|
13天前
|
前端开发 测试技术 开发工具
探索前端框架React Hooks的优势与应用
本文将深入探讨前端框架React Hooks的优势与应用。通过分析React Hooks的特性以及实际应用案例,帮助读者更好地理解和运用这一现代化的前端开发工具。
|
5天前
|
开发框架 Dart 前端开发
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
【4月更文挑战第30天】对比 Flutter(Dart,强类型,Google支持,快速热重载,高性能渲染)与 React Native(JavaScript,庞大生态,热重载,依赖原生渲染),文章讨论了开发语言、生态系统、性能、开发体验、学习曲线、社区支持及项目选择因素。两者各有优势,选择取决于项目需求、团队技能和长期维护考虑。参考文献包括官方文档和性能比较文章。
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
|
5天前
|
前端开发 JavaScript 开发者
【专栏:HTML与CSS前端技术趋势篇】前端框架(React/Vue/Angular)与HTML/CSS的结合使用
【4月更文挑战第30天】前端框架React、Vue和Angular助力UI开发,通过组件化、状态管理和虚拟DOM提升效率。这些框架与HTML/CSS结合,使用模板语法、样式管理及组件化思想。未来趋势包括框架简化、Web组件标准采用和CSS在框架中角色的演变。开发者需紧跟技术发展,掌握新工具,提升开发效能。
|
7天前
|
开发框架 缓存 前端开发
|
10天前
|
前端开发 JavaScript Linux
relectron框架——打包前端vue3、react为pc端exe可执行程序
relectron框架——打包前端vue3、react为pc端exe可执行程序
23 1
|
10天前
|
前端开发 数据可视化 API
前端react 18.2整合ckeditor富文本编辑器——配置插件、自定义toolbar工具栏(二)
前端react 18.2整合ckeditor富文本编辑器——配置插件、自定义toolbar工具栏
17 0
前端react 18.2整合ckeditor富文本编辑器——配置插件、自定义toolbar工具栏(二)
|
10天前
|
前端开发 JavaScript CDN
前端react 18.2整合ckeditor富文本编辑器——配置插件、自定义toolbar工具栏(一)
前端react 18.2整合ckeditor富文本编辑器——配置插件、自定义toolbar工具栏
22 0
|
2月前
|
前端开发 JavaScript C++
探讨前端框架选择:Vue vs React
在当今快速发展的前端开发领域,选择合适的前端框架至关重要。本文将围绕Vue和React这两大热门前端框架展开讨论,分析它们的特点、优势和适用场景,帮助开发者更好地做出选择。
|
2月前
|
前端开发 JavaScript 安全
使用React、TypeScript和Ant Design构建现代化前端应用
使用React、TypeScript和Ant Design构建现代化前端应用
29 0
|
2月前
|
开发框架 前端开发 JavaScript
深入探究React:前端开发的利器
深入探究React:前端开发的利器
22 1