第一篇:一个防御塔+多个野怪(简易版)
第二篇:防御塔随意放置
第三篇:防御塔随意放置+多组野怪

  1、动态addView防御塔

  2、防御塔放置后不可以移动

  3、弯曲道路

  4、素材替换

第四篇:多波野怪

第五篇:杀死野怪获得金币

第六篇:防御塔可升级,增强攻击力,增大射程

描述:防御塔可以放置多个,每一个都是独立的,他们的攻击互不影响(防御塔随意拖动在第二篇),这里用到的知识是,自定义view的拖动,防御塔是否可以攻击的计算,防御塔的攻击路径。

1、放置防御塔

  • 新建类ActivityTower5,主要控制放置塔的回调
  • 新建BattlefieldView5,主要渲染战场
  • 新建TowerView5,主要绘制防御塔,(其实野怪也需要单独创建view)

1.1ActivityTower5首页该做些什么?

这次我们想要做成动态的,由用户自行开启,玩累了还能暂停,而且有钱可以创建多个防御塔(后续加入攻击野怪获得金币),所以创建开启按钮,暂停按钮,创建A炮(后续有B炮,C炮...),代码如下

<?xml version="1.0" encoding="utf-8"?>
<layout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_relative"
......
android:gravity="center"
......> <com.liu.lib_view.tower.tower4.BattlefieldView4
android:id="@+id/TowerView"
......
/>
<LinearLayout
android:id="@+id/bottom"
......>
<Button
android:id="@+id/start"
......
android:text="开始"/>
<Button
android:id="@+id/pause"
......
android:text="暂停"/>
<Button
android:id="@+id/create"
......
android:text="创建A炮"/>
</LinearLayout>
</RelativeLayout>
</layout>

这次添加一些素材,这些都是在网上随便找的,一个背景图片,一个防御塔,一个野怪,这次做成横屏的,我们需要记录一下弯曲道路的xy坐标,封装成一个list(下面有解)。

1.2、BattlefieldView5渲染战场

集成ViewGroup,因为我们要在里面添加其他View,只有ViewGroup才有addView方法,这里我们声明一些属性,妖怪大道、野怪、防御塔画笔这些必不可少,我们这次是多个防御塔就要创建towerList来存储我们创建的防御塔,野怪数量也是如此。

注意:集成ViewGroup这里要写setWillNotDraw方法,不然onDraw()不执行。

我们设置完背景图片后,开始渲染战场,首先绘制道路,这次是弯曲的,会用到Path类,

  •   moveTo(x,y)  移动的起始点
  •   lineTo(x,y)  从起始点到该点画一条线。

我们按照背景图路线琢磨一下路线坐标(每个手机可能存在差异),大概是如下

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
towerX = w / 2;
towerY = h / 2; roadPath = new Path();
roadPath.moveTo(0,900);
roadPath.lineTo(500,900);
roadPath.lineTo(500,200);
roadPath.lineTo(800,200);
roadPath.lineTo(800,800);
roadPath.lineTo(1600,800);
roadPath.lineTo(1600,200);
roadPath.lineTo(towerX*2,200);
}

大体路线已经出来了,我们需要获取这条线上的坐标点,pathMeasure类可以获取path路径上的点数

源码解析

pathMeasure.getLength()获取点数
pathMeasure.getPosTan(距离,pos,tan);
源码解释:
* @param distance The distance along the current contour to sample 沿轮廓到样本的距离
* @param pos If not null, returns the sampled position (x==[0], y==[1])
* @param tan If not null, returns the sampled tangent (x==[0], y==[1])
* @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float pos[], float tan[]) {
if (pos != null && pos.length < 2 ||
tan != null && tan.length < 2) {
throw new ArrayIndexOutOfBoundsException();
}
return native_getPosTan(native_instance, distance, pos, tan);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
......
pathMeasure = new PathMeasure();
pathMeasure.setPath(roadPath,false);
}

到这里,我们就可以拿到路径了,我们还需要把妖怪大道path集合存入到每个野怪属性里,让野怪沿着这条路线走,

