介绍
本期我们将用css3+vue做一个养鱼小游戏,这里我们不用任何素材,都是由css绘制而成(咳咳,虽然是纸片鱼),当我们点击屏幕可以投放食物,鱼儿每当看到食物就会争抢,鱼儿吃到食物就会变大一点,因为是休闲养成类小游戏,所以就没有什么通不通关一说了,就看谁家的鱼儿养的最大吧。
废话不多说,我们先来康康展示效果怎样吧:
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做一个渐变,做一个分层即可,一开始比较清澈,然后不断加深,到了底部加一部分淡黄作为底沙。
微信截图_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)来进行鱼的翻转。
微信截图_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的动画,后面在写逻辑的时候,会让鱼在不同周期内突出泡泡来不断往上漂浮。
微信截图_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),从而改变鱼当前坐标。
微信截图_20220404213659.png
结语
css养鱼小游戏到这里已经完成了,感觉如何呢?或许,你发现了如果鱼多了导致重绘结点很多会比较的占用内存,当然你可以尝试用canvas来绘制也是不错的选择,js核心逻辑大致也是一样的。
PS:本作是个人早期自娱自乐的练手小作品,在codepen可以查看源码,新人尝试用此练手感觉对你的不管是css还是js提升还是有些作用的。本身养鱼也是一件修身养性的事,不急不躁的练习才可以打实根基,一起加油鸭~
关于本文