自建rustdesk docker_svg

介绍

本期我们将用css3+vue做一个养鱼小游戏,这里我们不用任何素材,都是由css绘制而成(咳咳,虽然是纸片鱼),当我们点击屏幕可以投放食物,鱼儿每当看到食物就会争抢,鱼儿吃到食物就会变大一点,因为是休闲养成类小游戏,所以就没有什么通不通关一说了,就看谁家的鱼儿养的最大吧。

废话不多说,我们先来康康展示效果怎样吧:

自建rustdesk docker_算法_02


VID_20220404_212850.gif

演示地址:https://codepen.io/jsmask/full/xxVaOMy

开始

绘制鱼池

<div class="main" ref ="pool"></div> 
复制代码
.main{
    width: 100%;
    height: 100vh;
    background: linear-gradient(180deg, #86defc 0%, #71cceb 20%, #73b2f1, #349ef8 83%, #cce293 93%, #e6cd6a 100%);
    overflow: hidden;
    position: relative;
}
复制代码

绘制背景用linear-gradient做一个渐变,做一个分层即可,一开始比较清澈,然后不断加深,到了底部加一部分淡黄作为底沙。



自建rustdesk docker_svg_03

微信截图_20220404213300.png

绘制小鱼

<div class="fish" ref="fish" v-for="(item,index) in fishList" :key="index" 
     :class="{'left':item.direction==-1,[item.type]:true}" 
     :style="{'transform':'translate('+item.x+'px,'+item.y+'px)'}">
    <div class="fish-main" :style="{'transform':'scale('+(1+item.level*0.025)+')'}">
        <div class="fish-body">
            <div class="fish-fins"></div>
        </div>
    </div>
</div>
复制代码
.fish{
   width: 60px;
   height: 30px;
   position:absolute;
   z-index:99;
}

.fish.left .fish-body{
    transform: scaleX(-1);
}
.fish.fish-type1 .fish-body{
    --main-skin:rgb(230,136,72);
}
.fish.fish-type2 .fish-body{
    --main-skin:rgb(230, 90, 72);
}
.fish.fish-type3 .fish-body{
    --main-skin:rgb(72, 127, 230);
}
.fish.fish-type4 .fish-body{
    --main-skin:rgb(241, 207, 94);
}
.fish.fish-type5 .fish-body{
    --main-skin:rgb(82, 151, 100);
}
.fish.fish-type6 .fish-body{
    --main-skin: rgb(255, 117, 117);
}
.fish-main{
    transition: .3s all;
}
.fish-body{
    position: relative;
    margin-left: 6px;
    width: 50px;
    height: 30px;
    border-radius: 50% 50%;
    border-bottom:1px solid rgba(0, 0, 0, .12);
    border-top:1px solid rgba(0, 0, 0, .06);
    background-color: var(--main-skin);
    transition: 1s all;
    transform-origin: 50% 50%;
}
.fish-body::before {
    content: '';
    display: block;
    position: absolute;
    left: -11px;
    width: 0;
    height: 0;
    border-left: solid 25px var(--main-skin);
    border-top: solid 15px transparent;
    border-bottom: solid 15px transparent;
    animation: move2 .24s linear infinite;
}
.fish-body::after {
    content: '';
    display: block;
    position: absolute;
    top: 8px;
    left: 34px;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background-color: black;
    box-shadow: 0px 0px 0 2px white;
}
.fish-fins{
    width: 0;
    height: 0;
    border-left: solid 6px var(--main-skin);
    border-top: solid 3px transparent;
    border-bottom: solid 3px transparent;
    position: absolute;
    top: 17px;
    left: 20px;
    filter: brightness(5.5);
    opacity: .1;
    animation: move .24s linear infinite;
    transform-origin: 100% 100%;
}
@keyframes move{
    0%{
        opacity: .1;
        transform: scaleX(1);
    }

    50%{
        opacity: .15;
        transform: scaleX(1.3);
    }

    100%{
        opacity: .1;
        transform: scaleX(1) ;
    }
}
@keyframes move2{
    0%{
        opacity: .9;
        transform: scaleX(1);
    }

    50%{
        opacity: 1;
        transform: scaleX(1.3);
    }

    100%{
        opacity: .9;
        transform: scaleX(1) ;
    }
}
复制代码

我们所绘制的小鱼是由鱼身+鱼眼+胸鳍+鱼尾四部分构成的,鱼身是矩形,鱼眼为圆形绘制非常的简单,而胸鳍与鱼尾则为三角形,我们通过border来做绘制,其中胸鳍部分我们通过filter:brightness方法增加了一部分亮度使其更加逼真。而且胸鳍和鱼尾还可以进行摆动,这里用animation做一个放大缩小的动画,再通过transform-origin控制一下基点就可以达到摆动的效果。另外,考虑到鱼要有左右方向所以我们用scaleX(-1)来进行鱼的翻转。



自建rustdesk docker_js_04

微信截图_20220404213434.png

气泡&食物

<div class="bubble" :key="item.index" v-for="item in bubbleList" :style="{'left':item.x +'px','top':item.y+'px'}">
 <div class="bubble-body"></div>
</div>
<div class="food" v-for="item in foodList" :key="item.index" :style="{'left':item.x +'px','top':item.y+'px'}">
 <div class="food-body"></div>
</div>
复制代码
.bubble{
    width: 5px;
    height: 5px;
    position: absolute;
    animation: up 5s linear;
    animation-fill-mode: forwards;
}
.bubble-body{
    width: 5px;
    height: 5px;
    border:1px solid rgb(255,255,255);
    border-radius: 50%;
    position:absolute;
    left: 60px;
    top: 10px;
    opacity: 1;
    animation: sway 3s linear infinite;
}
.food{
   width: 10px;
   height: 7px;
   position: absolute;
   opacity: 1;
}
.food-body{
   position: absolute;
   width: 10px;
   height: 7px;
   border-radius: 45% 42%;
   background: rgb(82, 57, 43);
   animation: sway 3s linear infinite;
}
@keyframes up{
    0%{
        opacity: 1;
        transform: translateY(0);
    }
    100%{
        opacity: 0;
        transform: translateY(-600px);
    }
}
@keyframes sway{
    0%,20%,40%,60%,80%,100%{
        transform: translateX(0px)  rotate(0);
    }
    10%,30%,50%,70%,90%{
        transform: translateX(-10px)  rotate(30deg);
    }
}
复制代码

气泡和食物的绘制非常的简单,一个是空心圆,一个圆角矩形,唯一要说明的是我给他们加了一个左右摇摆的动画sway,这样会让在水下世界更加真实。另外,还有一个up的动画,后面在写逻辑的时候,会让鱼在不同周期内突出泡泡来不断往上漂浮。



自建rustdesk docker_svg_05

微信截图_20220404213522.png

养鱼逻辑

new Vue({
    el:".main",
    data:{
      fishNum:10,    // 生成鱼的数量
      fishList:[],   // 小鱼数组
      bubbleList:[], // 气泡数组
      foodList:[]    // 食物数组
    },
    mounted() {
      this.init();
    },
    methods: {
      init(){
        // 初始化事件
        this.width = window.innerWidth;
        this.height = window.innerHeight;
        for (let i = 0; i < this.fishNum; i++) {;
           let fish = this.addFish(i);
           this.fishList.push(fish)
        } 
        this.move();
        this.foodMove();
        this.throw();
        window.onresize = () =>{
          this.width = window.innerWidth;
          this.height = window.innerHeight;
         }
      },
      addFish(i){
        // 随机生成鱼的参数               
        return {
          index:`fish_${i}`,
          x: this.random(0,this.width-60),
          y: this.random(15,this.height-30),
          direction:(this.random(0,1)>0.5)?1:-1,
          type: 'fish-type'+~~(this.random(1,6)),
          speed:this.random(1,3),
          bTime:this.random(1,3)*100,
          bMax:this.random(3,10)*100,
          sy:Math.random(0,10),
          level:~~(this.random(0,2))
        }
      },
      move() {
          // 鱼群移动
      },
      addBubble(fish){
          // 追加气泡
            const {index,x,y} = fish;
            for (let i = 0; i < this.bubbleList.length; i++) {
              if(this.bubbleList[i].index == index){
                this.bubbleList.splice(i,1);
              }
            }
            this.bubbleList.push({x, y, index });
      },  
      throw(){
          // 投喂
            this.$refs.pool.addEventListener("click",e=>{
              let food = {
                x:e.layerX,
                y:e.layerY
              }
              let index = this.foodList.push(food);                  
            })
      },
      foodMove(){
          // 投喂食物不断下沉
             window.requestAnimationFrame(()=>{
              this.foodList.forEach((food,index)=>{
                food.y++;
                if(food.y>this.height){
                  this.foodList.splice(index,1);
                }
              })
              this.foodMove();
            });
      },
      random(min,max){
        return min + Math.random()*max
      }
})
复制代码

起初我们在先生成不同鱼的参数,如位置,方向,速度,大小等,然后再添加到 fishList 数组中进行控制。这里注意,食物,小鱼,气泡这些坐标的更改是在vue的style已经绑定translate而改变的。

addBubble方法:后面我们在写不断绘制鱼游动逻辑之时使用(即move方法),这里把坐标和吐出气泡的鱼对象,先加入bubbleList数组中,因为刚才用css写了up动画,气泡动画一段时间后就会消失,当鱼下一次吐出气泡的时候,在从bubbleList数组中移除,重新添加进去,则又可以播放。

throw方法:则是注册一个点击事件,每次点击发一个食物的坐标给foodList数组。

foodMove方法:则是不断绘制出食物下降的动画,并且判断如果沉底则让其消失。

function move() {
      window.requestAnimationFrame(() => {    
        this.fishList.forEach( fish => {
          
          // 到达临界值时吐气泡
          if (++fish.bTime > fish.bMax) {
            fish.bTime = 0;
            this.addBubble(fish);
          }
            
          // 找到最近的食物
          if (this.foodList.length > 0) {
            let foodIndex = 0;
            if (this.foodList.length > 1) {
              for (let i = 0, sub = null; i < this.foodList.length; i++) {
                let num = Math.abs(this.foodList[i].x - fish.x);
                if (sub == null) sub = num;
                if (num < sub) {
                  sub = num;
                  foodIndex = i;
                }
              }
            }
            
            // 根据最近的食物找到改变与的方向
            let food = this.foodList[foodIndex];
            let dx = food.x - fish.x;
            let dy = food.y - fish.y;
            if (dx >= 0) {
              fish.direction = 1;
            } else {
              fish.direction = -1;
            }

            // 计算方向改变鱼的移动,如果触碰到食物则升级
            let angle = Math.atan2(dy, dx);
            if (dx < 10 && dx > -10 && dy < 10 && dy > -10) {
              fish.level++; // 升级长胖
              this.foodList.splice(foodIndex, 1);
              fish.direction = Math.random() > 0.5 ? 1 : -1;
            } else {
              let vx = fish.speed * 1.2 * Math.cos(angle);
              let vy = fish.speed * 1.2 * Math.sin(angle);
              fish.x += vx;
              fish.y += vy;
            }
          } else {
            fish.x += fish.speed * fish.direction;
            fish.sy += 0.01;
            fish.y += Math.cos(fish.sy) * 2;
          }
            
    // 边界判断
          if (fish.x < -60) {
            fish.x = -60;
            fish.direction *= -1;
            fish.speed = this.random(1, 3);
          }
          if (fish.x > this.width + 30) {
            fish.x = this.width + 30;
            fish.direction *= -1;
            fish.speed = this.random(1, 3);
          }
          if (fish.y < 0) {
            fish.y = 0;
          }
          if (fish.y > this.height - 30) {
            fish.y = this.height - 30;
          }
        });

        this.move();
   });
}
复制代码

鱼的游动逻辑还是有点多的,每段主要做了什么都写到了注释上,鱼真正都游动逻辑其实就是算出目标点与当前鱼坐标的差值(即dx与dy),然后通过 let angle = Math.atan2(dy, dx) 计算出角度,从而获得不同方向上的加速度(即vx与vy),从而改变鱼当前坐标。



自建rustdesk docker_算法_06

微信截图_20220404213659.png

结语

css养鱼小游戏到这里已经完成了,感觉如何呢?或许,你发现了如果鱼多了导致重绘结点很多会比较的占用内存,当然你可以尝试用canvas来绘制也是不错的选择,js核心逻辑大致也是一样的。

PS:本作是个人早期自娱自乐的练手小作品,在codepen可以查看源码,新人尝试用此练手感觉对你的不管是css还是js提升还是有些作用的。本身养鱼也是一件修身养性的事,不急不躁的练习才可以打实根基,一起加油鸭~

关于本文

作者:jsmask