public class BlameBean {
private int blameId;
public int x;
public int y;
public int speed;//行走速度
public int HP;//血量
public boolean isAttacks;//是否可以被攻击
public boolean wounded;//受伤效果 public int position=0;
public List<RoadXY> roadXYList = new ArrayList<>();

position是走到第几步,roadXYList就是路线,动态添加6个野怪,路线别忘记添加了,3000可以理解为整条路线野怪需要走3000步才能到终点。

1.3、动态添加野怪

/**
* 添加一个野怪
*/
private void addBlame() {
if (countDownTimer != null) {
return;
}
countDownTimer = new CountDownTimer(12000, 2000) { @Override
public void onTick(long millisUntilFinished) {
if (blameList.size() >= 6) {
return;
}
BlameBean blameBean = new BlameBean();
blameBean.setHP(100);
blameBean.setSpeed(1);
blameBean.setX(towerX + 200);
blameBean.setY(0); List<BlameBean.RoadXY> roadXYList = blameBean.getRoadXYList();
for (int i = 0; i < 3000; i++) {
pathMeasure.getPosTan(i/3000f * pathMeasure.getLength(),pos,tan);
BlameBean.RoadXY roadXY = new BlameBean.RoadXY();
roadXY.setRoadX((int) pos[0]);
roadXY.setRoadY((int) pos[1]);
roadXYList.add(roadXY);
}
blameBean.setRoadXYList(roadXYList); blameList.add(blameBean);
} @Override
public void onFinish() { }
}; }

1.4、添加防御塔 ,动态创建A炮

Activity中
mBinding.create.setOnClickListener(v -> {
TowerView5 towerView = new TowerView5(activity);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
lp.width = 700;
lp.height = 700;
towerView.setLayoutParams(lp);
//一旦放置成功后调用
towerView.setTowerListener(() -> {
int raduis = 350;
towerView.setMove(false);
mBinding.TowerView.addTower((int)towerView.getX()+raduis,(int)towerView.getY()+raduis,raduis);
});
mBinding.layoutRelative.addView(towerView);
listTower.add(towerView);
}); View中
/**
* 添加一个防御塔
*/
public void addTower(int x, int y, int raduis) {
TowerBean towerBean = new TowerBean();
towerBean.setTowerId(towerList.size());
towerBean.setAttacksSpeed(500);
towerBean.setHarm(5);
towerBean.setX(x);
towerBean.setY(y);
towerBean.setRaduis(raduis);
towerList.add(towerBean);
}

我们添加完成需要在ondraw方法中绘制出来

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//野怪路线
canvas.drawPath(roadPath,roadPaint);
//皇帝
for (int i = 0; i < blameList.size(); i++) {
if (blameList.get(i).getX() > (towerX * 2 - 100) && blameList.get(i).getHP() > 0) {
kingHP -= blameList.get(i).getHP();
}
}
if (kingHP <= 0) {
kingHP = 0;
canvas.drawText("失败", towerX, towerY, tp);
valueAnimator.cancel();
}
canvas.drawText("皇帝" + kingHP, towerX * 2 - 100, 200, tp); //野怪移动
for (int i = 0; i < blameList.size(); i++) {
BlameBean blameBean = blameList.get(i);
if (blameBean.getHP() > 0) {
canvas.drawRect(blameBean.getX() - 40, blameBean.getY()-15, blameBean.getX() + 60, blameBean.getY()-5, towerPaint);
canvas.drawRect(blameBean.getX() - 39, blameBean.getY() - 10, blameBean.getX() + 58 - (100 - blameBean.getHP()), blameBean.getY() - 10, hpPaint);
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.yeguai2);
canvas.drawBitmap(bitmap, blameBean.getX() - 20, blameBean.getY() , tp);
}
}
}

写到这里还没有写刷新view的代码,带着疑问,如何刷新数据,如何更新野怪行走的数据,如何判断是否在开炮射程内。

