- 浏览: 126972 次
- 性别:
- 来自: 武汉
最新评论
-
cangbaotu:
我觉得对于开发者来说,能脚本化编写爬虫是一件挺开心的事情( ̄▽ ...
用Watij爬取网页内容 -
qq_31941407:
请问怎么设置才不是自动播放啊,为什么我一运行就是自动播放啊。。 ...
Android 歌词同步滚动效果 -
whl_1990:
这个有点抖动,不知道怎么去抖动啊?将线程睡眠时间降低也不行,可 ...
Android 歌词同步滚动效果 -
翼扬gg:
Android 歌词同步滚动效果 -
至尊包:
Instance方法上应该加上同步锁 方法名小写
单例模式(Singleton)在SQLite操作中的应用
歌词是播放器类App必不可少的组件,而一般的歌词组件都需要做到歌词的显示与播放进度同步。我们知道,歌词是如下所示的文件:
lrc
[ti:原来爱情这么伤]
[ar:梁咏琪]
[al:给自己的情歌]
[00:00.55]梁咏琪 - 原来爱情这么伤
[00:05.43]作词:彭学斌
[00:06.68]作曲:彭学斌
[00:09.63]
[00:22.27]我睁开眼睛 却感觉不到天亮
[00:29.74]东西吃一半 莫名其妙哭一场
[00:37.06]我忍住不想 时间变得更漫长
[00:44.09]也与你有关 否则又开始胡思乱想
[00:53.81]我日月无光 忙得不知所以然
[00:59.96]找朋友交谈 其实全帮不上忙
[01:07.49]以为会习惯 有你在才是习惯
[01:14.62]你曾住在我心上 现在空了一个地方
[01:21.89]原来爱情这么伤 比想象中还难
[01:29.90]泪水总是不听话 幸福躲起来不声不响
[01:37.43]太多道理太牵强 道理全是一样
[01:44.34]说的时候很简单 爱上后却正巧打乱
[02:00.00]我日月无光 忙得不知所以然
[02:07.41]找朋友交谈 其实全帮不上忙
[02:15.07]以为会习惯 有你在才是习惯
[02:21.88]你曾住在我心上 现在空了一个地方
[02:29.38]原来爱情这么伤 比想象中还难
[02:36.60]泪水总是不听话 幸福躲起来不声不响
[02:44.22]太多道理太牵强 道理全是一样
[02:50.78]说的时候很简单 爱上后却正巧打乱
[03:00.32]只想变的坚强 强到能够去忘
[03:07.29]无所谓悲伤 只要学会抵抗
[03:14.19]原来爱情这么伤
[03:20.78]原来爱情是这样 这样峰回路转
[03:28.12]泪水明明流不干 瞎了眼还要再爱一趟
[03:35.83]有一天终于打完 思念的一场战
[03:43.45]回过头再看一看 原来爱情那么伤
[03:54.76]下次还会不会这样
[88:88.88]
[ar:梁咏琪]
[al:给自己的情歌]
[00:00.55]梁咏琪 - 原来爱情这么伤
[00:05.43]作词:彭学斌
[00:06.68]作曲:彭学斌
[00:09.63]
[00:22.27]我睁开眼睛 却感觉不到天亮
[00:29.74]东西吃一半 莫名其妙哭一场
[00:37.06]我忍住不想 时间变得更漫长
[00:44.09]也与你有关 否则又开始胡思乱想
[00:53.81]我日月无光 忙得不知所以然
[00:59.96]找朋友交谈 其实全帮不上忙
[01:07.49]以为会习惯 有你在才是习惯
[01:14.62]你曾住在我心上 现在空了一个地方
[01:21.89]原来爱情这么伤 比想象中还难
[01:29.90]泪水总是不听话 幸福躲起来不声不响
[01:37.43]太多道理太牵强 道理全是一样
[01:44.34]说的时候很简单 爱上后却正巧打乱
[02:00.00]我日月无光 忙得不知所以然
[02:07.41]找朋友交谈 其实全帮不上忙
[02:15.07]以为会习惯 有你在才是习惯
[02:21.88]你曾住在我心上 现在空了一个地方
[02:29.38]原来爱情这么伤 比想象中还难
[02:36.60]泪水总是不听话 幸福躲起来不声不响
[02:44.22]太多道理太牵强 道理全是一样
[02:50.78]说的时候很简单 爱上后却正巧打乱
[03:00.32]只想变的坚强 强到能够去忘
[03:07.29]无所谓悲伤 只要学会抵抗
[03:14.19]原来爱情这么伤
[03:20.78]原来爱情是这样 这样峰回路转
[03:28.12]泪水明明流不干 瞎了眼还要再爱一趟
[03:35.83]有一天终于打完 思念的一场战
[03:43.45]回过头再看一看 原来爱情那么伤
[03:54.76]下次还会不会这样
[88:88.88]
我们需要读取以上歌词文件的每一行转换成成一个个歌词实体:
package com.music.lyricsync; public class LyricObject { public int begintime; // 开始时间 public int endtime; // 结束时间 public int timeline; // 单句歌词用时 public String lrc; // 单句歌词 }
可根据当前播放器的播放进度与每句歌词的开始时间,得到当前屏幕中央高亮显示的那句歌词。在UI线程中另起线程,通过回调函数 onDraw() 每隔100ms重新绘制屏幕,实现歌词平滑滚动的动画效果。MainActivity代码如下:
package com.music.lyricsync; import java.io.IOException; import android.app.Activity; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; public class MainActivity extends Activity { /** Called when the activity is first created. */ private LyricView lyricView; private MediaPlayer mediaPlayer; private Button button; private SeekBar seekBar; private String mp3Path; private int INTERVAL=45;//歌词每行的间隔 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // this.requestWindowFeature(Window.FEATURE_NO_TITLE); // getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.main); mp3Path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/LyricSync/1.mp3"; lyricView = (LyricView) findViewById(R.id.mylrc); mediaPlayer = new MediaPlayer(); // this.requestWindowFeature(Window.FEATURE_NO_TITLE); ResetMusic(mp3Path); SerchLrc(); lyricView.SetTextSize(); button = (Button) findViewById(R.id.button); button.setText("播放"); seekBar = (SeekBar) findViewById(R.id.seekbarmusic); seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { // TODO Auto-generated method stub } @Override public void onStartTrackingTouch(SeekBar seekBar) { // TODO Auto-generated method stub } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // TODO Auto-generated method stub if (fromUser) { mediaPlayer.seekTo(progress); lyricView.setOffsetY(220 - lyricView.SelectIndex(progress) * (lyricView.getSIZEWORD() + INTERVAL-1)); } } }); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub if (mediaPlayer.isPlaying()) { button.setText("播放"); mediaPlayer.pause(); } else { button.setText("暂停"); mediaPlayer.start(); lyricView.setOffsetY(220 - lyricView.SelectIndex(mediaPlayer.getCurrentPosition()) * (lyricView.getSIZEWORD() + INTERVAL-1)); } } }); mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { ResetMusic(mp3Path); lyricView.SetTextSize(); lyricView.setOffsetY(200); mediaPlayer.start(); } }); seekBar.setMax(mediaPlayer.getDuration()); new Thread(new runable()).start(); } public void SerchLrc() { String lrc = mp3Path; lrc = lrc.substring(0, lrc.length() - 4).trim() + ".lrc".trim(); LyricView.read(lrc); lyricView.SetTextSize(); lyricView.setOffsetY(350); } public void ResetMusic(String path) { mediaPlayer.reset(); try { mediaPlayer.setDataSource(mp3Path); mediaPlayer.prepare(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } class runable implements Runnable { @Override public void run() { // TODO Auto-generated method stub while (true) { try { Thread.sleep(100); if (mediaPlayer.isPlaying()) { lyricView.setOffsetY(lyricView.getOffsetY() - lyricView.SpeedLrc()); lyricView.SelectIndex(mediaPlayer.getCurrentPosition()); seekBar.setProgress(mediaPlayer.getCurrentPosition()); mHandler.post(mUpdateResults); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } Handler mHandler = new Handler(); Runnable mUpdateResults = new Runnable() { public void run() { lyricView.invalidate(); // 更新视图 } }; }
歌词View的代码如下:
package com.music.lyricsync; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.util.Iterator; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class LyricView extends View{ private static TreeMap<Integer, LyricObject> lrc_map; private float mX; //屏幕X轴的中点,此值固定,保持歌词在X中间显示 private float offsetY; //歌词在Y轴上的偏移量,此值会根据歌词的滚动变小 private static boolean blLrc=false; private float touchY; //当触摸歌词View时,保存为当前触点的Y轴坐标 private float touchX; private boolean blScrollView=false; private int lrcIndex=0; //保存歌词TreeMap的下标 private int SIZEWORD=0;//显示歌词文字的大小值 private int INTERVAL=45;//歌词每行的间隔 Paint paint=new Paint();//画笔,用于画不是高亮的歌词 Paint paintHL=new Paint(); //画笔,用于画高亮的歌词,即当前唱到这句歌词 public LyricView(Context context){ super(context); init(); } public LyricView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /* (non-Javadoc) * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw(Canvas canvas) { if(blLrc){ paintHL.setTextSize(SIZEWORD); paint.setTextSize(SIZEWORD); LyricObject temp=lrc_map.get(lrcIndex); canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*lrcIndex, paintHL); // 画当前歌词之前的歌词 for(int i=lrcIndex-1;i>=0;i--){ temp=lrc_map.get(i); if(offsetY+(SIZEWORD+INTERVAL)*i<0){ break; } canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*i, paint); } // 画当前歌词之后的歌词 for(int i=lrcIndex+1;i<lrc_map.size();i++){ temp=lrc_map.get(i); if(offsetY+(SIZEWORD+INTERVAL)*i>600){ break; } canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*i, paint); } } else{ paint.setTextSize(25); canvas.drawText("找不到歌词", mX, 310, paint); } super.onDraw(canvas); } /* (non-Javadoc) * @see android.view.View#onTouchEvent(android.view.MotionEvent) */ @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub System.out.println("bllll==="+blScrollView); float tt=event.getY(); if(!blLrc){ //return super.onTouchEvent(event); return super.onTouchEvent(event); } switch(event.getAction()){ case MotionEvent.ACTION_DOWN: touchX=event.getX(); break; case MotionEvent.ACTION_MOVE: touchY=tt-touchY; offsetY=offsetY+touchY; break; case MotionEvent.ACTION_UP: blScrollView=false; break; } touchY=tt; return true; } public void init(){ lrc_map = new TreeMap<Integer, LyricObject>(); offsetY=320; paint=new Paint(); paint.setTextAlign(Paint.Align.CENTER); paint.setColor(Color.GREEN); paint.setAntiAlias(true); paint.setDither(true); paint.setAlpha(180); paintHL=new Paint(); paintHL.setTextAlign(Paint.Align.CENTER); paintHL.setColor(Color.RED); paintHL.setAntiAlias(true); paintHL.setAlpha(255); } /** * 根据歌词里面最长的那句来确定歌词字体的大小 */ public void SetTextSize(){ if(!blLrc){ return; } int max=lrc_map.get(0).lrc.length(); for(int i=1;i<lrc_map.size();i++){ LyricObject lrcStrLength=lrc_map.get(i); if(max<lrcStrLength.lrc.length()){ max=lrcStrLength.lrc.length(); } } SIZEWORD=320/max; } protected void onSizeChanged(int w, int h, int oldw, int oldh) { mX = w * 0.5f; super.onSizeChanged(w, h, oldw, oldh); } /** * 歌词滚动的速度 * * @return 返回歌词滚动的速度 */ public Float SpeedLrc(){ float speed=0; if(offsetY+(SIZEWORD+INTERVAL)*lrcIndex>220){ speed=((offsetY+(SIZEWORD+INTERVAL)*lrcIndex-220)/20); } else if(offsetY+(SIZEWORD+INTERVAL)*lrcIndex < 120){ Log.i("speed", "speed is too fast!!!"); speed = 0; } // if(speed<0.2){ // speed=0.2f; // } return speed; } /** * 按当前的歌曲的播放时间,从歌词里面获得那一句 * @param time 当前歌曲的播放时间 * @return 返回当前歌词的索引值 */ public int SelectIndex(int time){ if(!blLrc){ return 0; } int index=0; for(int i=0;i<lrc_map.size();i++){ LyricObject temp=lrc_map.get(i); if(temp.begintime<time){ ++index; } } lrcIndex=index-1; if(lrcIndex<0){ lrcIndex=0; } return lrcIndex; } /** * 读取歌词文件 * @param file 歌词的路径 * */ public static void read(String file) { TreeMap<Integer, LyricObject> lrc_read =new TreeMap<Integer, LyricObject>(); String data = ""; try { File saveFile=new File(file); // System.out.println("是否有歌词文件"+saveFile.isFile()); if(!saveFile.isFile()){ blLrc=false; return; } blLrc=true; //System.out.println("bllrc==="+blLrc); FileInputStream stream = new FileInputStream(saveFile);// context.openFileInput(file); BufferedReader br = new BufferedReader(new InputStreamReader(stream,"GB2312")); int i = 0; Pattern pattern = Pattern.compile("\\d{2}"); while ((data = br.readLine()) != null) { // System.out.println("++++++++++++>>"+data); data = data.replace("[","");//将前面的替换成后面的 data = data.replace("]","@"); String splitdata[] =data.split("@");//分隔 if(data.endsWith("@")){ for(int k=0;k<splitdata.length;k++){ String str=splitdata[k]; str = str.replace(":","."); str = str.replace(".","@"); String timedata[] =str.split("@"); Matcher matcher = pattern.matcher(timedata[0]); if(timedata.length==3 && matcher.matches()){ int m = Integer.parseInt(timedata[0]); //分 int s = Integer.parseInt(timedata[1]); //秒 int ms = Integer.parseInt(timedata[2]); //毫秒 int currTime = (m*60+s)*1000+ms*10; LyricObject item1= new LyricObject(); item1.begintime = currTime; item1.lrc = ""; lrc_read.put(currTime,item1); } } } else{ String lrcContenet = splitdata[splitdata.length-1]; for (int j=0;j<splitdata.length-1;j++) { String tmpstr = splitdata[j]; tmpstr = tmpstr.replace(":","."); tmpstr = tmpstr.replace(".","@"); String timedata[] =tmpstr.split("@"); Matcher matcher = pattern.matcher(timedata[0]); if(timedata.length==3 && matcher.matches()){ int m = Integer.parseInt(timedata[0]); //分 int s = Integer.parseInt(timedata[1]); //秒 int ms = Integer.parseInt(timedata[2]); //毫秒 int currTime = (m*60+s)*1000+ms*10; LyricObject item1= new LyricObject(); item1.begintime = currTime; item1.lrc = lrcContenet; lrc_read.put(currTime,item1);// 将currTime当标签 item1当数据 插入TreeMap里 i++; } } } } stream.close(); } catch (FileNotFoundException e) { } catch (IOException e) { } /* * 遍历hashmap 计算每句歌词所需要的时间 */ lrc_map.clear(); data =""; Iterator<Integer> iterator = lrc_read.keySet().iterator(); LyricObject oldval = null; int i =0; while(iterator.hasNext()) { Object ob =iterator.next(); LyricObject val = (LyricObject)lrc_read.get(ob); if (oldval==null) oldval = val; else { LyricObject item1= new LyricObject(); item1 = oldval; item1.timeline = val.begintime-oldval.begintime; lrc_map.put(new Integer(i), item1); i++; oldval = val; } if (!iterator.hasNext()) { lrc_map.put(new Integer(i), val); } } } /** * @return the blLrc */ public static boolean isBlLrc() { return blLrc; } /** * @return the offsetY */ public float getOffsetY() { return offsetY; } /** * @param offsetY the offsetY to set */ public void setOffsetY(float offsetY) { this.offsetY = offsetY; } /** * @return 返回歌词文字的大小 */ public int getSIZEWORD() { return SIZEWORD; } /** * 设置歌词文字的大小 * @param sIZEWORD the sIZEWORD to set */ public void setSIZEWORD(int sIZEWORD) { SIZEWORD = sIZEWORD; } }
xml布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#FFFFFF" > <com.music.lyricsync.LyricView android:id="@+id/mylrc" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginBottom="50dip" android:layout_marginTop="50dip" /> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:orientation="horizontal" > <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <SeekBar android:id="@+id/seekbarmusic" android:layout_width="205px" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginBottom="5px" android:progress="0" /> </LinearLayout> </RelativeLayout>
程序运行后如下图所示:
运行程序前,先在SDCard根目录下新建LyricSync目录,将 歌曲和歌词.zip 中的 1.mp3 和 1.lrc 文件解压放入LyricSync目录下即可。
评论
4 楼
qq_31941407
2015-12-24
请问怎么设置才不是自动播放啊,为什么我一运行就是自动播放啊。。
3 楼
whl_1990
2015-06-05
这个有点抖动,不知道怎么去抖动啊?将线程睡眠时间降低也不行,可能要改成SurfaceView
2 楼
翼扬gg
2015-05-16
1 楼
blue飞扬
2013-09-12
求教那个歌词速度和文字大小设定是怎么完成的啊?新手,有点看不懂
发表评论
-
FFmpeg 移植 Android
2012-07-27 17:12 29622近期项目需要解析苹果的HLS流媒体协议,而FFmpeg ... -
Android 加载现有数据库文件
2012-03-21 22:03 6002想把一个数据库db文件放在 res/raw/ 目录下,安装应用 ... -
用Dialog创建带箭头的对话框
2012-03-07 18:58 1929很多应用中,在点击Button后,会弹出一个带箭头 ... -
Android消息队列模型——Thread,Handler,Looper,Massage Queue
2012-03-07 09:17 11545Android系统的消息队列和消息循环都是针对具 ... -
浅谈Java平台与Android平台
2012-03-06 13:46 2271先说一说Java平台与Java语言的关系: ... -
Android UI主线程与子线程
2012-03-05 22:47 1678在一个Android 程序开始运行的时候,会单独 ... -
assets目录与res/raw、res/drawable目录的区别
2012-02-26 22:44 6879assets目录与res下的raw、d ... -
Dialog向Activity传递数据
2012-02-22 21:11 9450我们知道,从一个Activity向另一个Acti ... -
用Dialog创建带箭头的对话框
2012-02-22 18:25 5570很多应用中,在点击Button后,会弹出一个带箭头 ...
相关推荐
CoCoPlayer 可以让初学者学习使用android播放音乐,实现歌词同步滚动显示效果,自定义view等用法,也包含了LRC解析相关代码,解析LRC使用的正则表达式~请多多支持~~CoCoPlayer 完整正在开发中,开发完成之后将把源码...
主要介绍了Android实现两个ScrollView互相联动的同步滚动效果代码,涉及Android操作ScrollView实现联动功能的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
最近在做Android 的MP3播放的项目,要实现歌词的自动滚动,以及同步显示。 lyric的歌词解析主要用yoyoplayer里面的,显示部分参考了这里 ,这里只是模拟MP3歌词的滚动。 先上一下效果图: 滚动实现的代码其实也简单...
我们需要读取以上歌词文件的每一行转换成成一个个歌词实体: 代码如下: public class ...在UI线程中另起线程,通过回调函数 onDraw() 每隔100ms重新绘制屏幕,实现歌词平滑滚动的动画效果。MainActivity代码如下: 代码
文件中收集了三个实现股票联动效果的控件,上下左右同步滚动效果。
继承自ViewGroup然后配合Scroller完成滑动效果 主要弄清楚scrollTo和scrollBy 及scroller的用法即可很容易实现
支持和 ListView 的无缝同步滚动 和 CoordinatorLayout 的嵌套滚动 . 支持自动刷新、自动上拉加载(自动检测列表惯性滚动到底部,而不用手动上拉). 支持自定义回弹动画的插值器,实现各种炫酷的动画效果. 支持设置...
公司的项目需要一个视频的滚动列表。 搜了些文章比较常见的是根据列表项的可视百分比来判断的。实现起来略复杂。 这里想了一个在要求不高的情况...基于SurfaceView的VideoView由于没有同步缓冲区,它不能在ListView中
使用RecyclerView实现双表联动效果,点击左侧分类后右侧数据改变,滑动右侧列表时左侧分类跟随变动~ 如有不懂可在我该篇博客内留言,博客地址:https://blog.csdn.net/qq_20451879/article/details/81664188
16.3.3 使用Camera实现2D图像的深度效果 16.3.4 探索AnimationListener类 16.3.5 关于变换矩阵的一些说明 16.4 资源 16.5 小结 第17章 地图和基于位置的服务 17.1 地图包 17.1.1 从Google获取...
16.3.3 使用Camera实现2D图像的深度效果 16.3.4 探索AnimationListener类 16.3.5 关于变换矩阵的一些说明 16.4 资源 16.5 小结 第17章 地图和基于位置的服务 17.1 地图包 17.1.1 从Google获取...
}1.3同步一下配置结束了2.使用 2.1、正负数的使用 默认效果从零开始,以最小精度开始动画 ,支持正负数,可以直接设置 ntvTestOne.setNumberValue("100"); ntvTestOne.setNumberValue("-100");2.2支持...
因为最近一个项目的需求,写了一个双向的ListView. ...并且达到了很好的同步效果. 想到一直以来都是从网上拿东西,也应该有贡献精神.所以这里把代码共享,本人尽量的屏蔽了实现的细节.以下提供API,希望对大家有帮助.
支持广泛的便笺粒度,允许进行网格内或网格外编程直观的多点触控支持无处不在手机或平板电脑可以处理的曲目数量回路长度和位置可调一系列内置鼓采样丰富的效果集,包括混响,立体声延迟(自由或速度同步),...
*Tickers:在全球范围内添加滚动/闪烁信息! *装饰师:个性化每个国家的外观(颜色、质地、标签) *地图编辑器:修改提供的地图和创建您的虚构场景! *小地图:拖放迷你地图预置到场景方便一键式世界导航! -...
6.9 多层背景滚动效果 188 6.10 本章小结 190 第7章 物理模拟与碰撞检测 192 7.1 概述 192 7.2 游戏中的碰撞检测 193 7.3 碰撞检测的方法 194 7.3.1 平面几何在碰撞检测中的应用 194 7.3.2 物体的包围盒 197 7.3.3 ...