灵感来源:【《赛 博 沙 漏》】 https://www.bilibili.com/video/BV1yj411o7Hf/?share_source=copy_web&vd_source=a9d52604c74e8a682e8f5b4ef43d9198
代码基于STM32实现,不过硬件部分只涉及重力环境的获取以及绘制,因此移植到其它平台是十分简单的,只需要重写update_angle()和两种绘制函数即可。
这种东西在GitHub上也有源码,但我在看了“硬禾学堂”的相关源码后彻底放弃了理解作者思路的尝试,或许自己从头想还会更简单一点……吧?
不过看这些源码也不能是毫无收获。至少我知道了当同时出现两种可行的路线时,所有人都不约而同地用随机数代替复杂的重力判断。
sand.h
#ifndef SAND_H
#define SAND_H
#include "./BSP/MPU6050/MPU6050.h"
#include "math.h"
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include "./BSP/LCD/lcd.h"
#include "./SYSTEM/delay/delay.h"
/* 宏定义 */
#define SAND_NUM 520 //沙子的数量
#define BOX_LEN 30 //沙漏每部分的长度有几个单位,不包括边缘,详情见init_block()
#define SAND_LEN 5 //沙子的长度
/* 结构体 */
typedef struct{
double cosx;
double cosy;
double cosz;
}BoxAngel;//姿态角
typedef struct{
int flag;
int snum;
int broad[BOX_LEN+2][BOX_LEN+2];
}SandBlock;//沙漏块
/* 全局变量 */
extern BoxAngel boxangel;
extern SandBlock block1,block2;
extern int sand_clock_time;
extern int forward; /* 前进方向 */
extern int model; /*模式选择,是MPU6050读取0还是摇杆读取1*/
/* 摇杆 */
extern uint16_t PS2_xy_value[2]; /* 0x 1y */
extern uint8_t PS2_z; /* z轴是否被按下 */
extern float PS2_x_f,PS2_y_f; /* xy的相对值,取值0~1 */
/* 函数声明 */
void update_angle(void);
void init_block(void);
void update_local(SandBlock *block);
void draw_sandblock(int zx,int zy,SandBlock *block);
void draw_sandblock2(int zx,int zy,SandBlock *block);
void draw_sand(int zx,int zy,uint16_t color);
void sand_clock(void);
#endif
sand.c
#include "./BSP/Sandbox/sand.h"
/*
芯片方向:
X<----------
| SDA
| SCL
| GND
| 5V
V Y
但是我们可以对x轴取反,这样就变成了正常的坐标系:
屏幕坐标系:
------------->X
|
|
|
|
|
V Y
经过测量,各个典型角度的读数如下所示(方位余弦值):
正面朝上平放:
x=0.04
y=-0.01
z=-1.00
正面朝前竖直放置:
x=0.04
y=-1.00
z=-0.03
正面朝后倒立放置:
y=1.00
正面向右侧翻,侧立:
x=1.00
y=-0.03
z=0.05
正面向左侧立:
x=-1.00
正面超前竖立,然后右倾45°左右:
x=0.71
y=-0.70
z=0.08
左倾45°:
x=-0.71
右倾135°,即旋转180°倒立,在左转45°:
x=0.73
y=o.68
左倾135°:
x=-0.76
*/
BoxAngel boxangel;
SandBlock block1,block2;
int sand_clock_time=SAND_NUM;
int forward=2; /* 前进方向 */
/* 摇杆 */
uint16_t PS2_xy_value[2]; /* 0x 1y */
uint8_t PS2_z = 0; /* z轴是否被按下 */
float PS2_x_f = 0,PS2_y_f = 0; /* xy的相对值,取值0~1 */
int model=0;
int sandclock_Random(int n)
{
srand(SysTick->VAL+n); /* 半主机模式下使用time函数会报错,在这里用系统定时器的值替代 */
return rand() % 100;
}
/*
通过读取加速度来计算姿态角。
至于陀螺仪谁爱写谁写。
*/
void update_angle(void)
{
if(model){
boxangel.cosx=PS2_x_f*2;
boxangel.cosy=-PS2_y_f*2;
}
else{
int16_t ax,ay,az, GX, GY, GZ; //定义用于存放各个数据的变量,A是加速度,G是陀螺仪
MPU6050_GetData(&ax, &ay, &az, &GX, &GY, &GZ); //获取MPU6050的数据
double A =sqrt(ax*ax + ay*ay + az*az);
boxangel.cosx=ax/A;
boxangel.cosy=ay/A;
boxangel.cosz=az/A;//计算方向余弦
}
}
/*
初始化沙漏块
*/
void init_block(void){
block1.flag=1;
block2.flag=2;
block1.snum=SAND_NUM;
block2.snum=0;
for(int i=1;i<=BOX_LEN;i++){
for(int j=1;j<=BOX_LEN;j++){
block1.broad[i][j]=0;
block2.broad[i][j]=0;
}
}
for(int i=0;i<=BOX_LEN+1;i++){//将边缘处全部赋值为-1
block1.broad[i][0]=-1;
block1.broad[0][i]=-1;
block1.broad[i][BOX_LEN+1]=-1;
block1.broad[BOX_LEN+1][i]=-1;
block2.broad[i][0]=-1;
block2.broad[0][i]=-1;
block2.broad[i][BOX_LEN+1]=-1;
block2.broad[BOX_LEN+1][i]=-1;
}
int snum=0;
for(int i=1;i<=BOX_LEN;i++){//放置沙子
if(snum == SAND_NUM)break;
for(int j=1;j<=BOX_LEN;j++){
if(snum == SAND_NUM)break;
block1.broad[i][j]=1;
snum++;
}
}
}
/*
更新位置,如果某个沙子的重力方向上没有其它沙子,也不处于底边,则移动。
由于我们把边缘处全部赋值为1了,所以不需要判断哪边是底边,只要判断有没有其它沙子就行了
重力方向:当某个方向与坐标轴的夹角小于60度则将该方向视为重力方向之一。
位移判断有两次:第一次是常规判断;
第二次在第一次判定没有产生位移的情况下产生,并使用更小的重力阈值;
且第二次判断不会产生斜向位移并用随机减小位移概率。
这是为了更好的模拟出倒塌的物理效果而不会出现过于整齐的堆叠
*/
void update_local(SandBlock *block){
int x = 0;
int y = 0;
//int z = 0;//重力方向,1表示是正方向,-1表示反方向;当该方向无重力时为0 。
int s_i=BOX_LEN,s_j=BOX_LEN;//遍历起点,默认右下角开始
update_angle();//获取重力方向
if(boxangel.cosx>0.5){ //重力向右
x=1;
s_i=BOX_LEN-1;
}
if(boxangel.cosx<-0.5){ //重力向左
x=-1;
s_i=1;
}
if(boxangel.cosy<-0.5){ //重力向下
y=1;
s_j=BOX_LEN-1;
}
if(boxangel.cosy>0.5){ //重力向上
y=-1;
s_j=1;
}//判断重力方向并设置循环起点
for(int i=s_i;i>0&&i<=BOX_LEN;i-=(x?x:1)){//当该轴有重力作用时采用对应的遍历方向,否则使用1
for(int j=s_j;j>0&&j<=BOX_LEN;j-=(y?y:1)){
if(block->broad[i][j]){
int ni=i,nj=j;
if(!block->broad[i+x][j+y]){
ni=i+x,nj=j+y;
}else if(sandclock_Random(i+j)%2==0){ //随机判断先向左还是先向右
if(!block->broad[i+x][j]){
ni=i+x,nj=j;
}else if(!block->broad[i][j+y]){
ni=i,nj=j+y;
}
}else{
if(!block->broad[i][j+y]){
ni=i,nj=j+y;
}else if(!block->broad[i+x][j]){
ni=i+x,nj=j;
}
}
if(ni==i&&nj==j){
/*如果经过上面的判定没有任何移动则进入第二次判断
这次判断是为了更好的模拟物理效果
*/
int nx=x,ny=y; //判断新的重力方向
if(boxangel.cosx>0.2){ //重力向右
nx=1;
}
if(boxangel.cosx<-0.2){ //重力向左
nx=-1;
}
if(boxangel.cosy<-0.2){ //重力向下
ny=1;
}
if(boxangel.cosy>0.2){ //重力向上
ny=-1;
}
if(!block->broad[i+nx][j]&&block->broad[i-nx][j]){//如果重力方向上没有物体且反方向有
ni=i+nx,nj=j;
}else if(!block->broad[i][j+ny]&&block->broad[i][j-ny]){
ni=i,nj=j+ny;
}
if(sandclock_Random(i+j)%10>6){//随机决定到底要不要移动
ni=i,nj=j;
}
}
block->broad[i][j]=0;
block->broad[ni][nj]=1;//这两个赋值不能颠倒顺序
}
}
}
}
/*
绘制方块
zx,zy:方块左上角坐标
*/
void draw_sand(int zx,int zy,uint16_t color){
//delay_us(10);
//printf("drawsand:%d,%d\r\n",zx,zy);
lcd_fill(zx,zy,zx+SAND_LEN,zy+SAND_LEN,color);
}
/*
绘制沙漏块
zx,zy:左上角的坐标
block:要绘制的块
*/
void draw_sandblock(int zx,int zy,SandBlock *block){
draw_sand(300,400,WHITE);
//printf("drawblock\n");
int length = BOX_LEN*SAND_LEN; //边框长度
// lcd_fill(zx,zy,zx+length,zy+length,BLACK);//清空绘画区域
lcd_draw_line(zx-1,zy-1,zx+length+1,zy-1,g_point_color);
lcd_draw_line(zx-1,zy-1,zx-1,zy+length+1,g_point_color);
lcd_draw_line(zx+length+1,zy-1,zx+length+1,zy+length+1,g_point_color);
lcd_draw_line(zx-1,zy+length+1,zx+length+1,zy+length+1,g_point_color);//绘制边框
for(int i=1;i<=BOX_LEN;i++){
for(int j=1;j<=BOX_LEN;j++){
if(block->broad[i][j]==1){
if(i==BOX_LEN&&j==BOX_LEN){
draw_sand(zx+(i-1)*SAND_LEN,zy+(j-1)*SAND_LEN,RED);
}else{
draw_sand(zx+(i-1)*SAND_LEN,zy+(j-1)*SAND_LEN,WHITE);
}
}
else{
draw_sand(zx+(i-1)*SAND_LEN,zy+(j-1)*SAND_LEN,BLACK);
}
}
}
}
/*
沙子流逝。
会根据重力判断从哪个流向哪个
*/
void sand_clock(void){
update_angle();
if((boxangel.cosx>0.5||boxangel.cosy<-0.5)&&(boxangel.cosx>-0.5&&boxangel.cosy<0.5)){
//从一到二
if(block1.broad[BOX_LEN][BOX_LEN]==1){
block1.snum--;
block2.snum++;
block1.broad[BOX_LEN][BOX_LEN]=0;
block2.broad[1][1]=1;
}
sand_clock_time=block1.snum;
}
if((boxangel.cosx<-0.5||boxangel.cosy>0.5)&&(boxangel.cosx<0.5&&boxangel.cosy>-0.5)){
//从二到一
if(block2.broad[1][1]==1){
block1.snum++;
block2.snum--;
block2.broad[1][1]=0;
block1.broad[BOX_LEN][BOX_LEN]=1;
}
sand_clock_time=block2.snum;
}
}
下面我们来看看具体的各个函数:
宏定义
/* 宏定义 */
#define SAND_NUM 520 //沙子的数量
#define BOX_LEN 30 //沙漏每部分的长度有几个单位,不包括边缘,详情见init_block()
#define SAND_LEN 5 //沙子的长度可能会觉得疑惑的应该只有BOX_LEN了:这里需要提一嘴我们是如何判断数组边缘的:如果你想要创建一个30*30的数组,那么程序实际上创建的是32*32的数组。多余的部分将在初始化函数里被赋值为-1作为边缘(或是别的什么也行,只要非0就行)。
结构体
/* 结构体 */
typedef struct{
double cosx;
double cosy;
double cosz;
}BoxAngel;//姿态角
typedef struct{
int flag;
int snum;
int broad[BOX_LEN+2][BOX_LEN+2];
}SandBlock;//沙漏块首先是姿态角1:
三个COS对应重力单位向量三轴方向的余弦值,即数学上的“方向余弦”。
然后是沙漏块:沙漏通常由两部分组成,因此我们也需要两个数组来存储它。flag成员指明了该实例对应的是上面那个块(1)还是下面那个块(2);snum统计了当前块里有多少里沙子;broad数组里存储了每个沙子的位置。
全局变量
/* 全局变量 */
extern BoxAngel boxangel;
extern SandBlock block1,block2;
extern int sand_clock_time;boxangel:设备当前的姿态角;
blockx:沙漏上下两个块;
sand_clock_time:沙漏剩余时间,即当前处于上方的块里剩余的沙子数量;根据重力方向而自动改变。
函数
获取重力方向、在屏幕上绘制图形属于硬件依赖,在此不过多赘述。感兴趣者可以查看正点原子和江协科大的视频。
init_block()
/*
初始化沙漏块
*/
void init_block(void){
block1.flag=1;
block2.flag=2;
block1.snum=SAND_NUM;
block2.snum=0;
for(int i=1;i<=BOX_LEN;i++){
for(int j=1;j<=BOX_LEN;j++){
block1.broad[i][j]=0;
block2.broad[i][j]=0;
}
}
for(int i=0;i<=BOX_LEN+1;i++){//将边缘处全部赋值为-1
block1.broad[i][0]=-1;
block1.broad[0][i]=-1;
block1.broad[i][BOX_LEN+1]=-1;
block1.broad[BOX_LEN+1][i]=-1;
block2.broad[i][0]=-1;
block2.broad[0][i]=-1;
block2.broad[i][BOX_LEN+1]=-1;
block2.broad[BOX_LEN+1][i]=-1;
}
int snum=0;
for(int i=1;i<=BOX_LEN;i++){//放置沙子
if(snum == SAND_NUM)break;
for(int j=1;j<=BOX_LEN;j++){
if(snum == SAND_NUM)break;
block1.broad[i][j]=1;
snum++;
}
}
}
沙漏初始函数。
首先程序将两个沙漏块的有效区域全部清零;接着将边缘处的部分全部赋值为-1;最后在块1里放置沙子。
update_local(SandBlock *block);
/*
更新位置,如果某个沙子的重力方向上没有其它沙子,也不处于底边,则移动。
由于我们把边缘处全部赋值为1了,所以不需要判断哪边是底边,只要判断有没有其它沙子就行了
重力方向:当某个方向与坐标轴的夹角小于60度则将该方向视为重力方向之一。
位移判断有两次:第一次是常规判断;
第二次在第一次判定没有产生位移的情况下产生,并使用更小的重力阈值;
且第二次判断不会产生斜向位移并用随机减小位移概率。
这是为了更好的模拟出倒塌的物理效果而不会出现过于整齐的堆叠
*/
void update_local(SandBlock *block){
int x = 0;
int y = 0;
//int z = 0;//重力方向,1表示是正方向,-1表示反方向;当该方向无重力时为0 。
int s_i=BOX_LEN,s_j=BOX_LEN;//遍历起点,默认右下角开始
update_angle();//获取重力方向
if(boxangel.cosx>0.5){ //重力向右
x=1;
s_i=BOX_LEN-1;
}
if(boxangel.cosx<-0.5){ //重力向左
x=-1;
s_i=1;
}
if(boxangel.cosy<-0.5){ //重力向下
y=1;
s_j=BOX_LEN-1;
}
if(boxangel.cosy>0.5){ //重力向上
y=-1;
s_j=1;
}//判断重力方向并设置循环起点
for(int i=s_i;i>0&&i<=BOX_LEN;i-=(x?x:1)){//当该轴有重力作用时采用对应的遍历方向,否则使用1
for(int j=s_j;j>0&&j<=BOX_LEN;j-=(y?y:1)){
if(block->broad[i][j]){
int ni=i,nj=j;
if(!block->broad[i+x][j+y]){
ni=i+x,nj=j+y;
}else if(sandclock_Random(i+j)%2==0){ //随机判断先向左还是先向右
if(!block->broad[i+x][j]){
ni=i+x,nj=j;
}else if(!block->broad[i][j+y]){
ni=i,nj=j+y;
}
}else{
if(!block->broad[i][j+y]){
ni=i,nj=j+y;
}else if(!block->broad[i+x][j]){
ni=i+x,nj=j;
}
}
if(ni==i&&nj==j){
/*如果经过上面的判定没有任何移动则进入第二次判断
这次判断是为了更好的模拟物理效果
*/
int nx=x,ny=y; //判断新的重力方向
if(boxangel.cosx>0.2){ //重力向右
nx=1;
}
if(boxangel.cosx<-0.2){ //重力向左
nx=-1;
}
if(boxangel.cosy<-0.2){ //重力向下
ny=1;
}
if(boxangel.cosy>0.2){ //重力向上
ny=-1;
}
if(!block->broad[i+nx][j]&&block->broad[i-nx][j]){//如果重力方向上没有物体且反方向有
ni=i+nx,nj=j;
}else if(!block->broad[i][j+ny]&&block->broad[i][j-ny]){
ni=i,nj=j+ny;
}
if(sandclock_Random(i+j)%10>6){//随机决定到底要不要移动
ni=i,nj=j;
}
}
block->broad[i][j]=0;
block->broad[ni][nj]=1;//这两个赋值不能颠倒顺序
}
}
}
}更新沙子的位置,block指定更新哪一个块。
这里最重要的是位移逻辑:
首先我们获取了重力方向,并将xy赋上对应的值;
接着我们还要决定从哪个角开始遍历:从逻辑上来说,我们应该确保处于“下方”的沙子最先被更新,以便腾出位置共上方的沙子位移。因此我们还需要根据重力方向决定遍历起始坐标点。
由于起始点是在变化的,遍历时ij到底是自增还是自减就也不确定了;这时还要再次检查重力才行(即for循环里的“i-=(x?x:1)”)。
最后,当我们遍历到的某个位置有沙子(即被赋值为1),进入第一次判断:
- 重力方向对应的斜下方是否有空(即为0)?
- 两个重力方向上是否有空?
如果成立,则进行位移,并进入下一个循环,如果都不成立,则进入第二次判断:
- 将重力阈值调到0.2;
- 大体逻辑与第一次相同,不过没有斜向的位移;
- 只有当沙子一边有空一边没空的时候才会位移(为了产生沙丘的形状)
- 即便符合所有条件,也要经过一个随机数判断才会位移;
大体就是如此,判断如此麻烦是因为想要尽可能使沙子“自然”的运动,如坍塌、晃动或是下落。虽然最终效果还算差强人意,不过仍有许多地方可以改良。(比如引入加速度)
sand_clock(void);
/*
沙子流逝。
会根据重力判断从哪个流向哪个
*/
void sand_clock(void){
update_angle();
if((boxangel.cosx>0.5||boxangel.cosy<-0.5)&&(boxangel.cosx>-0.5&&boxangel.cosy<0.5)){
//从一到二
if(block1.broad[BOX_LEN][BOX_LEN]==1){
block1.snum--;
block2.snum++;
block1.broad[BOX_LEN][BOX_LEN]=0;
block2.broad[1][1]=1;
}
sand_clock_time=block1.snum;
}
if((boxangel.cosx<-0.5||boxangel.cosy>0.5)&&(boxangel.cosx<0.5&&boxangel.cosy>-0.5)){
//从二到一
if(block2.broad[1][1]==1){
block1.snum++;
block2.snum--;
block2.broad[1][1]=0;
block1.broad[BOX_LEN][BOX_LEN]=1;
}
sand_clock_time=block2.snum;
}
}
没什么好说的。
上面的沙漏是斜着摆放的,这里把它给摆正了:
sand.h
#ifndef SAND_H
#define SAND_H
#include "./BSP/MPU6050/MPU6050.h"
#include "math.h"
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include "./BSP/LCD/lcd.h"
#include "./SYSTEM/delay/delay.h"
/* 宏定义 */
#define SAND_NUM 210 //沙子的数量
#define BOX_LEN 30 //沙漏每部分的长度有几个单位,不包括边缘,详情见init_block()
#define SAND_LEN 3 //沙子的长度
/* 结构体 */
typedef struct{
double cosx;
double cosy;
double cosz;
}BoxAngel;//姿态角
typedef struct{
int flag;
int snum;
int broad[BOX_LEN+2][BOX_LEN+2];
}SandBlock;//沙漏块
/* 全局变量 */
extern BoxAngel boxangel;
extern SandBlock block1,block2;
extern int sand_clock_time;
extern int forward; /* 前进方向 */
extern int model; /*模式选择,是MPU6050读取0还是摇杆读取1*/
/* 摇杆 */
extern uint16_t PS2_xy_value[2]; /* 0x 1y */
extern uint8_t PS2_z; /* z轴是否被按下 */
extern float PS2_x_f,PS2_y_f; /* xy的相对值,取值0~1 */
/* 函数声明 */
void update_angle(void);
void init_block(void);
//uint8_t update_forword(void);
void update_local(SandBlock *block);
void draw_sandblock(int zx,int zy,SandBlock *block);
void draw_sand(int zx,int zy,uint16_t color);
void sand_clock(void);
#endif
sand.c
#include "./BSP/Sandbox/sand.h"
#include "math.h"
#define ABS(x) ((x) > 0 ? (x) : -(x))
/*
芯片方向:
X<----------
| SDA
| SCL
| GND
| 5V
V Y
但是我们可以对x轴取反,这样就变成了正常的坐标系:
屏幕坐标系:
------------->X
|
|
|
|
|
V Y
经过测量,各个典型角度的读数如下所示(方位余弦值):
正面朝上平放:
x=0.04
y=-0.01
z=-1.00
正面朝前竖直放置:
x=0.04
y=-1.00
z=-0.03
正面朝后倒立放置:
y=1.00
正面向右侧翻,侧立:
x=1.00
y=-0.03
z=0.05
正面向左侧立:
x=-1.00
正面超前竖立,然后右倾45°左右:
x=0.71
y=-0.70
z=0.08
左倾45°:
x=-0.71
右倾135°,即旋转180°倒立,在左转45°:
x=0.73
y=o.68
左倾135°:
x=-0.76
*/
BoxAngel boxangel;
SandBlock block1,block2;
int sand_clock_time=SAND_NUM;
int forward=2; /* 前进方向 */
/* 摇杆 */
uint16_t PS2_xy_value[2]; /* 0x 1y */
uint8_t PS2_z = 0; /* z轴是否被按下 */
float PS2_x_f = 0,PS2_y_f = 0; /* xy的相对值,取值0~1 */
int model=0;
int sandclock_Random(int n)
{
srand(SysTick->VAL+n); /* 半主机模式下使用time函数会报错,在这里用系统定时器的值替代 */
return rand() % 100;
}
/*
通过读取加速度来计算姿态角。
至于陀螺仪谁爱写谁写。
*/
void update_angle(void)
{
if(model){
boxangel.cosx=PS2_x_f*2;
boxangel.cosy=-PS2_y_f*2;
}
else{
int16_t ax,ay,az, GX, GY, GZ; //定义用于存放各个数据的变量,A是加速度,G是陀螺仪
MPU6050_GetData(&ax, &ay, &az, &GX, &GY, &GZ); //获取MPU6050的数据
double A =sqrt(ax*ax + ay*ay + az*az);
boxangel.cosx=ax/A;
boxangel.cosy=ay/A;
boxangel.cosz=az/A;//计算方向余弦
}
}
/*
初始化沙漏块
*/
void init_block(void){
block1.flag=1;
block2.flag=2;
block1.snum=SAND_NUM;
block2.snum=0;
for(int i=1;i<=BOX_LEN;i++){
for(int j=1;j<=BOX_LEN;j++){
block1.broad[i][j]=0;
block2.broad[i][j]=0;
}
}
for(int i=0;i<=BOX_LEN+1;i++){//将边缘处全部赋值为-1
block1.broad[i][0]=-1;
block1.broad[0][i]=-1;
block1.broad[i][BOX_LEN+1]=-1;
block1.broad[BOX_LEN+1][i]=-1;
block2.broad[i][0]=-1;
block2.broad[0][i]=-1;
block2.broad[i][BOX_LEN+1]=-1;
block2.broad[BOX_LEN+1][i]=-1;
}
int snum=0;
for(int i=1;i<=BOX_LEN;i++){//放置沙子
if(snum == SAND_NUM)break;
for(int j=1;j<=BOX_LEN;j++){
if(snum == SAND_NUM)break;
block1.broad[i][j]=1;
snum++;
}
}
}
/*
更新位置,如果某个沙子的重力方向上没有其它沙子,也不处于底边,则移动。
由于我们把边缘处全部赋值为1了,所以不需要判断哪边是底边,只要判断有没有其它沙子就行了
重力方向:当某个方向与坐标轴的夹角小于60度则将该方向视为重力方向之一。
位移判断有两次:第一次是常规判断;
第二次在第一次判定没有产生位移的情况下产生,并使用更小的重力阈值;
且第二次判断不会产生斜向位移并用随机减小位移概率。
这是为了更好的模拟出倒塌的物理效果而不会出现过于整齐的堆叠
*/
/*更新方向 0平放,1代表下,2右下,3右,4右上,5上,6左上,7左,8左下
v是判断阈值
*/
uint8_t update_forword(float v){
int x,y=0;
update_angle();//获取重力方向
if(boxangel.cosx>v){ //重力向右
x=1;
}
if(boxangel.cosx<-v){ //重力向左
x=-1;
}
if(boxangel.cosy<-v){ //重力向下
y=1;
}
if(boxangel.cosy>v){ //重力向上
y=-1;
}
if(!x&&!y){//平放
return 0;
}
if(y==1&&x==0){//下
return 1;
}
if(y==1&&x==1){//右下
return 2;
}
if(x==1&&y==0){//右
return 3;
}
if(y==-1&&x==1){//右上
return 4;
}
if(y==-1&&x==0){//上
return 5;
}
if(y==-1&&x==-1){//左上
return 6;
}
if(y==0&&x==-1){//左
return 7;
}
if(y==1&&x==-1){//左下
return 8;
}
return 0;
}
void update_local(SandBlock *block){
int x = 0;
int y = 0;
//int z = 0;//重力方向,1表示是正方向,-1表示反方向;当该方向无重力时为0 。
int s_i=BOX_LEN,s_j=BOX_LEN;//遍历起点,默认右下角开始
update_angle();//获取重力方向
// 1. 计算重力在菱形网格上的分量
// ---------------------------------------------------------
// 物理向下 (屏幕下方): 对应网格 (i增加, j增加)
if (boxangel.cosy < -0.5) {
x += 1;
y += 1;
}
// 物理向上 (屏幕上方): 对应网格 (i减小, j减小)
else if (boxangel.cosy > 0.5) {
x -= 1;
y -= 1;
}
// 物理向右 (屏幕右方): 对应网格 (i增加, j减小)
if (boxangel.cosx > 0.5) {
x += 1;
y -= 1;
}
// 物理向左 (屏幕左方): 对应网格 (i减小, j增加)
else if (boxangel.cosx < -0.5) {
x -= 1;
y += 1;
}
// 2. 限制步长范围 (Clamp)
// 防止斜向重力叠加导致步长变成2
if (x > 1) x = 1;
if (x < -1) x = -1;
if (y > 1) y = 1;
if (y < -1) y = -1;
// 3. 确定遍历起点 (s_i, s_j)
if (x > 0) s_i = BOX_LEN - 1;
else s_i = 1; // 没位移时默认从头开始
if (y > 0) s_j = BOX_LEN - 1;
else s_j = 1;
//判断重力方向并设置循环起点
for(int i=s_i;i>0&&i<=BOX_LEN;i-=(x?x:-1)){//当该轴有重力作用时采用对应的遍历方向,否则使用1
for(int j=s_j;j>0&&j<=BOX_LEN;j-=(y?y:-1)){
if(block->broad[i][j]){
int ni=i,nj=j;
if(!block->broad[i+x][j+y]){
ni=i+x,nj=j+y;
}else if(sandclock_Random(i+j)%2==0){ //随机判断先向左还是先向右
if(!block->broad[i+x][j]){
ni=i+x,nj=j;
}else if(!block->broad[i][j+y]){
ni=i,nj=j+y;
}
}else{
if(!block->broad[i][j+y]){
ni=i,nj=j+y;
}else if(!block->broad[i+x][j]){
ni=i+x,nj=j;
}
}
if(ni==i&&nj==j){
/*如果经过上面的判定没有任何移动则进入第二次判断
这次判断是为了更好的模拟物理效果
*/
int nx=x,ny=y;
if (boxangel.cosy < -0.2) {
nx += 1;
ny += 1;
}
else if (boxangel.cosy > 0.2) {
nx -= 1;
ny -= 1;
}
if (boxangel.cosx > 0.2) {
nx += 1;
ny -= 1;
}
else if (boxangel.cosx < -0.2) {
nx -= 1;
ny += 1;
}
if (nx > 1) nx = 1;
if (nx < -1) nx = -1;
if (ny > 1) ny = 1;
if (ny < -1) ny = -1;
if(!block->broad[i+nx][j]&&block->broad[i-nx][j]){//如果重力方向上没有物体且反方向有
ni=i+nx,nj=j;
}else if(!block->broad[i][j+ny]&&block->broad[i][j-ny]){
ni=i,nj=j+ny;
}
if(sandclock_Random(i+j)%10>6){//随机决定到底要不要移动
ni=i,nj=j;
}
}
block->broad[i][j]=0;
block->broad[ni][nj]=1;//这两个赋值不能颠倒顺序
}
}
}
}
/*
* 绘制菱形沙粒函数
* 增加了边界检查,防止负坐标导致的乱码/满屏线条
*/
void draw_sand(int cx, int cy, uint16_t color) {
// 将半径设为 SAND_LEN 的一半,这样大小跟原来的方块差不多
// 如果觉得沙粒太小,可以改为 int r = SAND_LEN;
int r = (SAND_LEN > 1) ? (SAND_LEN / 2) : 1;
// 逐行绘制
for (int dy = -r; dy <= r; dy++) {
// 计算当前行的半宽
int w = r - abs(dy);
// 计算起点和终点
int x_start = cx - w;
int x_end = cx + w;
int y_pos = cy + dy;
// --- 核心修复:边界裁剪 (Clipping) ---
// 1. 如果 Y 轴超屏,直接跳过
if (y_pos < 0 || y_pos >= SSD_VER_RESOLUTION) continue;
// 2. 如果 X 轴完全在屏幕外,跳过
if (x_end < 0 || x_start >= SSD_HOR_RESOLUTION) continue;
// 3. 限制 X 轴在屏幕范围内 (防止负数变成65535)
if (x_start < 0) x_start = 0;
if (x_end >= SSD_HOR_RESOLUTION) x_end = SSD_HOR_RESOLUTION - 1;
// 绘制
if (x_end >= x_start) {
lcd_draw_line(x_start, y_pos, x_end, y_pos, color);
}
}
}
/*
* 绘制旋转沙漏
* zx: 请务必传入屏幕宽度的中间值(例如 120),否则会画到屏幕外面!
* zy: 距离顶部的距离
*/
void draw_sandblock(int zx, int zy, SandBlock *block) {
// 步长:两个格子的中心距离
// 为了紧密堆叠,步长应等于 2倍半径 + 缝隙(0或1)
// 这里设为 SAND_LEN 即可(假设上面的半径是 SAND_LEN/2)
int step = SAND_LEN;
// 1. 绘制边框
// 计算四个顶点的逻辑位置 (i, j)
// 顶点1 (Top): 1, 1
// 顶点2 (Right): N, 1
// 顶点3 (Bottom): N, N
// 顶点4 (Left): 1, N
// 使用简单的相对坐标计算屏幕坐标
// 旋转公式:ScreenX = zx + (i - j) * step
// ScreenY = zy + (i + j) * step / 2 <-- 除以2是为了压扁一点,形成透视感;不除以2则是标准45度
// 这里使用标准的 45度 坐标映射 (x = x-y, y = x+y)
// 预先计算四个角的屏幕坐标
int x_top = zx;
int y_top = zy-3;
int x_right = zx + (BOX_LEN * step)+3;
int y_right = zy + (BOX_LEN * step);
int x_bottom = zx;
int y_bottom = zy + (2 * BOX_LEN * step)+3;
int x_left = zx - (BOX_LEN * step)-3;
int y_left = zy + (BOX_LEN * step);
// 绘制外框
// 注意:这里的边框是连接网格中心的,为了美观外扩一点,这里先画核心骨架
lcd_draw_line(x_top, y_top, x_right, y_right, g_point_color);
lcd_draw_line(x_right, y_right, x_bottom, y_bottom, g_point_color);
lcd_draw_line(x_bottom, y_bottom, x_left, y_left, g_point_color);
lcd_draw_line(x_left, y_left, x_top, y_top, g_point_color);
// 2. 绘制沙粒
for (int i = 1; i <= BOX_LEN; i++) {
for (int j = 1; j <= BOX_LEN; j++) {
// 核心坐标变换算法
// (i-1) 和 (j-1) 是为了让 (1,1) 在原点
int grid_i = i - 1;
int grid_j = j - 1;
// X轴:i 增加向右,j 增加向左
int sx = zx + (grid_i - grid_j) * step;
// Y轴:i 增加向下,j 增加向下
int sy = zy + (grid_i + grid_j) * step;
if (block->broad[i][j] == 1) {
uint16_t c = (i == BOX_LEN && j == BOX_LEN) ? RED : WHITE;
draw_sand(sx, sy, c);
} else {
draw_sand(sx, sy, BLACK);
}
}
}
}
/*
沙子流逝。
会根据重力判断从哪个流向哪个
*/
void sand_clock(void){
int f=update_forword(0.5);
if(f==1||f==2||f==8){
//从一到二
if(block1.broad[BOX_LEN][BOX_LEN]==1){
block1.snum--;
block2.snum++;
block1.broad[BOX_LEN][BOX_LEN]=0;
block2.broad[1][1]=1;
}
sand_clock_time=block1.snum;
}
if(f==4||f==5||f==6){
//从二到一
if(block2.broad[1][1]==1){
block1.snum++;
block2.snum--;
block2.broad[1][1]=0;
block1.broad[BOX_LEN][BOX_LEN]=1;
}
sand_clock_time=block2.snum;
}
}
- 虽然我给它命名为“姿态角”,但它和真正的姿态角还是有些不同的。真正的姿态角需要结合重力加速度传感器以及角速度加速度传感器(即俗称的六轴加速度传感器)经过一系列运算才能得出期间当前的姿态角。而我们的数据完全来自于重力。 ↩︎


发表回复