天天看點

「前端遊戲開發體驗」我用react實作網頁遊戲的全過程(包括規則設計)

關于遊戲的靈感來源

今年元宵節的時候,我玩的小遊戲裡面有限時任務,可以解鎖節日限定物品,于是那幾天我玩的很歡樂很積極。端午節到來之前,我想玩一下身份轉換,從玩家轉換到遊戲策劃。一個有趣的想法在腦海中逐漸清晰。

假如我是遊戲策劃

假如我是遊戲策劃,首先會對自己靈魂三連問:活動内容什麼?活動怎麼玩?活動獎勵是什麼?

現有大體的想法,然後再拆分到各個細節中去。

因為遊戲中的一些場景搭配、日常活動名稱、稱号等借鑒了我最近沉迷的遊戲《美人傳》,是以這次的遊戲僅供學習練習,不做任何商業用途。

産品視角

站在産品的角度思考活動設計,我的産品視角是這樣的:

一入夏,就盼着假期,過了五一很快就會到端午,一想到端午就不由自主的想到美味的粽子。是以端午的活動就來了,包粽子。衆所周知,包粽子需要糯米、粽葉等必備材料,而粽子的内餡有很多種,本次活動中需要的是紅棗。是以包粽子的材料就標明了糯米、粽葉、紅棗三種。(活動内容是什麼)

遊戲中有日常收集任務,每個收集任務掉落的材料都是固定的。活動期間一般會增加活動材料限時掉落,是以在活動期間,日常收集時會掉落包粽子需要的材料,不同收集任務掉落不同材料。(活動怎麼玩)

粽子積累到一定數量就可以兌換節日限定物品。一般遊戲中的節日限定物品都是精心設計的,但是由于時間和精力有限,我這次活動設計的比較簡單,不同數量的粽子可以兌換不同的稱号,最高稱号為“榮寵萬千”。(活動獎勵是什麼)

(^U^)ノ~YO,一切準備就緒,開始幹活。

互動設計

大緻畫了一下設計草圖,幫助理清楚布局思路。(第一次畫,還有待提高。)

首頁

「前端遊戲開發體驗」我用react實作網頁遊戲的全過程(包括規則設計)

日常任務

「前端遊戲開發體驗」我用react實作網頁遊戲的全過程(包括規則設計)

端午活動

「前端遊戲開發體驗」我用react實作網頁遊戲的全過程(包括規則設計)

功能設計

首頁

内容

主要包括使用者資訊、任務入口、活動入口等展示。

稱号規則

稱号和糯米粽子數量對應如下:

稱号 糯米粽子數量
殿上佳人 <50
淑儀傾城 >=50 && < 100
花容初綻 >=100 && < 200
花成蜜就 >=200 && < 300
寵冠六宮 >=300 && < 400
鳳儀千載 >=400

功能實作

首頁頁面
/**
 * @description 首頁
 */
