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

  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. 移动web开发02

    虽然视口很多,但是我们只用一个.就是理想视口. 单标签. 原本是高宽都300的.后来变成移动端后没有变成300/750,也不是300/1334.而是占据了一大半(300/375).甚至375就满屏了. ...

  2. python编程思想及对象与类

    目录 编程思想 面向对象 面向过程 对象与类的概念 对象与类的创建 对象的实例化方法-独有数据 编程思想 1.面向对象 1.1. 面向对象前戏 案例:人狗大战 # 需求:人狗大战# 1.'创造'出人和 ...

  3. 在DELL服务器上安装windows2012 r2服务器系统

    主要过程: 1.准备安装光盘,开启服务器,当出现画面按F10进入服务器自带光盘系统安装向导.(若没有系统光盘,可以用软蝶通刻一个服务系统到+R的光盘).进入后选择设置和安装系统. 2.开始安装前,提示 ...

  4. 两天时间学习的html的知识笔记

    坚持努力背 特殊字符: 空格符  < 小于号 <> 大于号 >& 和号 &¥ 人民币 ¥  版权 ©R 注册商标 ®. 摄氏度 ° 正负号 ±X 乘号 × 除号 ...

  5. 推荐几款最好用的MySQL开源客户端,建议收藏!

    一.摘要 众所周知,MYSQL 是目前使得最广泛.最流行的数据库技术之一,为了更方便的管理数据库,市场上出现了大量软件公司和个人开发者研发的客户端工具,比如我们所熟知的比较知名的客户端: Navica ...

  6. Spring源码 14 IOC refresh方法9

    参考源 https://www.bilibili.com/video/BV1tR4y1F75R?spm_id_from=333.337.search-card.all.click https://ww ...

  7. Linux—进程管理

    Linux 进程管理 1.进程管理介绍 1.1 什么是进程? 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础. 简而言之 ...

  8. NOI 2019 省选模拟赛 T1【JZOJ6082】 染色问题(color) (多项式,数论优化)

    题面 一根长为 n 的无色纸条,每个位置依次编号为 1,2,3,-,n ,m 次操作,第 i 次操作把纸条的一段区间 [l,r] (l <= r , l,r ∈ {1,2,3,-,n})涂成颜色 ...

  9. java数组---特点,边界

    数组的四个基本特点 1.其长度是确定的.数组一旦被创建,它的大小就是不可以改变的. 2.其元素必须是相同类型,不允许出现混合类型. 3.数组中的元素可以是任何数据类型,包括基本类型和引用类型. 4.数 ...

  10. win10电脑自动连接蓝牙设备攻略

    每次在电脑上想连接蓝牙耳机,但不想手动去连接怎么办呢? 自动连接! 其实微软已经解决了这个问题,只要打开蓝牙,他就会自动匹配上次连接的设备 打开设置--->设备勾选 "显示使用&quo ...