网页游戏 | 我用react实现网页游戏的全过程【万字总结】

举报
叶一一 发表于 2023/02/13 18:41:31 2023/02/13
【摘要】 游戏开发体验挺新奇的,我用react实现网页游戏的全过程(包括规则设计)。

关于游戏的灵感来源

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

假如我是游戏策划

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

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

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

产品视角

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

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

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

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

(^U^)ノ~YO,一切准备就绪,开始干活。

交互设计

大致画了一下设计草图,帮助理清楚布局思路。(第一次画,还有待提高。)

首页

image

日常任务

image

端午活动

image

功能设计

首页

内容

主要包括用户信息、任务入口、活动入口等展示。

称号规则

称号和糯米粽子数量对应如下:

称号

糯米粽子数量

殿上佳人

<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-bg'>
        <div className='door-opening'>
          <div className='door-opening-center'>{entranceContent()}</div>
          <div className='door-opening-flowers'>
            <FlowerCluster />
          </div>
        </div>
      </div>
    </div>
  );
};
export default Home;

头像组件

头像组件主要包括头像图片、称号、花朵点缀三个部分。

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

最终UI

设计为古代的室内,参考的《美人传》小游戏中的UI设计,包括木质的墙壁、门和地板。除此之外还加了一些动画效果增加趣味性:

  • 称号上面加了一个花朵做装饰;
  • 任务和活动入口上加了光效闪动的效果;
  • 地板上的猫咪耳朵和肚子随着呼吸而动;

image

日常任务

日常任务收集规则

  • 每天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>
              <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 公共方法
 */
// 获取用户信息
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

image

端午活动

活动规则

活动时间

1.2022-5-31 至 2022-6-5,提前预热3天。

2.页面上设置活动倒计时

  • 活动结束前,展示距离活动结束还剩多长时间,时间格式为DD天 hh:mm:ss;
  • 活动结束后,展示内容为"活动已结束";

兑换规则

食材兑换比例

粽子类型

需要材料

糯米粽子

10 * 糯米 + 2 * 粽叶 + 2 * 红枣

食材兑换规则

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

功能实现

活动名称

 <div className='festival-head'>
          <div className='festival-head-tip' onClick={headTip}>
            <QuestionCircleFill fontSize={28} color='#f69bad' />
          </div>
          <div className='festival-head-title'>"粽"得凤仪</div>
</div>

活动倒计时

  • 进行中,展示具体剩余时间,展示效果为天 HH:MM:SS;
  • 到达截止时间,展示活动已结束。
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);
    }
  };

活动规则

点击活动标题左侧的问号icon,会展示详细的规则。

// 顶部提示
  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 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);
  };

最终UI

活动展示

image

兑换弹窗展示

image

总结

本次收获还是挺多的。

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

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

作者:非职业「传道授业解惑」的开发者叶一一
简介:「趣学前端」、「CSS畅想」系列作者,华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。