import React from 'react';
import { useHistory } from 'react-router-dom';
import Avatar from '@/components/Avatar';
import FlowerCluster from '@/components/FlowerCluster';
import { Button } from 'antd-mobile';
import './index.less';
const Home = () => {
  const history = useHistory();
  // 頁面跳轉
  const goTo = path => {
    history.push(path);
  };
  // 入口展示
  const entranceContent = () => {
    return (
      <div className='home-entrance'>
        <Button block shape='rounded' className='entrance-btn' onClick={() => goTo('/tasks')}>
          日常任務
        </Button>
        <Button block shape='rounded' className='entrance-btn' onClick={() => goTo('/festival')}>
          端午活動
        </Button>
      </div>
    );
  };
  return (
    <div className='home'>
      <div className='home-head'>
        <Avatar />
      </div>
      <div className='home-center'></div>
      <div className='home-bg'>
        {/* 門 */}
        <div className='door'>
          <div className='door-beam'>
            <div className='tiaoliang'></div>
          </div>
          <div className='door-frame'>
            <div className='door-top'></div>
            <div className='door-line door-line-left'></div>
            <div className='door-line door-line-right'></div>
            <div className='door-line door-line-bottom'></div>
            <div className='door-frame'>
              <div className='stick-h stick-h1'></div>
              <div className='stick-h stick-h2'></div>
              <div className='stick-h stick-h3'></div>
              <div className='stick-h stick-h4'></div>
              <div className='stick-h stick-h5'></div>
              <div className='stick-h stick-h6'></div>
              <div className='stick-h stick-h7'></div>
              <div className='stick-h stick-h8'></div>
              <div className='stick-h stick-h9'></div>
              <div className='stick-h stick-h10'></div>
              <div className='stick-h stick-h11'></div>
              <div className='stick-h stick-h12'></div>
              <div className='stick-d stick-d1'></div>
              <div className='stick-d stick-d2'></div>
              <div className='stick-d stick-d3'></div>
              <div className='stick-d stick-d4'></div>
              <div className='stick-d stick-d5'></div>
              <div className='stick-d stick-d6'></div>
            </div>
            <div className='door-opening'>
              <div className='door-opening-center'>{entranceContent()}</div>
              <div className='door-opening-decorate door-opening-decorate1'></div>
              <div className='door-opening-decorate door-opening-decorate2'></div>
              <div className='door-opening-decorate door-opening-decorate3'></div>
              <div className='door-opening-flowers'>
                <FlowerCluster />
              </div>
            </div>
          </div>
        </div>
        {/* 地闆 */}
        <div className='floor'>
          <div className='floor-line floor-line1'></div>
          <div className='floor-line floor-line2'></div>
          <div className='floor-line floor-line3'></div>
          <div className='floor-line floor-line4'></div>
          <div className='floor-line floor-line5'></div>
          <div className='floor-line floor-line6'></div>
          <div className='floor-line floor-line7'></div>
          <div className='floor-line floor-line8'></div>
          <div className='floor-line floor-line9'></div>
          <div className='floor-line floor-line10'></div>
          <div className='home-cat'>
            <div className='body'></div>
            <div className='head'>
              <div className='ear ear-left'></div>
              <div className='ear ear-right'></div>
              <div className='nose'></div>
              <div className='whisker whisker-left'></div>
              <div className='whisker whisker-right'></div>
            </div>
            <div className='tail'>
              <div className='tail-line'></div>
              <div className='tail-round'></div>
              <div className='tail-end'></div>
            </div>
          </div>
          <div className='home-table'></div>
        </div>
      </div>
    </div>
  );
};
export default Home;      

樣式