public BattlefieldView5(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//ViewGroup不跳过onDraw()方法,默认是跳过
setWillNotDraw(false);
......
valueAnimator = ValueAnimator.ofInt(0, 10);
valueAnimator.setDuration(5000);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(-1);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
updateParticle();
invalidate();
}
}); }

解释:我们开启一个动画,让其不断的重绘。 updateParticle方法最关键,记录了野怪移动数据,开炮动画等

private void updateParticle() {
//野怪移动
for (int i = 0; i < blameList.size(); i++) {
BlameBean blameBean = blameList.get(i);
if(blameBean.getPosition()>=3000){
break;
}
int roadX = blameBean.getRoadXYList().get(blameBean.position).getRoadX();
int roadY = blameBean.getRoadXYList().get(blameBean.position).getRoadY();
blameBean.setPosition(blameBean.getPosition()+1);
blameBean.setX(roadX);
blameBean.setY(roadY);
//野怪进入防御塔范围
isAttacks(i);
//开炮动画
if (blameList.get(i) != null && blameList.get(i).getMapAttacksTower().size() > 0) {
Map<Integer, Integer> listAttacksTower = blameList.get(i).getMapAttacksTower();
for (Integer j : listAttacksTower.keySet()) {
shotMove(towerList.get(listAttacksTower.get(j)).getX(), towerList.get(listAttacksTower.get(j)).getY(), blameBean.getX(), blameBean.getY(), i, listAttacksTower.get(j));
}
}
}
}

是否进入防御塔范围,这里我们使用map来存int raduis = (int) Math.hypot(Math.abs(x), Math.abs(y));常用于勾股定理,如果在防御塔范围内,野怪就记录一下id,如果在两个防御塔内,就把两个防御塔的id记录一下,map的特性,不会有key重复。也就是不会有防御塔重复攻击。

public class BlameBean {
/**
*使用map的好处是唯一
* 被哪些防御塔攻击
* */
public Map<Integer,Integer> mapAttacksTower=new HashMap<>();
private void isAttacks(int position) {
for (int j = 0; j < towerList.size(); j++) {
int x = blameList.get(position).getX() - towerList.get(j).getX();
int y = blameList.get(position).getY() - towerList.get(j).getY();
int raduis = (int) Math.hypot(Math.abs(x), Math.abs(y));
Map<Integer, Integer> mapAttacksTower = blameList.get(position).getMapAttacksTower();
if (raduis < towerList.get(j).getRaduis() && blameList.get(position).getHP() > 0) {
mapAttacksTower.put(towerList.get(j).getTowerId(), towerList.get(j).getTowerId());
blameList.get(position).setMapAttacksTower(mapAttacksTower);
} else {
//移除防御塔
//攻击到该野怪 塔的集合
for (Integer key : mapAttacksTower.keySet()) {
if (mapAttacksTower.get(key) == towerList.get(j).getTowerId()) {
mapAttacksTower.remove(key);
blameList.get(position).setMapAttacksTower(mapAttacksTower);
break;
}
}
}
}
}

开炮动画,遍历野怪可被攻击的集合即可

//开炮动画
if (blameList.get(i) != null && blameList.get(i).getMapAttacksTower().size() > 0) {
Map<Integer, Integer> listAttacksTower = blameList.get(i).getMapAttacksTower();
for (Integer j : listAttacksTower.keySet()) {
shotMove(towerList.get(listAttacksTower.get(j)).getX(), towerList.get(listAttacksTower.get(j)).getY(), blameBean.getX(), blameBean.getY(), i, listAttacksTower.get(j));
}
}

1.5、炮弹动画

判断如果可以攻击了,就开启一个从xy(防御塔),移动到x2y2 (野怪)的动画 ,动画结束后掉血。动画开始时不可能再次开启,要符合防御塔一次只能攻击一个野怪的效果,这里开炮动画有点问题,就是视觉上老是打偏,有的时候炮弹慢的话,就会打在野怪身后,也没有好的解决方式。博友有想法请留言。

//炮弹动画
private void shotMove(float x, float y, float x2, float y2, int blamePosition, int towerPosition) {
if (!towerList.get(towerPosition).isAttacking()) {
towerList.get(towerPosition).setAttacking(true);
shotView = new ImageView(this.getContext());
shotView.setImageDrawable(getContext().getDrawable(R.drawable.shot));
shotView.layout(0, 0, 20, 20);
addView(shotView);
//开炮音效回调
iShotService.shot();
translateAnimation = new TranslateAnimation(x, x2, y, y2);
translateAnimation.setDuration(towerList.get(0).getAttacksSpeed());
translateAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) { } @Override
public void onAnimationEnd(Animation animation) {
blameList.get(blamePosition).setHP(blameList.get(blamePosition).getHP() - towerList.get(towerPosition).getHarm());
towerList.get(towerPosition).setAttacking(false);
int childCount = getChildCount();
if (childCount > 1) {
removeView(getChildAt(childCount - 1));
}
} @Override
public void onAnimationRepeat(Animation animation) { }
});
shotView.startAnimation(translateAnimation);
}
}

