C语言实现动态气泡碰撞和移动的效果
作者 | 将狼才鲸 |
---|---|
创建日期 | 2023-01-29 |
- Git源码仓库地址:C语言实现动态气泡碰撞和移动的效果
- CSDN文章地址:01 C语言实现动态气泡碰撞和移动的效果
一、前言
-
想要实现多气泡相互碰撞的效果;
- 想着这种在Win7壁纸中早就实现的效果,网上应该能找到源码的,但是,事实是我并没有找到能在实际产品中使用的,成熟的源码,没有办法,只有自己写。
-
想要的效果有:
- 一开始出现一堆相互不重叠的,大小不一的气泡;
- 或者一开始气泡从底下或者侧面出现,然后方向有一些随机的不同;
- 自然和符合物理规律的碰撞效果;
- 一开始出现一堆相互不重叠的,大小不一的气泡;
-
使用windows api、c语言、vs
-
一些参考网址:
- Win32 API 编程 —— 前言
- Windows API
二、各个阶段的代码效果
1)一个圆形沿着曲线运动
- 参考的源码来源:windows API 画正弦曲线,并添加一沿线走的动态圆
- Git版本:
- 2023-01-06 10:29 +0800 o [master] 增加动态气泡Demo的工程
- commit 8cce3e91e69b33723581117e689b5453fbdc08ed
2)多个圆形从上往下运动
- Git版本:
- 2023-01-09 11:52 +0800 o [CASE 16] 修正帧序号显示错误的问题
- commit b9e7acf2f37ef82c995a3486a7533ab4cd81d953
3)多个圆形发生碰撞
- 参考网址:
- 如何判断2个元素发生了碰撞
- 一个canvas动画中,如何处理气泡与其它气泡的合理碰撞?
- java两个小球相撞 JAVA小游戏之两个物体碰撞产生的碰撞检测
- cocos2dx 多个物体碰撞检测
- 每一帧检查多个球之间碰撞的快速方法。
- Git版本:
- 2023-01-16 11:00 +0800 zhangjing o [develop] [CASE 16] 已修正有一半情况碰撞角度不对的bug
- commit 0b3c7393da4bd9764a26323d4b5d53ffdbc50409
三、代码展示
/******************************************************************************
* \brief 显示界面中实现气泡或者圆圈的碰撞、移动、加速度等功能
* \details 在Windows下使用VS编译,直接使用Windows API(C/C++),不使用MFC、WPF等框架
* \note UTF-8 BOM编码
* \author 将狼才鲸
* \date 2023-01-09
* \remarks
* 最开始代码的参考网址:
* [windows API 画正弦曲线,并添加一沿线走的动态圆](https://blog.csdn.net/IT_li_wenshun/article/details/52780706)
* 其它的参考网址:
* [【数学知识】角度与弧度](https://blog.csdn.net/wbf1013/article/details/122811230)
******************************************************************************/
/*********************************** 头文件 ***********************************/
#include <windows.h> /* Windows API的头文件 */
#include <stdio.h> /* printf */
#include <stdlib.h> /* malloc free rand abs */
#include <math.h> /* sin cos sqrt asin fabs */
#include <limits.h> /* INT_MAX */
/*********************************** 宏定义 ***********************************/
//#define COLLISION_ALGORITHM_TEST /* 测试碰撞算法时只生成2个气泡 */
#define BUBBLES_MAX_COUNT 100 /* 最多允许出现100个气泡 */
#define PI 3.1415926
#define FPS 60 /* 帧率,Frames per Second */
#define DEFAULT_RADIUS_RATIO 100 /* 默认的气泡半径是窗口宽度的多少分之1 */
#define MIN_SPEED 2 /* 单位:像素/s,小球的最低运动速度,防止静止不动 */
/********************************** 类型定义 **********************************/
/* 每一个气泡的参数 */
typedef struct _bubble_context {
float x; /* 当前圆心的位置坐标,单位为像素 */
float y; /* 当前圆心的位置坐标,单位为像素 */
int x_last; /* 上一帧时的坐标x */
int y_last; /* 上一帧时的坐标y */
int radius; /* 半径,单位为像素;不做气泡大小动态适配窗口大小 */
int mass; /* 质量;假设所有气泡都是同样的物质,质量和表面积正相关,能通过半径直接算出来 */
int speed; /* 当前速度;单位为像素/s,因加速度的存在,所以可能每一刻的速度都不一样 */
double speed_direction; /* 当前速度的运动方向,单位为弧度,范围为0~2Pai(π) */
int acceleration; /* 加速度;单位为像素/s^2 */
double acceleration_direction; /* 加速度的方向,单位为弧度,范围为0~2Pai(π) */
bool collision_flag; /* 当前时刻是否发生碰撞 */
int collision_target; /* 与之碰撞的小球序号 */
int time_left; /* 剩下的存活时间,单位为帧数 */
int opacity; /* 透明度,范围为0~100;0为不透明,100为全透明 */
} BUBBLE_CONTEXT_T;
/* 顺时针的16个方向 */
typedef enum _common_direction {
NORTH = 0, /* 北↑ */
NORTH_NORTHEAST, /* 东北偏北↗ */
NORTHEAST, /* 东北↗ */
EAST_NORTHEAST, /* 东北偏东↗ */
EAST, /* 东→ */
EAST_SOUTHEAST, /* 东南偏东↘ */
SOUTHEAST, /* 东南↘ */
SOUTH_SOUTHEAST, /* 东南偏南↘ */
SOUTH, /* 南↓ */
SOUTH_SOUTHWEST, /* 西南偏南↙ */
SOUTHWEST, /* 西南↙ */
WEST_SOUTHWEST, /* 西南偏西↙ */
WEST, /* 西← */
WEST_NORTHWEST, /* 西北偏西↖ */
NORTHWEST, /* 西北↖ */
NORTH_NORTHWEST, /* 西北偏北↖ */
} COMMON_DIRECTION;
/********************************** 全局变量 **********************************/
static WCHAR titleName[] = L"Windows API Demo"; /* 窗口标题文字 */
static int g_window_x, g_window_y; /* 窗口宽度和高度 */
static int g_fno; /* 帧序号,每一帧都会对所有气泡的运动做一次操作 */
static int g_bubbles_count; /* 当前圆的总个数 */
static int default_radius; /* 当前圆的默认半径设置为窗口宽度的1/DEFAULT_RADIUS_RATIO */
static BUBBLE_CONTEXT_T *bubbles_ctx; /* 所有气泡的信息 */
static bool window_start_flag = FALSE; /* 是否已获取到窗口或者屏幕的宽高,单位为像素 */
static HANDLE hThread; /* 创建的绘图线程 */
static HPEN hPenWhite; /* 白色画笔 */
static HPEN hPenRed; /* 红色画笔 */
static int ms_per_frame = 1000 / FPS; /* 每帧所占据的时间,单位为ms */
static int frame_delay_adjust = 0; /* 每帧延时中要减去的时间,也就是计算每帧数据时所消耗的时间 */
/**
* 如何判断碰撞?
* 方法一:所有的气泡两两判断位置和半径,如果重合则认为是碰撞;
* 方法二:每帧刷新时,对每一个像素进行遍历,如果该像素是否距离两个的气泡的距离都小于等于直径,则这两个气泡发生碰撞;
* 其它方法:如划分空间、最近对点算法、德劳内三角测量法,都比较复杂;所以我这里直接对所有圆两两比较,由此可见圆的个数不能太多;
* 当前使用的是方法一:每帧时间执行一次update,在update中判断碰撞并修改碰撞后的物体速度;
*
* 如何判断多个物体堆在一起发生了碰撞?
* 已发生碰撞的两个球直接从碰撞目标中移除,剩下的其它球忽略碰撞,这样会产生图形重合,但是暂时只能这样了;
*
* 碰撞时可以选择检测是否与屏幕边缘碰撞
* 需要额外判断同时碰到两个边缘的情况,但也可以分别先后判断和两个外壁的碰撞来镜像反射的效果;
*/
/********************************** 私有函数 **********************************/
/**
* \brief 方向转弧度
* \details 设定为最上方为方向北,最上方为角度0、弧度0的方向
*/
static double direction_to_radian(COMMON_DIRECTION direct)
{
double radian = 0.0;
switch (direct) {
case NORTH: /* 北↑,0° */
radian = 0.0;
break;
case NORTH_NORTHEAST: /* 东北偏北↗,22.5° */
radian = PI / 8;
break;
case NORTHEAST: /* 东北↗,45° */
radian = PI / 4;
break;
case EAST_NORTHEAST: /* 东北偏东↗,67.5° */
radian = PI / 4 + PI / 8;
case EAST: /* 东→,90° */
radian = PI / 2;
break;
case EAST_SOUTHEAST: /* 东南偏东↘ */
radian = PI / 2 + PI / 8;
break;
case SOUTHEAST: /* 东南↘,135° */
radian = PI / 2 + PI / 4;
break;
case SOUTH_SOUTHEAST: /* 东南偏南↘ */
radian = PI / 2 + PI / 4 + PI / 8;
break;
case SOUTH: /* 南↓,180° */
radian = PI;
break;
case SOUTH_SOUTHWEST: /* 西南偏南↙ */
radian = PI + PI / 8;
break;
case SOUTHWEST: /* 西南↙,225° */
radian = PI + PI / 4;
break;
case WEST_SOUTHWEST: /* 西南偏西↙ */
radian = PI + PI / 4 + PI / 8;
break;
case WEST: /* 西←,270° */
radian = PI + PI / 2;
break;
case WEST_NORTHWEST: /* 西北偏西↖ */
radian = PI + PI / 2 + PI / 8;
break;
case NORTHWEST: /* 西北↖,315度 */
radian = PI + PI / 2 + PI / 4;
break;
case NORTH_NORTHWEST: /* 西北偏北↖ */
radian = PI + PI / 2 + PI / 4 + PI / 8;
break;
default:
break;
}
return radian;
}
/**
* \brief 初始化时创建一些气泡
* \details 起始位置在最下面一排,起始方向不能太杂乱,需要在不同之中还有一些一致性,气泡不能重叠
* \param num 创建多少个气泡(或者是画的圆圈)
* \param speed 所有气泡的大致速度,气泡都会在这个速度上,上下减少或增加一点随机的速度
* \param direction 所有气泡的大致方向,气泡都会在这个主方向上,左右减少或增加一点随机的角度
*/
static int bubbles_create(int num, int speed, COMMON_DIRECTION direction)
{
if (num < 1)
g_bubbles_count = 1;
else if (num > BUBBLES_MAX_COUNT)
g_bubbles_count = BUBBLES_MAX_COUNT;
else
g_bubbles_count = num;
#ifdef COLLISION_ALGORITHM_TEST
g_bubbles_count = 2;
#endif
bubbles_ctx = (BUBBLE_CONTEXT_T *)malloc(g_bubbles_count * sizeof(BUBBLE_CONTEXT_T)); /* sizeof是关键字之一,不需要包含头文件 */
if (!bubbles_ctx) {
printf("malloc %d fail!\n", g_bubbles_count * sizeof(BUBBLE_CONTEXT_T));
g_bubbles_count = 0;
return -1;
}
int bubbles_interval = 0; /* 横向两个气泡的间隔宽度 */
if (g_bubbles_count > 1)
bubbles_interval = (g_window_x - 3 * default_radius) / (g_bubbles_count - 1);
/* 所有气泡的起始位置 */
for (int i = 0; i < g_bubbles_count; i++) {
bubbles_ctx[i].x = (float)default_radius + i * bubbles_interval + bubbles_interval / 2;
bubbles_ctx[i].y = (float)g_window_y - default_radius - bubbles_interval / 2;
bubbles_ctx[i].x_last = INT_MAX; /* 初始化时将上一帧的坐标设置为一个不可能的值 */
bubbles_ctx[i].y_last = INT_MAX;
bubbles_ctx[i].radius = default_radius + rand() % default_radius; /* 随机半径 */
bubbles_ctx[i].mass = (int)(4 * PI * bubbles_ctx[i].radius * bubbles_ctx[i].radius);
/* 随机速度 */
//bubbles_ctx[i].speed = g_window_y / 5 + g_window_y / 5 * (rand() % 101) / 100; /* 速度为1/5 ~ 2/5屏高之间的随机值 */
bubbles_ctx[i].speed = g_window_y / 15 + g_window_y / 10 * (rand() % 101) / 100; /* 速度为1/5 ~ 2/5屏高之间的随机值 */
/* 随机方向 */
//double radian = direction_to_radian(direction) + PI / 4 * (rand() % 11) / 10; /* 当前角度往右偏移随机的值,最多偏移45°,单位是弧度 */
double radian = direction_to_radian(direction) + PI / 2 * (rand() % 11) / 10; /* 当前角度往右偏移随机的值,最多偏移45°,单位是弧度 */
if (radian > 2 * PI)
radian -= 2 * PI; /* 超过360°后直接从0°开始算 */
bubbles_ctx[i].speed_direction = radian;
/* 测试碰撞算法时,只创建两个小球,指定方向和速度 */
#ifdef COLLISION_ALGORITHM_TEST
if (i == 0) {
/* 第一个小圆 */
bubbles_ctx[i].radius = default_radius * 10; /* 半径:1/10屏幕宽度 */
bubbles_ctx[i].speed = g_window_y / 5; /* 速度 */
} else if (i == 1) {
/* 第二个大圆 */
bubbles_ctx[i].radius = default_radius * 20; /* 半径:1/5屏幕宽度 */
bubbles_ctx[i].speed = g_window_y / 5; /* 速度 */
}
/* 要保证两个小球能碰在一起,测什么角度就手动改什么值 */
#if 1
/* 第一个小球从左往右走,第二个大球从右往左走,多角度碰撞 */
if (i == 0) {
bubbles_ctx[i].speed_direction = direction_to_radian(EAST); /* 初始方向 */
bubbles_ctx[i].x = (float)default_radius * 30; /* 初始位置:第2个大圆的位置 */
bubbles_ctx[i].y = (float)g_window_y / 2 + 30 * default_radius;
//bubbles_ctx[i].y = (float)g_window_y / 2 + default_radius * 40;
//bubbles_ctx[i].y = (float)g_window_y / 2 + default_radius * 30;
}
else if (i == 1) {
bubbles_ctx[i].speed_direction = direction_to_radian(WEST); /* 初始方向 */
bubbles_ctx[i].x = (float)g_window_x / 2 + default_radius * 30; /* 初始位置:第1个小圆的位置 */
bubbles_ctx[i].y = (float)g_window_y - default_radius * 30;
}
#endif
#if 0
/* 第一个小球从右往左走,第二个大球从左往右走,多角度碰撞 */
if (i == 0) {
bubbles_ctx[i].speed_direction = direction_to_radian(WEST); /* 初始方向 */
bubbles_ctx[i].x = (float)g_window_x / 2 + default_radius * 30; /* 初始位置:第1个小圆的位置 */
bubbles_ctx[i].y = (float)g_window_y - default_radius * 30;
}
else if (i == 1) {
bubbles_ctx[i].speed_direction = direction_to_radian(EAST); /* 初始方向 */
bubbles_ctx[i].x = (float)default_radius * 30; /* 初始位置:第2个大圆的位置 */
bubbles_ctx[i].y = (float)g_window_y / 2 + 30 * default_radius;
//bubbles_ctx[i].y = (float)g_window_y / 2 + default_radius * 40;
}
#endif
#if 0
/* 第一个小球从右往左走,第二个大球从上往下走,多角度碰撞 */
if (i == 0) {
bubbles_ctx[i].speed_direction = direction_to_radian(WEST); /* 初始方向 */
bubbles_ctx[i].x = (float)g_window_x / 2; /* 初始位置:第1个小圆的位置 */
bubbles_ctx[i].y = (float)g_window_y - default_radius * 30;
}
else if (i == 1) {
bubbles_ctx[i].speed_direction = direction_to_radian(SOUTH); /* 初始方向 */
bubbles_ctx[i].x = (float)default_radius * 30; /* 初始位置:第2个大圆的位置 */
//bubbles_ctx[i].y = (float)g_window_y / 2;
bubbles_ctx[i].y = (float)g_window_y / 2 + default_radius * 50;
}
#endif
#if 0
/* 第一个小球从下往上走,第二个大球从左往右走,多角度碰撞 */
if (i == 0) {
bubbles_ctx[i].speed_direction = direction_to_radian(NORTH); /* 初始方向 */
bubbles_ctx[i].x = (float)g_window_x / 2; /* 初始位置:第1个小圆的位置 */
bubbles_ctx[i].y = (float)g_window_y - default_radius * 30;
} else if (i == 1) {
bubbles_ctx[i].speed_direction = direction_to_radian(EAST); /* 初始方向 */
//!!!有bug
//bubbles_ctx[i].x = (float)default_radius * 30 + 40 * default_radius; /* 初始位置:第2个大圆的位置 */
bubbles_ctx[i].x = (float)default_radius * 30; /* 初始位置:第2个大圆的位置 */
bubbles_ctx[i].y = (float)g_window_y / 2;
}
#endif
#endif /* COLLISION_ALGORITHM_TEST */
//TODO: 速度功能和加速度逐渐衰减的功能暂不添加
bubbles_ctx[i].acceleration = 0;
bubbles_ctx[i].acceleration_direction = 0.0;
/* 初始化时要确保每个气泡都不重叠,保证气泡没发生碰撞 */
bubbles_ctx[i].collision_flag = FALSE;
bubbles_ctx[i].collision_target = 0;
bubbles_ctx[i].time_left = -1; //TODO: 当前让气泡永久存在,后续可以存在一段时间后控制其消失
bubbles_ctx[i].opacity = 0; //TODO: 当前先全部不透明
}
return 0;
}
/**
* \brief 在窗口中刷新一帧内容
* \details 这个函数每次进入的间隔时间和帧率有关,60帧时是16.7ms,30帧时是33.3ms
*/
static int window_update(HDC hdc)
{
static WCHAR text_info[64]; /* 屏幕上显示文字时使用 */
BOOL ret; /* 用于拷机时画面无显示时调试用 */
/* 1. 帧数文字信息显示 */
wsprintf((LPWSTR)text_info, (LPCWSTR)L"当前帧:%d", g_fno++);
ret = TextOut(hdc, 10, 10, (LPCWSTR)text_info, wcslen(text_info));
if (ret != 1)
printf("error");
/* 2. 刷新所有气泡,更新所有气泡的下次位置 */
for (int i = 0; i < g_bubbles_count; i++) {
/* 2.1 清除上一帧的所有圆 */
if (bubbles_ctx[i].x_last != INT_MAX && bubbles_ctx[i].y_last != INT_MAX) {
HPEN hOldPen1 = (HPEN)::SelectObject(hdc, hPenWhite);
Ellipse(hdc, bubbles_ctx[i].x_last - bubbles_ctx[i].radius, bubbles_ctx[i].y_last - bubbles_ctx[i].radius,
bubbles_ctx[i].x_last + bubbles_ctx[i].radius, bubbles_ctx[i].y_last + bubbles_ctx[i].radius);
}
bubbles_ctx[i].x_last = (int)bubbles_ctx[i].x;
bubbles_ctx[i].y_last = (int)bubbles_ctx[i].y;
/* 2.2 画出当前帧所有的圆 */
/* 左上角的坐标是0,0,右下角的坐标是xmax,ymax;横轴向右是增加,纵轴向上是减少 */
HPEN hOldPen2 = (HPEN)::SelectObject(hdc, hPenRed);
Ellipse(hdc, (int)bubbles_ctx[i].x - bubbles_ctx[i].radius, (int)bubbles_ctx[i].y - bubbles_ctx[i].radius,
(int)bubbles_ctx[i].x + bubbles_ctx[i].radius, (int)bubbles_ctx[i].y + bubbles_ctx[i].radius);
/* 2.3 计算下一帧所有圆的位置 */
/* 随机方向的运动,通过方向和速度算出下一个横坐标和纵坐标的位置 */
/* 2.3.1 气泡在两帧间的运动 */
float hypotenuse = (float)bubbles_ctx[i].speed * ms_per_frame / 1000; /* 两帧间气泡实际运动的距离,不带方向,也就是xy坐标对应斜边的像素个数 */
double direct_radian = bubbles_ctx[i].speed_direction; /* 运动方向的弧度值0~2PiΠ */
/* sin(direct_radian) = leg_x / hypotenuse,sin()的值=对边长度(x)/斜边长度;
* cos(direct_radian) = leg_y / hypotenuse,cos()的值=临边长度(y)/斜边长度;
* sin(direct_radian)值的符号代表了x相对值的符号(正负数);
* cos(direct_radian)值的符号代表了y相对值的符号;*/
float leg_x = (float)sin(direct_radian) * hypotenuse; /* x方向的直角边,带正负,也就是横向移动的距离和方向 */
float leg_y = (float)cos(direct_radian) * hypotenuse; /* y方向的直角边,带正负,也就是纵向移动的距离和方向 */
bubbles_ctx[i].x += leg_x;
bubbles_ctx[i].y -= leg_y; /* 屏幕竖向像素的减才是向上运动,加是向下运动 */
/* 2.3.2 边界碰撞检测 */
/* 刚好碰到线不算碰,超过线才算碰 */
/* 如果坐标变成负,说明碰到屏幕左边界或上边界,直接在右边界和下边界出现 */
if (bubbles_ctx[i].x < bubbles_ctx[i].radius) {
bubbles_ctx[i].x = (float)g_window_x - bubbles_ctx[i].radius;
//bubbles_ctx[i].y = (float)g_window_y - bubbles_ctx[i].y; /* 如果想在在对角出现,则打开这个注释 */
}
if (bubbles_ctx[i].y < bubbles_ctx[i].radius) {
bubbles_ctx[i].y = (float)g_window_y - bubbles_ctx[i].radius;
//bubbles_ctx[i].x = (float)g_window_x - bubbles_ctx[i].x;
}
/* 如果坐标超过屏幕宽高,说明碰到屏幕下边界或右边界,直接从上边界或左边界出现 */
if (bubbles_ctx[i].x > g_window_x - bubbles_ctx[i].radius) {
bubbles_ctx[i].x = (float)bubbles_ctx[i].radius;
//bubbles_ctx[i].y = (float)g_window_y - bubbles_ctx[i].y;
}
if (bubbles_ctx[i].y > g_window_y - bubbles_ctx[i].radius) {
bubbles_ctx[i].y = (float)bubbles_ctx[i].radius;
//bubbles_ctx[i].x = (float)g_window_x - bubbles_ctx[i].x;
}
}
#if 1 /* 如果开启碰撞 */
/* 3 气泡间碰撞检测,和碰撞后的位置计算 */
for (int i = 0; i < g_bubbles_count; i++) {
if (i < g_bubbles_count - 1) { /* 存在2个及以上个气泡时才需要判断碰撞 */
/* 所有气泡两两之间都要判断一次 */
for (int j = i + 1; j < g_bubbles_count; j++) {
/* 以j球为碰撞方向矢量的坐标原点 */
float x1 = (float)bubbles_ctx[i].x - (float)bubbles_ctx[j].x; /* 带符号 */
float y1 = (float)bubbles_ctx[i].y - (float)bubbles_ctx[j].y;
y1 = -y1; /* 因为纵坐标向下递增,所以算角度时要将y坐标反向 */
float z1 = (float)sqrt((double)x1 * x1 + (double)y1 * y1); /* 两球球心之间的距离 */
if (z1 >= bubbles_ctx[i].radius + bubbles_ctx[j].radius) {
/* 如果未碰撞 */
if (bubbles_ctx[i].collision_flag && bubbles_ctx[i].collision_target == j) {
/* 如果这两个球之前存在碰撞状态,则一起清除状态,让其靠自己的速度摆脱碰撞状态,
因为碰撞检测是按固定顺序判断的,那么碰撞记录只保存在低序号的小球context中 */
bubbles_ctx[i].collision_flag = FALSE; /* 已解除碰撞 */
bubbles_ctx[i].collision_target = 0;
}
} else {
/* 如果两球圆心之间的距离小于两球的半径之和,则一定发生了碰撞;
注意,如果帧率过快或者小球速度过快,则当前帧可能两球已经重叠了
很大一部分,甚至已经重合,要处理好这种情况 */
if (bubbles_ctx[i].collision_flag && bubbles_ctx[i].collision_target == j) {
/* 防止碰撞粘连,已经处于碰撞中时,不多次连续改变方向和速度,直接退出 */
/* 这种情况下多个小球同时碰撞会丢失之前的标志,先容忍这种情况 */
break;
}
bubbles_ctx[i].collision_flag = TRUE; /* 当前正处于碰撞中 */
bubbles_ctx[i].collision_target = j;
/* 3.1 碰撞处理,改变速度和方向 */
#if 1 /* 如果是同质量小球任意角度碰撞 */
/* A) 场景一:同质量小球任意角度碰撞;
将所有气泡质量都看成是相同的,根据动量守恒和能量守恒定律进行推导,
简化的速度为:
v1' = v2;
v2' = v1;
两个质量相同的小球完全刚性碰撞后交换速度大小和方向(仅指沿碰撞方向的速度矢量)
需要通过碰撞点的切线进行计算,只有切线垂直方向的速度反向了,与切线平行的速度还是不变;
其它的碰撞算法还有:
[C++多小球非对心弹性碰撞(HGE引擎)](https://blog.csdn.net/y85171642/article/details/25008091)
[canvas 踩坑 * 小球弹性碰撞逻辑解析](https://blog.csdn.net/qq_43420617/article/details/104524232)
*/
/* (a). 算出碰撞方向 */
/* 不使用矢量计算,直接使用坐标旋转,以j球圆心为碰撞方向坐标系的原点 */
float collision_direct;
if (z1 < 0.1) {
/* 当前帧可能两个气泡大部分都重叠在一起,甚至两个圆心距离为0,要处理这种额外情况 */
int x2 = bubbles_ctx[i].x_last - bubbles_ctx[j].x_last;
int y2 = bubbles_ctx[i].y_last - bubbles_ctx[j].y_last;
y2 = -y2; /* y坐标向下才是递增 */
int z2 = (int)sqrt((double)x2 * x2 + (double)y2 * y2); /* 两球球心之间的距离 */
if (z2 < 2) {
collision_direct = 0; /* 如果上一帧这两个球也是大部分重叠的,则将碰撞角度设为正北,因为值太小时角度算不准 */
} else {
collision_direct = (float)acos(y2 / z2);
float temp_direct = (float)asin(x2 / z2);
if (temp_direct > 0.0)
collision_direct = 2 * PI - collision_direct;
}
} else {
/* 如果单独用asin或acos来计算,无法完整区分4个象限,
需要组合判断asin和acos的值才能得到唯一的角度,并且x、y值是要带正负符号的;
[0到360度三角函数值表特殊角的三角函数值表](https://wenku.baidu.com/view/bdcf677dc9aedd3383c4bb4cf7ec4afe04a1b138.html)
这里的话,起始碰撞角度算不算无所谓,从两个角度看都可以 */
collision_direct = (float)acos(y1 / z1);
float temp_direct = (float)asin(x1 / z1);
if (temp_direct < 0.0) {
collision_direct = 2 * PI - collision_direct;
//collision_direct += PI; /* 如果想要实现一半的碰撞正常,另一半的碰撞通过相互粘连再将对方甩出去的效果,则用这行替换上一行 */
}
}
/* (b). 将碰撞的两个小球xy位置坐标轴旋转到以碰撞方向作为y轴的坐标轴上 */
/* 将小球原来的速度坐标轴偏转为以碰撞的那条线作为y轴 */
float re_direct1 = (float)bubbles_ctx[i].speed_direction - collision_direct; /* 偏转以后运动方向的弧度值,范围0~2PiΠ */
float re_direct2 = (float)bubbles_ctx[j].speed_direction - collision_direct;
if (re_direct1 < 0)
re_direct1 += 2 * (float)PI;
if (re_direct2 < 0)
re_direct2 += 2 * (float)PI;
/* (c). 将两个小球原来的速度矢量按新坐标轴拆分成x和y分量 */
/* 将小球速度大小进行拆分,这样就可以避免进行矢量计算 */
float hypotenuse1 = (float)bubbles_ctx[i].speed * ms_per_frame / 1000; /* 一帧间气泡实际运动的距离,不带方向,单位为像素;也就是xy坐标对应的斜边长 */
float leg_x1 = (float)sin(re_direct1) * hypotenuse1; /* 横向移动的距离和方向,按碰撞方向拆分x坐标,大小有正有负 */
float leg_y1 = (float)cos(re_direct1) * hypotenuse1; /* 纵向移动的距离和方向,这里向上为正,向下为负;按碰撞方向拆分y坐标 */
float hypotenuse2 = (float)bubbles_ctx[j].speed * ms_per_frame / 1000;
float leg_x2 = (float)sin(re_direct2) * hypotenuse2;
float leg_y2 = (float)cos(re_direct2) * hypotenuse2;
/* (d). 碰后速度计算:用于同质量小球的对心碰撞,或者斜碰时在碰撞方向上动量一样的情况 */
/* 和碰撞方向平行的分量,可以看成就是对心碰撞,所以纵向y坐标交换;垂直的速度分量可以看为未参与碰撞,保持不变,横向x不变 */
//TODO: 如果要考虑斜碰时碰撞方向的动量不一样的情况,则需使用“B) 场景二”中的公式;这里只直接将速度反向和交换
float tmp = leg_y1;
leg_y1 = leg_y2;
leg_y2 = tmp;
/* 重新计算方向和速度,重新修改两个小球的方向和速度,
因为碰撞后方向变了,角度斜边长hypotenuse1和hypotenuse2已经变了,要重新计算 */
hypotenuse1 = (float)sqrt((double)leg_x1 * leg_x1 + (double)leg_y1 * leg_y1);
hypotenuse2 = (float)sqrt((double)leg_x2 * leg_x2 + (double)leg_y2 * leg_y2);
/* 重新计算碰后方向 */
/* asin(0.5)结果只是30度,不是120度;asin(-0.5)结果只是-30度,不是210或者330度;
acos(0.5)结果只是60度,不是300度;acos(-0.5)结果只是120度,不是240度;
acos能得出0~180°的角,asin能得出-90~90°的角 */
bubbles_ctx[i].speed_direction = acos(leg_y1 / hypotenuse1);
float temp_direct = (float)asin(leg_x1 / hypotenuse1);
if (temp_direct < 0.0)
bubbles_ctx[i].speed_direction = 2 * PI - bubbles_ctx[i].speed_direction;
bubbles_ctx[j].speed_direction = acos(leg_y2 / hypotenuse2);
temp_direct = (float)asin(leg_x2 / hypotenuse2);
if (temp_direct < 0.0)
bubbles_ctx[j].speed_direction = 2 * PI - bubbles_ctx[j].speed_direction;
/* (e). 还原坐标轴后重新算出速度方向和速度大小 */
/* 重新计算碰后速度 */
bubbles_ctx[i].speed = (int)(hypotenuse1 * 1000 / ms_per_frame);
bubbles_ctx[j].speed = (int)(hypotenuse2 * 1000 / ms_per_frame);
if (bubbles_ctx[i].speed <= MIN_SPEED) /* 防止有小球始终静止;如果速度过小则给一个最小值 */
bubbles_ctx[i].speed = MIN_SPEED;
if (bubbles_ctx[j].speed <= MIN_SPEED)
bubbles_ctx[j].speed = MIN_SPEED;
/* (f). 将坐标轴还原到原来的坐标轴上 */
/* 将运动方向参考系从碰撞的那条线切换回原来的xy坐标轴 */
bubbles_ctx[i].speed_direction += collision_direct;
bubbles_ctx[j].speed_direction += collision_direct;
if (bubbles_ctx[i].speed_direction >= 2 * PI)
bubbles_ctx[i].speed_direction -= 2 * PI;
if (bubbles_ctx[j].speed_direction >= 2 * PI)
bubbles_ctx[j].speed_direction -= 2 * PI;
# if 1 /* 如果连续进行多次碰撞判断,防止碰撞后粘在一起 */
/*!!! 这个功能也可以不打开,前面已经用另一种方式实现过避免多帧重复计算碰撞 */
/* (g). 再判断一次碰撞,防止碰后两球运动速度不够,仍然有重合,导致碰撞检测陷入死循环 */
/* 如果仍然碰撞,让小球额外运动一帧距离;设个阈值,如果超过10帧仍然碰撞,直接加一个固定值 */
float x1, y1, x2, y2;
float leg1_x, leg1_y, leg2_x, leg2_y;
float hypotenuse;
double direct_radian;
hypotenuse = (float)bubbles_ctx[i].speed * ms_per_frame / 1000;
direct_radian = bubbles_ctx[i].speed_direction;
leg1_x = (float)sin(direct_radian) * hypotenuse; /* xy坐标每帧增量 */
leg1_y = (float)cos(direct_radian) * hypotenuse;
hypotenuse = (float)bubbles_ctx[j].speed * ms_per_frame / 1000;
direct_radian = bubbles_ctx[j].speed_direction;
leg2_x = (float)sin(direct_radian) * hypotenuse; /* xy坐标每帧增量 */
leg2_y = (float)cos(direct_radian) * hypotenuse;
x1 = bubbles_ctx[i].x;
y1 = bubbles_ctx[i].y;
x2 = bubbles_ctx[j].x;
y2 = bubbles_ctx[j].y;
#define RETRY_MAX_TIMES 10
int k;
for (k = 0; k < RETRY_MAX_TIMES; k++) {
float line1x, line1y, line1z;
/* 计算下一帧时的两个气泡的坐标 */
x1 += leg1_x;
y1 -= leg1_y;
x2 += leg2_x;
y2 -= leg2_y;
/* 碰撞检测 */
line1x = x1 - x2; /* 带符号 */
line1y = y1 - y2;
line1y = -line1y;
line1z = (float)sqrt((double)line1x * line1x + (double)line1y * line1y);
if (line1z < bubbles_ctx[i].radius + bubbles_ctx[j].radius) {
/* 如果仍然碰撞;也可能是有个球从一侧边界跃到另一侧边界刚好和一个球重叠了一部分 */
bubbles_ctx[i].x += leg1_x;
bubbles_ctx[i].y -= leg1_y;
bubbles_ctx[j].x += leg2_x;
bubbles_ctx[j].y -= leg2_y;
/* 边界碰撞检测 */
if (bubbles_ctx[i].x < bubbles_ctx[i].radius)
bubbles_ctx[i].x = (float)g_window_x - bubbles_ctx[i].radius;
if (bubbles_ctx[i].y < bubbles_ctx[i].radius)
bubbles_ctx[i].y = (float)g_window_y - bubbles_ctx[i].radius;
if (bubbles_ctx[i].x > g_window_x - bubbles_ctx[i].radius)
bubbles_ctx[i].x = (float)bubbles_ctx[i].radius;
if (bubbles_ctx[i].y > g_window_y - bubbles_ctx[i].radius)
bubbles_ctx[i].y = (float)bubbles_ctx[i].radius;
if (bubbles_ctx[j].x < bubbles_ctx[j].radius)
bubbles_ctx[j].x = (float)g_window_x - bubbles_ctx[j].radius;
if (bubbles_ctx[j].y < bubbles_ctx[j].radius)
bubbles_ctx[j].y = (float)g_window_y - bubbles_ctx[j].radius;
if (bubbles_ctx[j].x > g_window_x - bubbles_ctx[j].radius)
bubbles_ctx[j].x = (float)bubbles_ctx[j].radius;
if (bubbles_ctx[j].y > g_window_y - bubbles_ctx[j].radius)
bubbles_ctx[j].y = (float)bubbles_ctx[j].radius;
x1 = bubbles_ctx[i].x;
y1 = bubbles_ctx[i].y;
x2 = bubbles_ctx[j].x;
y2 = bubbles_ctx[j].y;
} else {
break;
}
}
if (RETRY_MAX_TIMES == k) {
/* 如果连续多次两个球都重叠,有可能它们重叠并同速,
将其中一个球的方向波动90度,并将速度加入一个随机值 */
bubbles_ctx[i].speed = bubbles_ctx[i].speed * (rand() % 10 + 5) / 10;
bubbles_ctx[i].speed_direction += PI / 2;
if (bubbles_ctx[i].speed_direction > 2 * PI)
bubbles_ctx[i].speed_direction -= 2 * PI;
}
# endif /* 如果连续进行多次碰撞判断,防止碰撞后粘在一起 */
#endif /* 如果是同质量小球任意角度碰撞 */
#if 0 /* 如果是不同质量气泡的完全弹性碰撞 */
/* B) 场景二:不同质量气泡的完全弹性碰撞(暂不实现):
根据动量守恒和能量守恒定律进行推导,将速度矢量拆分为横向和竖向单独计算,最后再将结果合起来
[对于一动碰一动碰后速度的快速计算](https://zhuanlan.zhihu.com/p/363174687)
与碰撞点切线垂直的方向上的速度变化:
v1' = ((m1 - m2) * v1 + 2 * m2 * v2) / (m1 + m2);
v2' = ((m2 - m1) * v2 + 2 * m1 * v1) / (m1 + m2);
与碰撞点切线平行的方向上速度不变 */
//TODO: 内容待完善
v1rx = ((m1 - m2) * v1 + 2 * m2 * v2) / (m1 + m2);
v2ry = ((m2 - m1) * v2 + 2 * m1 * v1) / (m1 + m2);
#endif /* 如果是不同质量气泡的完全弹性碰撞 */
#if 0 /* 如果是简单的将气泡运动方向都反向 */
/* C) 场景三:简单的将气泡运动方向都反向 */
bubbles_ctx[i].speed_direction += PI;
if (bubbles_ctx[i].speed_direction > 2 * PI)
bubbles_ctx[i].speed_direction -= 2 * PI;
bubbles_ctx[j].speed_direction += PI;
if (bubbles_ctx[j].speed_direction > 2 * PI)
bubbles_ctx[j].speed_direction -= 2 * PI;
#endif /* 如果是简单的将气泡运动方向都反向 */
}
}
}
}
#endif /* 如果开启碰撞 */
return 0;
}
/**
* \brief 执行窗口内容更新的线程
*/
DWORD WINAPI ThreadProcessFunc(LPVOID lpParamter)
{
HWND hWnd = (HWND)lpParamter;
HDC hdc; /* 窗口信息 */
while (!window_start_flag) /* 等待窗口创建完成,已获取到窗口宽高 */
Sleep(1); /* 延时1ms */
/* 窗口合法性判断 */
hdc = GetDC(hWnd);
if (IsIconic(hWnd))
return 0;
/* 画笔设置 */
hPenWhite = (HPEN)::CreatePen(PS_SOLID, 2, RGB(255, 255, 255)); /* 白色线 */
hPenRed = (HPEN)::CreatePen(PS_SOLID, 2, RGB(176, 48, 96)); /* 红色线 */
/* 1. 创建一批气泡 */
bubbles_create(20, 200, NORTH);
//bubbles_create(10, 200, SOUTH);
/* 死循环,持续运行 */
while (TRUE)
{
window_update(hdc);
//GdiFlush(); /* 及时将绘图区绘制的内容写入到窗口显存中去 */
Sleep(ms_per_frame - frame_delay_adjust); //单位是毫秒
}
return 0;
}
/**
* \brief 窗口消息处理回调函数
* \details 如响应窗口的大小变化
*/
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
RECT rect; /* 窗口大小 */
/* 1. 处理想要处理的窗口消息 */
switch (message)
{
case WM_CREATE: /* 窗口被创建时的消息 */
/* 使用定时器连续绘图的话,拷机会有bug,画面卡住,画面相关的操作函数返回错误,屏蔽这些代码 */
/* 创建一个定时器,用来刷新每一帧;实测定时器最短只能设置10ms触发一次,生效时间是15ms,所以理论上最高66帧 */
//if (SetTimer(hWnd, TIMER1, 10, NULL) == 0) /* 间隔10ms,1s 100点,理论上跑完一次要5s,实测7.8s,这已经是最短的间隔 */
//{
// MessageBox(hWnd, (LPCWSTR)L"定时器安装失败!", (LPCWSTR)L"03Timer", MB_OK);
//}
return 0;
case WM_SIZE: /* 适配窗口大小的改变 */
{
/* 程序刚运行,窗口刚打开的时候,会自动进入一次 */
GetClientRect(hWnd, &rect); /* 获取窗口的大小 */
g_window_x = rect.right - rect.left;
g_window_y = rect.bottom - rect.top;
default_radius = g_window_x / DEFAULT_RADIUS_RATIO;
window_start_flag = TRUE;
}
return 0;
case WM_TIMER: /* 定时器消息(中断)处理 */
{
/* 定时器功能已弃用 */
/* 定时器设置成1ms时,按道理说,一次500个坐标,1ms一个坐标,半秒就应该跑一遍,但实测有7.8s,说明定时器最短也要15ms触发一次 */
/* 当前bug:单独显示文字或者单独显示圆圈的话,连续显示几轮后停住了,返回值是0 */
}
return 0;
case WM_PAINT: /* 最开始绘制窗口中默认显示的内容 */
/* 打点画线的功能当前不用 */
//PAINTSTRUCT ps; /* 绘图板 */
//hdc = BeginPaint(hWnd, &ps); /* 对窗口进行绘图的准备 */
//Polyline(hdc, pt, SEGMENTS); /* 批量打点,pt里面存放要画的一系列点 */
//EndPaint(hWnd,&ps); /* 和BeginPaint配套使用 */
break;
case WM_CLOSE: /* 程序退出,先close再destroy */
{
//KillTimer(hWnd, TIMER1); /* 退出前销毁定时器资源 */
TerminateThread(hThread, 0); /* 强制退出绘图线程 */
DestroyWindow(hWnd);
}
return 0;
case WM_DESTROY: /* 程序退出 */
PostQuitMessage(0); /* 该函数向消息队列中插入一条WM_QUIT消息,由GetMessage函数捕获返回0而退出程序 */
break;
}
/* 为应用程序没有处理的任何窗口消息提供缺省的处理,该函数确保每一个消息都得到处理 */
return DefWindowProc(hWnd, message, wParam, lParam);
}
/********************************** 接口函数 **********************************/
/**
* \brief Windows图形界面程序固定的入口函数
* \details 如果是命令行函数,则入口函数不一样
* \remarks
* 1、注册窗口类 (RegisterClassEx)
* 2、创建窗口 (CreateWindowsEx)
* 3、在桌面显示窗口 (ShowWindows)
* 4、更新窗口客户区 (UpdataWindows)
* 5、进入无限循环的消息获取和处理的循环:
* GetMessage,获取消息
* TranslateMessage,转换键盘消息
* DispatchMessage,将消息发送到相应的窗口函数
*/
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
/*TODO: 增加创建窗口时指定宽高*/
/* 1. 设置窗口属性和注册窗口 */
WNDCLASSEX wcex; /* 定义窗口属性结构体 */
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW; /* 允许窗口缩放 */
wcex.lpfnWndProc = (WNDPROC)WndProc; /* 窗口消息处理回调函数 */
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = NULL; /* 窗口左上角图标的句柄 */
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = (LPCWSTR)titleName; /* 类名称 */
wcex.hIconSm = NULL; /* 小图标句柄 */
RegisterClassEx(&wcex);
/* 2. 创建窗口 */
HWND hWnd;
#ifdef COLLISION_ALGORITHM_TEST
hWnd = CreateWindow((LPCWSTR)titleName, (LPCWSTR)titleName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, 400, 400, NULL, NULL, hInstance, NULL); /* 窗口指定宽高 */
#else
/* 当前我的电脑上默认初始窗口大小 1424 * 720,测试碰撞时基于此设置初始位置和速度 */
hWnd = CreateWindow((LPCWSTR)titleName, (LPCWSTR)titleName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); /* 窗口使用默认宽高 */
#endif
if (!hWnd)
return FALSE; /* 如果创建窗口失败则返回 */
/* 3. 显示窗口和刷新窗口 */
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
/* 4. 添加线程,通过hWnd持续绘制显示区域 */
/* CreateThread()函数参数介绍:
* LPSECURITY_ATTRIBUTESlpThreadAttributes, 表示线程内核对象的安全属性,一般传入NULL表示使用默认设置
* DWORDdwStackSize, 表示线程栈空间大小,传入0表示使用默认大小(1MB)
* LPTHREAD_START_ROUTINElpStartAddress, 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址
* LPVOIDlpParameter, 是传给线程函数的参数
* DWORDdwCreationFlags,指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()
* LPDWORDlpThreadId 将返回线程的ID号,传入NULL表示不需要返回该线程ID号 */
hThread = CreateThread(NULL, 0, ThreadProcessFunc, hWnd, 0, NULL);
/* 5. 循环获取和处理窗口上的消息 */
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(&msg); /* 转换键盘等产生的消息 */
DispatchMessage(&msg); /* 将消息发送到窗口函数 */
}
return (int)msg.wParam;
}
/*********************************** 文件尾 ***********************************/
四、一些细节
1)C语言通过asin、acos反正弦反余弦算出方向角度,矢量、0~360°、上下左右、东南西北、方位
/* 如果单独用asin或acos来计算,无法完整区分4个象限,
需要组合判断asin和acos的值才能得到唯一的角度,并且x、y值是要带正负符号的;
[0到360度三角函数值表特殊角的三角函数值表](https://wenku.baidu.com/view/bdcf677dc9aedd3383c4bb4cf7ec4afe04a1b138.html)
这里的话,起始碰撞角度算不算无所谓,从两个角度看都可以 */
collision_direct = (float)acos(y1 / z1);
float temp_direct = (float)asin(x1 / z1);
if (temp_direct < 0.0) {
collision_direct = 2 * PI - collision_direct;
}
/* collision_direct是算出的0~2PI(0~360°)实际角度或方向 */