.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:3px solid #9b6d59;
        background: #825146;
        position: relative;
        .tiaoliang {
          width: 100%;
          height: 50px;
          background: #4e2e29;
          background-image: repeating-linear-gradient(45deg, transparent, transparent 13px, #9b6d59 13px, #9b6d59 15px), repeating-linear-gradient(-45deg, transparent, transparent 13px, #9b6d59 13px, #9b6d59 15px);
          border-top: 5px solid #f5a672;
          border-bottom: 5px solid #f5a672;
          position: absolute;
          top: 20px;
          left: 0;
        }
      }
      &-frame {
        width: 100%;
        height: 300px;
        position: relative;
        overflow: hidden;
        .door-top {
          width: 100%;
          height: 30px;
          border-top: 4px solid #f5a672;
          border-bottom: 4px solid #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: 2px solid #815345;
          }
          &-right{
            width: 15px;
            height: 100%;
            top: 0;
            right: 0;
            border-left: 2px solid #815345;
          }
          &-bottom{
            width: 100%;
            height: 15px;
            bottom: 0;
            left: 0;
            z-index: 87;
            border-top: 2px solid #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( to bottom, #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: catbody 10s none infinite;
    }
    @keyframes catbody {
      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: 12px solid transparent;
      border-right: 12px solid transparent;
      border-bottom: 20px solid #745341;
      transform: rotate(-30deg);
      animation: catearleft 10s both infinite;
    }
    .ear-right {
      top: -11px;
      left: 21px;
      animation: catearright 10s both infinite;
    }
    @keyframes catearleft {
      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);
      }
    }
    @keyframes catearright {
      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: 1px solid #fff;
      transform-origin: 100% 0;
      transform: rotate(10deg);
    }
    .whisker::after {
      transform: rotate(-20deg);
    }
    .whisker-left {
      animation: catwhiskerleft 10s both infinite;
    }
    .whisker-right {
      left: 27px;
      bottom: 12px;
      transform: rotate(180deg);
      animation: catwhiskerright 10s both infinite;
    }
    @keyframes catwhiskerleft {
      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);
      }
    }
    @keyframes catwhiskerright {
      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: 14px 14px 0 0;
      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(to right, #ed6ea0, #ec8c69, #f7186a, #FBB03B);
      background-size: 300% 100%;
      box-shadow: 0 4px 15px 0 #ed6ea0;
      margin-bottom: 20px;
      animation: 5s ease-in-out entrance infinite;
    }
  }
}
@keyframes entrance {
  0% {
    background-image: linear-gradient(to right, #ed6ea0, #ec8c69, #f7186a, #FBB03B);
    background-size: 300% 100%;
  }
  100% {
    background-image: linear-gradient(to right, #FBB03B, #ec8c69, #f7186a, #ed6ea0);
    background-position: 100% 0;
  }
}      
頭像元件

頭像元件主要包括頭像圖檔、稱号、花朵點綴三個部分。

/**
 * @description 頭像元件
 */
import React from 'react';
import './index.less';
import util from '../../utils/util';
const Avatar = () => {
  const userInfo = util.getUserInfo() || {};
  const getDesignationByZongziNum = () => {
    const festival = userInfo.festival ? userInfo.festival : {};
    const zongzi = festival.zongzi ? festival.zongzi : 0;
    let name = '殿上佳人';
    if (zongzi < 50) {
      name = '殿上佳人';
    } else if (zongzi <= 100) {
      name = '淑儀傾城';
    } else if (zongzi <= 200) {
      name = '花容初綻';
    } else if (zongzi <= 300) {
      name = '花成蜜就';
    } else if (zongzi <= 400) {
      name = '寵冠六宮';
    } else if (zongzi > 400) {
      name = '鳳儀千載';
    }
    return name;
  };
  return (
    <div className='avatar'>
      <img className='avatar-img' src='https://p6-passport.byteacctimg.com/img/user-avatar/c6c1a335a3b48adc43e011dd21bfdc60~300x300.image' alt='' />
      <div className='avatar-nickname'>葉一一</div>
      <div className='avatar-designation'>
        <span>{getDesignationByZongziNum()}</span>
        <div className='avatar-flower'>
          <div className='avatar-flower-leaf avatar-flower-leaf1'></div>
          <div className='avatar-flower-leaf avatar-flower-leaf2'></div>
          <div className='avatar-flower-leaf avatar-flower-leaf3'></div>
          <div className='avatar-flower-leaf avatar-flower-leaf4'></div>
          <div className='avatar-flower-leaf avatar-flower-leaf5'></div>
          <div className='avatar-flower-circle'></div>
        </div>
      </div>
    </div>
  );
};
export default Avatar;      
花叢元件

這個是參考的網站是的,參考位址我放到了文章末尾。

/**
 * @description 花叢元件
 */
import React from 'react';
import './index.less';
const FlowerCluster = () => {
  return (
    <div className='flowercluster'>
      <div className='flower-leaves'></div>
      <div className='bunch'>
        <div className='flower'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='flower'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='flower'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
      </div>
    </div>
  );
};
export default FlowerCluster;      

最終UI

設計為古代的室内,參考的《美人傳》小遊戲中的UI設計,包括木質的牆壁、門和地闆。除此之外還加了一些動畫效果增加趣味性:

  • 稱号上面加了一個花朵做裝飾;
  • 任務和活動入口上加了光效閃動的效果;
  • 地闆上的貓咪耳朵和肚子随着呼吸而動;
「前端遊戲開發體驗」我用react實作網頁遊戲的全過程(包括規則設計)

日常任務

日常任務收集規則

  • 每天0點開始進行資源生産,每個小時生産1萬資源,不足1個小時的時候不産生,滿足1個小時的時候産生;
  • 可以進行資源收集,每次收集完成,對應的資源值進行疊加;
  • 不同資源收集時,随機掉落不同的活動材料。對應如下:
任務名稱 活動材料名稱 活動材料數量
開源節流 粽葉 5~10
助宮易物 糯米 5~10
布施濟民 紅棗 2~5

功能實作

日常頁面
/**
 * @description 日常任務
 */
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import Back from '@/components/Back';
import Flower from '@/components/Flower';
import FlowerTree from '@/components/FlowerTree';
import { Modal } from 'antd-mobile';
import { QuestionCircleFill, KoubeiFill, FireFill, HeartFill } from 'antd-mobile-icons';
import util from '../../utils/util';
import './index.less';
const Tasks = () => {
  const userInfo = util.getUserInfo() || {};
  const [tasksObj, setTasksObj] = useState(
    userInfo.tasks
      ? userInfo.tasks
      : {
          zheng: 0,
          cai: 0,
          mei: 0,
          creatAt: 0,
        },
  );
  const listInit = [
    {
      key: 'zheng',
      title: '政',
      name: '開源節流',
      num: 0,
      harvestFalg: true,
      taskKey: 'zongye',
      icon: <KoubeiFill fontSize={16} color='#fcb887' />,
    },
    {
      key: 'cai',
      title: '才',
      name: '助宮易物',
      num: 0,
      harvestFalg: true,
      taskKey: 'nuomi',
      icon: <FireFill fontSize={16} color='#f6f6f6' />,
    },
    {
      key: 'mei',
      title: '魅',
      name: '布施濟民',
      num: 0,
      harvestFalg: true,
      taskKey: 'hongzao',
      icon: <HeartFill fontSize={16} color='#59ca94' />,
    },
  ];
  const [list, setList] = useState(listInit);
  // 擷取目前内務展示資料
  const getNewNum = () => {
    // 梯齡換算成月
    const newData = new Date();
    let diffData = tasksObj.creatAt;
    if (!tasksObj.creatAt) {
      // 如果收獲時間預設活動開始時間
      diffData = moment('2022-06-01');
    }
    let hour = moment(newData).diff(moment(diffData), 'hours');
    console.log(hour, 'hour');
    let numCurr = hour * 1000;
    const listInit = [...list];
    listInit.map(item => {
      item.num += numCurr;
    });
    setList(listInit);
  };
  useEffect(() => {
    getNewNum();
  }, []);
  // 擷取随機數
  const getRandomNumber = key => {
    const randomObj = {
      zheng: [5, 10],
      cai: [5, 10],
      mei: [2, 5],
    };
    const randomItem = randomObj[key];
    const m = randomItem[1];
    const n = randomItem[0];
    let randomNum = Math.random() * (m - n) + n;
    randomNum = Math.round(randomNum);
    console.log(randomNum, 'randomNum');
    return randomNum;
  };
  // 收獲
  const handleHarvest = index => {
    const newData = new Date();
    let userInfoInit = { ...userInfo };
    const handleList = [].concat(list);
    let item = handleList[index];
    let tasksObjInit = { ...tasksObj };
    tasksObjInit.creatAt = newData;
    const festivalObjInit = 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);
  };
  // 頂部提示
  const headTip = () => {
    return Modal.show({
      title: '内務',
      content: (
        <div className='tasks-modal'>
          <div className='tasks-modal-title'>内務打理</div>
          <div className='tasks-modal-content mb10'>
            <p className='mb10'>内務分為“開源節流”,“助宮易物”,“布施濟民”三種類型,分别可以獲得銅币、珍品和名望。</p>
            <p>打理内務有一定幾率獲得包粽子的材料。</p>
          </div>
          <div className='tasks-modal-title'>内務獎勵</div>
          <div className='tasks-modal-content'>
            <p className='mb10'>開源節流有一定幾率獲得粽葉。</p>
            <p className='mb10'>助宮易物有一定幾率獲得糯米。</p>
            <p>布施濟民有一定幾率獲得紅棗。</p>
          </div>
        </div>
      ),
      showCloseButton: true,
    });
  };
  // 将資料除以10000進行展示
  const getTaskNumContent = num => {
    num = num / 10000;
    return num;
  };
  return (
    <div className='tasks'>
      <Back />
      <div className='tasks-info'>
        {list.map(item => {
          return (
            <div className='tasks-info-item' key={item.key}>
              <div className='tasks-info-item-icon'>{item.icon}</div>
              <span>
                {getTaskNumContent(tasksObj[item.key])} {tasksObj[item.key] > 0 ? '萬' : ''}
              </span>
            </div>
          );
        })}
      </div>
      <div className='tasks-head'>
        <div className='tasks-head-tip' onClick={headTip}>
          <QuestionCircleFill fontSize={28} color='#f69bad' />
        </div>
        <div className='tasks-head-title'>内務打理</div>
      </div>
      <div className='tasks-list'>
        {list.map((item, index) => {
          return (
            <div className='tasks-item' key={item.key}>
              <div className='tasks-item-top'></div>
              <div className='tasks-item-title'>{item.title}</div>
              <div className='tasks-item-name'>
                <span>{item.name}</span>
                <div className='name-circular name-circular1'></div>
                <div className='name-circular name-circular2'></div>
                <div className='name-circular name-circular3'></div>
                <div className='name-circular name-circular4'></div>
                <div className='name-circular name-circular5'></div>
                <div className='name-circular name-circular6'></div>
              </div>
              <div className='tasks-item-num'>{item.num}</div>
              <div className={classnames('tasks-item-btn', { inactive: !item.harvestFalg })} onClick={() => handleHarvest(index)}>
                <div className='btn-flower1'>
                  <Flower />
                </div>
                <div className='btn-flower2'>
                  <Flower />
                </div>
                <span>{item.harvestFalg ? '收獲' : '恢複'}</span>
              </div>
            </div>
          );
        })}
      </div>
      <div className='tasks-footer'></div>
      <div className='tasks-tree'>
        <FlowerTree />
      </div>
      <div className='tasks-rule'>
        <div className='tasks-rule-title'>
          <span>宮規</span>
        </div>
        <div className='tasks-rule-text'>内務收獲 +5%</div>
      </div>
    </div>
  );
};
export default Tasks;      
傳回元件

每個二級、三級頁面都會放傳回按鈕,是以我封裝成了元件。

/**
 * @description 回退按鈕元件
 */
import React from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import './index.less';
const Back = ({ ...props }) => {
  const history = useHistory();
  const { path } = props;
  // 點選事件
  const handleClick = () => {
    history.push(path);
  };
  return (
    <div className='back' onClick={handleClick}>
      <div className='back-left'></div>
      <div className='back-right'></div>
    </div>
  );
};
Back.propTypes = {
  path: PropTypes.string, // 跳轉路徑
};
Back.defaultProps = {
  path: '/home',
};
export default Back;      
花朵元件

有些頁面需要花朵裝飾,是以我把花朵封裝成了元件。

/**
 * @description 花朵元件
 */
import React from 'react';
import './index.less';
const Flower = () => {
  return (
    <div className='flower'>
      <div className='flower-leaf flower-leaf1'></div>
      <div className='flower-leaf flower-leaf2'></div>
      <div className='flower-leaf flower-leaf3'></div>
      <div className='flower-leaf flower-leaf4'></div>
      <div className='flower-leaf flower-leaf5'></div>
      <div className='flower-circle'></div>
    </div>
  );
};
export default Flower;      
開滿花的樹元件

這個是參考的網站是的,參考位址我放到了文章末尾。

/**
 * @description 開滿花的樹元件
 */
import React from 'react';
import './index.less';
const FlowerTree = () => {
  return (
    <div className='flowertree'>
      <div className='trunk'>
        <div className='roots'>
          <div className='root'></div>
          <div className='root'></div>
          <div className='root'></div>
          <div className='root'></div>
          <div className='root'></div>
        </div>
      </div>
      <div className='leaves cherry-blossoms'>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
        <div className='cherry-blossom'>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
          <div className='petal'></div>
        </div>
      </div>
    </div>
  );
};
export default FlowerTree;      
公共方法

有些基礎的功能、或者出現頻率較高的功能,可以提煉成公共方法。

/**
 * @description 公共方法
 */
// 擷取使用者資訊
const getUserInfo = () => {
  let userInfo = localStorage.getItem('userInfo');
  if (userInfo) {
    return JSON.parse(userInfo);
  }
  return null;
};
// 儲存使用者資訊
const saveUserInfo = userInfo => {
  if (userInfo) {
    localStorage.setItem('userInfo', JSON.stringify(userInfo));
  }
};
/**
 * 兩個是否可以整除
 * @param {number} num1 除數
 * @param {number} num2 被除數
 * @return {boolean} 是否整除的布爾值
 */
const getNumDivisibleFlag = (num1, num2) => {
  let flag = false;
  // 如果除數小于被除數 則表示不可以被整除
  if (num1 > num2 && num1 / num2 > 1) {
    flag = true;
  }
  return flag;
};
export default { getUserInfo, saveUserInfo, getNumDivisibleFlag };      

最終UI

「前端遊戲開發體驗」我用react實作網頁遊戲的全過程(包括規則設計)

端午活動

活動規則

活動時間

1.2022-5-31 至 2022-6-5,提前預熱3天。

2.頁面上設定活動倒計時

  • 活動結束前,展示距離活動結束還剩多長時間,時間格式為DD天 hh:mm:ss;
  • 活動結束後,展示内容為"活動已結束";
兌換規則

食材兌換比例

粽子類型 需要材料
糯米粽子 10 * 糯米 + 2 * 粽葉 + 2 * 紅棗

食材兌換規則

  1. 通過頁面按鈕進行兌換,當食材數量不足時,按鈕不可點選,當食材數量充足時可以進行點選。
  2. 點選兌換按鈕喚起兌換彈窗,可以通過加減号進行兌換數量的修改,當達到最大可兌換值時,加号不可點選。
  3. 确定兌換之後,粽子數量增加,食材數量對應減少。

功能實作

活動頁面
/**
 * @description 端午活動
 */
import React, { useEffect, useState } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import Back from '@/components/Back';
import { Modal, Stepper } from 'antd-mobile';
import { QuestionCircleFill } from 'antd-mobile-icons';
import util from '../../utils/util';
import './index.less';
const Festival = () => {
  const userInfo = util.getUserInfo() || {};
  const [festivalObj, setFestivalObj] = useState(
    userInfo.festival
      ? userInfo.festival
      : {
          nuomi: 20,
          zongye: 10,
          hongzao: 10,
          zongzi: 150,
        },
  );
  const list = [
    {
      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('');
  let timer = null;
  // 擷取目前兌換按鈕是否可以點選
  const getInactiveFlag = festivalObj => {
    let activeInit = false;
    let nuomi = festivalObj.nuomi;
    let zongye = festivalObj.zongye;
    let hongzao = festivalObj.hongzao;
    if (nuomi && zongye && hongzao) {
      let nuomiFlag = util.getNumDivisibleFlag(nuomi, 10);
      let zongyeFlag = util.getNumDivisibleFlag(zongye, 2);
      let hongzaoFlag = util.getNumDivisibleFlag(hongzao, 2);
      if (nuomiFlag && zongyeFlag && hongzaoFlag) {
        activeInit = true;
      }
    }
    setActiveFlag(activeInit);
  };
  const getCountdown = () => {
    let nowDate = new Date();
    // console.log(nowDate, 'nowDate');
    // 擷取的2022-06-05的23:59:59的時間戳
    let endTime = moment('2022-06-05').endOf('day').format('x');
    let countdownInit = '';
    // 剩餘時間 毫秒
    let surplusTime = endTime - nowDate.getTime();
    if (surplusTime <= 0) {
      clearTimeout(timer);
      countdownInit = '活動已結束';
      setCountdown(countdownInit);
    } else {
      // 剩餘時間 秒
      let runTime = surplusTime / 1000;
      const day = Math.floor(runTime / 86400);
      runTime = runTime % 86400;
      const hour = Math.floor(runTime / 3600);
      runTime = runTime % 3600;
      const minute = Math.floor(runTime / 60);
      runTime = runTime % 60;
      const second = Math.floor(runTime);
      const dayText = day ? `${day}天` : '';
      countdownInit = `剩餘時間:${dayText} ${hour}:${minute}:${second}`;
      setCountdown(countdownInit);
      timer = setTimeout(getCountdown, 1000);
    }
  };
  useEffect(() => {
    getInactiveFlag(festivalObj);
    getCountdown();
  }, []);
  useEffect(() => {
    // 清除定時
    return () => {
      clearInterval(timer);
    };
  }, []);
  // 頂部提示
  const headTip = () => {
    return Modal.show({
      title: '"粽"得鳳儀',
      content: (
        <div className='festival-modal'>
          <div className='festival-modal-title'>合成粽子</div>
          <div className='festival-modal-content mb10'>
            <p className='mb10'>10*糯米+2*粽葉+2*紅棗可以兌換1個糯米粽子。</p>
            <p>當糯米、粽葉、紅棗的比例不是5:1:1時,無法進行兌換。</p>
          </div>
          <div className='festival-modal-title'>稱号獎勵</div>
          <div className='festival-modal-content'>
            <p className='mb10'>目前粽子數量達到50個可獲得稱号“淑儀傾城”。</p>
            <p className='mb10'>目前粽子數量達到100個可獲得稱号“花容初綻”。</p>
            <p className='mb10'>目前粽子數量達到200個可獲得稱号“花成蜜就”。</p>
            <p className='mb10'>目前粽子數量達到300個可獲得稱号“寵冠六宮”。</p>
            <p className='mb10'>目前粽子數量達到400個可獲得稱号“鳳儀千載”。</p>
            <p>稱号自動擷取無需額外操作</p>
          </div>
        </div>
      ),
      showCloseButton: true,
    });
  };
  // 粽子展示
  const zongziContent = () => {
    return (
      <div className='festival-zongzi'>
        <div className='festival-zongzi-left'></div>
        <div className='festival-zongzi-center'></div>
        <div className='festival-zongzi-right'></div>
      </div>
    );
  };
  // 兌換确定操作
  const convertOnConfirm = () => {
    setVisible(false);
    let festivalObjInit = { ...festivalObj };
    console.log(convertNum, 'convertNum');
    festivalObjInit.nuomi -= convertNum * 10;
    festivalObjInit.zongye -= convertNum * 2;
    festivalObjInit.hongzao -= convertNum * 2;
    festivalObjInit.zongzi += convertNum;
    console.log(festivalObjInit, 'festivalObjInit');
    // 設定緩存
    let userInfoInit = { ...userInfo };
    userInfoInit.festival = festivalObjInit;
    util.saveUserInfo(userInfoInit);
    setFestivalObj(festivalObjInit);
    getInactiveFlag(festivalObjInit);
  };
  // 擷取可以兌換的數量
  const getConvertCount = () => {
    let nuomi = festivalObj.nuomi;
    let zongye = festivalObj.zongye;
    let hongzao = festivalObj.hongzao;
    let nuomiNum = Math.floor((nuomi * 100) / (10 * 100));
    let zongyeNum = Math.floor((zongye * 100) / (2 * 100));
    let hongzaoNum = Math.floor((hongzao * 100) / (2 * 100));
    return Math.min(nuomiNum, zongyeNum, hongzaoNum);
  };
  // 兌換操作
  const handleConvert = () => {
    if (!activeFlag) return;
    const count = getConvertCount();
    setConvertNum(1);
    setCount(count);
    setVisible(true);
  };
  return (
    <div className='festival'>
      <div className='festival-content'>
        <Back />
        <div className='festival-head'>
          <div className='festival-head-tip' onClick={headTip}>
            <QuestionCircleFill fontSize={28} color='#f69bad' />
          </div>
          <div className='festival-head-title'>"粽"得鳳儀</div>
        </div>
        <div className='festival-time'>{countdown}</div>
        <div className='festival-convert'>
          <div className='festival-convert-zongzi'>{zongziContent()}</div>
          <div className='festival-convert-zongzi2'>{zongziContent()}</div>
          <div className='festival-convert-num'>
            {list.map(item => {
              return (
                <div className='festival-convert-num-item' key={item.key}>
                  {item.name}: {festivalObj[item.key]}
                </div>
              );
            })}
          </div>
          <div className='festival-convert-rule'>
            <div className='festival-convert-rule-nuomi'></div> x 10<div className='festival-convert-rule-add'></div>
            <div className='festival-convert-rule-zongye'></div> x 2<div className='festival-convert-rule-add'></div>
            <div className='festival-convert-rule-hongzao'></div> x 2
          </div>
          <div className={classnames('festival-convert-btn', { inactive: !activeFlag })} onClick={handleConvert}>
            兌換
          </div>
        </div>
      </div>
      <div className='festival-room'>
        <div className='festival-room-wall'>
          <div className='wall-poetry'>
            <div className='wall-poetry-nail'></div>
            <div className='wall-poetry-shaft wall-poetry-shaft-top'></div>
            <div className='wall-poetry-inner'>
              <div className='wall-poetry-title'>浣溪沙·端午</div>
              <div className='wall-poetry-author'>宋·蘇轼</div>
              <div className='wall-poetry-content'>輕汗微微透碧纨,明朝端午浴芳蘭。流香漲膩滿晴川。彩線輕纏紅玉臂,小符斜挂綠雲鬟。佳人相見一千年。</div>
            </div>
            <div className='wall-poetry-shaft wall-poetry-shaft-bottom'></div>
          </div>
        </div>
        <div className='festival-room-floor'>
          <div className='floor-line floor-line1'></div>
          <div className='floor-line floor-line2'></div>
          <div className='floor-line floor-line3'></div>
          <div className='floor-line floor-line4'></div>
          <div className='floor-line floor-line5'></div>
          <div className='floor-line floor-line6'></div>
          <div className='floor-line floor-line7'></div>
          <div className='floor-line floor-line8'></div>
          <div className='floor-line floor-line9'></div>
          <div className='floor-line floor-line10'></div>
        </div>
      </div>
      <Modal
        visible={visible}
        content={
          <div className='festival-modal'>
            <div className='festival-modal-text'>最多可以兌換: {count}</div>
            <div className='festival-modal-stepper'>
              <Stepper
                step={1}
                value={convertNum}
                min={1}
                max={count}
                onChange={value => {
                  setConvertNum(value);
                }}
              />
            </div>
            <div className='festival-modal-confirm' onClick={() => convertOnConfirm()}>
              兌換
            </div>
          </div>
        }
        showCloseButton={true}
        closeOnAction
        onClose={() => {
          setVisible(false);
        }}
      />
    </div>
  );
};
export default Festival;      

最終UI

活動展示
「前端遊戲開發體驗」我用react實作網頁遊戲的全過程(包括規則設計)
兌換彈窗展示

總結

  1. CSS用的别以前熟練了很多,這次的遊戲裡除了頭像圖檔、一顆樹、一簇花,其他的都是我用CSS寫出來的,沒有用圖檔素材,實作過程不斷收獲新的創意。說起來多虧這段時間碼上掘金活動,我才能使用CSS實作功能做的如此之快,ღ( ´・ᴗ・` );
  2. 遊戲設計,體驗了一把産品/策劃的感覺,站在不同的角度去思考需要實作的功能,鍛煉邏輯思維,很有收獲;
  3. 核心功能的實作,包括内務收集的計算、食材的随機掉落計算、粽子兌換的計算等多個計算功能,雖然方法可能不是最優,但是在遇到類似的功能實作算是有經驗了;

參考文章

  • ​​花叢和開滿花的樹參考​​