我们需要控制开始与暂停和onResume下动画的释放等

    public void start() {
if (valueAnimator != null) {
//开启动画
addBlame();
// addTower(450,450);
valueAnimator.start();
countDownTimer.start();
}
} public void pause() {
if (valueAnimator != null) {
//开启动画
valueAnimator.pause();
countDownTimer.cancel();
}
} /**
* hasWindowFocus:true 获得焦点,开启动画;false 失去焦点,停止动画
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
if (valueAnimator != null) {
//开启动画
valueAnimator.pause();
countDownTimer.cancel();
countDownTimer = null;
}
if (countDownTimer != null) {
countDownTimer.cancel();
countDownTimer = null;
}
}
}

总结:这里加入了新的背景图、多个防御塔随意摆放、一旦摆放就无法移动(后续加入拆除、升级)等功能。难点还是在野怪移动上,还有多个防御塔攻击互相不影响。

问题:现在的思路是刷新一下,野怪走一步,后续如果加入减速防御塔的话,应该怎么走呢,多个野怪如何做到行走速度互不影响呢。

个人思路:可以正常野怪一次走5步,如果被减速防御塔打中后,就把5步见为2步,position+5调整为position+2。这里只是记录学习View的过程,不要较真哦。

自定义View6 -塔防小游戏:第三篇防御塔随意放置+多组野怪的更多相关文章

  1. 自定义View5 -塔防小游戏:第二篇防御塔随意放置

    第一篇:一个防御塔+多个野怪(简易版) 第二篇:防御塔随意放置 自定义View,处理事件分发,up,move,down. 第三篇:防御塔随意放置+多组野怪 第四篇:多波野怪 第五篇:杀死野怪获得金币 ...

  2. Python制作塔防小游戏

    开发工具 Python版本:3.6.4 相关模块: pygame模块: 以及一些Python自带的模块.

  3. 自定义View4-塔防小游戏第一篇:一个防御塔+多个野怪(简易版)*

    塔防小游戏 第一篇:一个防御塔+多个野怪(简易版)    1.canvas画防御塔,妖怪大道,妖怪行走路线    2.防御塔攻击范围是按照妖怪与防御塔中心距离计算的,大于防御塔半径则不攻击,小于则攻击 ...

  4. unity3D游戏开发实战原创视频讲座系列9之塔防类游戏开发第一季

    解说文件夹 塔防游戏0基础篇... 第一讲  游戏演示和资源介绍... 第二讲  游戏场景的完毕... 第三讲  预制体的制作... 第四讲  敌人的随机产生和按路径行走... 第五讲  塔防工具的产 ...

  5. web版扫雷小游戏(三)

    ~~~接上篇,上篇介绍了游戏实现过程中第一个比较繁琐的地方,现在展现在玩家面前的是一个有血有肉的棋盘,从某种意义上说玩家已经可以开始游戏了,但是不够人性化,玩家只能一个一个节点的点开,然后判断,然后标 ...

  6. 重拾H5小游戏之入门篇(二)

    上一篇,水了近千字,很酸爽,同时表达了"重拾"一项旧本领并不容易,还有点题之效果.其实压缩起来就一句话:经过了一番记忆搜索,以及try..catch的尝试后,终于选定了Phaser ...

  7. Unity 3D游戏-塔防类游戏源码:重要方法和功能的实现

    Unity-塔防游戏源码 本文提供全流程,中文翻译.Chinar坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) 1 2 3 4 5 6 7 8 9 ...

  8. 软件设计之基于Java的连连看小游戏(三)——所有功能的实现

    新年快乐!期末接二连三的考试实在太忙了忘记连连看没有更新完,今天想要学习生信时才发现.所以这次直接把连连看所有功能全部放上. 在传统的连连看的基础上,我增加了上传头像的功能,即可以自行上传图片作为游戏 ...

  9. Pygame小游戏练习三

    @Python编程从入门到实践 Python项目练习 七.创建Passenger类 创建passenger.py文件,创建Passenger类,控制乘客属性和行为 # passenger.py imp ...

随机推荐

  1. 【每天学一点-01】 在SpringBoot项目中使用Swagger2

    今天在做毕设的时候,发现在前后端分离的情况下,去调用接口数据时很不方便,然后回想过去,和同学一起做项目的时候,他负责后端,我负责前端,当时调用他的弄好的接口可以说是非常方便,主要是可以通过UI页面直接 ...

  2. Nginx第三方模块Ngx-dyups安装过程

    Ngx-dyups是什么,能干什么 它是一个Nginx第三方动态Upstream配置模块,可以实现在不重启Nginx情况下动态更新反向代理Upstream表.该模块由淘宝开发团队维护,淘宝自家的Ten ...

  3. 如何用WebGPU流畅渲染百万级2D物体?

    大家好~本文使用WebGPU和光线追踪算法,从0开始实现和逐步优化Demo,展示了从渲染500个2D物体都吃力到流畅渲染4百万个2D物体的优化过程和思路 目录 需求 成果 1.选择渲染的算法 2.实现 ...

  4. Linux(Centos7) 实例搭建 FTP 服务

    本文以 CentOS 7.2 64位系统为例,使用 vsftpd 作为 FTP 服务端,FileZilla 作为客户端.指导您如何在 Linux 云服务器上搭建 FTP 服务. 操作步骤 安装 vsf ...

  5. 浩若烟海事半功倍|利用Docker容器技术构建自动化分布式web测试集群Selenium Grid

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_195 "世界上有那么多城市,城市里有那么多的酒馆,可她,却偏偏走进了我的-",这是电影<卡萨布拉卡> ...

  6. 用JavaScript计算平年闰年

    var i = prompt("请输入你要查询的年份") if(i % 4 == 0 && i % 100 != 0 || i % 400 == 0){ conso ...

  7. Python爬虫:为什么你爬取不到网页数据

    前言: 之前小编写了一篇关于爬虫为什么爬取不到数据文章(文章链接为:Python爬虫经常爬不到数据,或许你可以看一下小编的这篇文章), 但是当时小编也是胡乱编写的,其实里面有很多问题的,现在小编重新发 ...

  8. SP104 Highways (矩阵树,高斯消元)

    矩阵树定理裸题 //#include <iostream> #include <cstdio> #include <cstring> #include <al ...

  9. django中的自定义分页器

    1.什么是自定义分页器 当我们需要在前端页面展示的数据太多的时候,我们总不能将数据展示在一页上面吧!这时,我们就需要自定义一个分页器,将数据分成特定的页数进行展示,每一页展示固定条数的数据! 2.为什 ...

  10. EMAS Serverless系列~4步教你快速搭建小程序

    体验简介 本实验基于 EMAS Serverless 的云函数.云数据库.云存储等云服务能力一站式快速开发小程序<私人云相册>.Demo 主要包括如下功能: 1 相册管理 2 上传相片 3 ...