灵感来源:【《赛 博 沙 漏》】 https://www.bilibili.com/video/BV1yj411o7Hf/?share_source=copy_web&vd_source=a9d52604c74e8a682e8f5b4ef43d9198

代码基于STM32实现,不过硬件部分只涉及重力环境的获取以及绘制,因此移植到其它平台是十分简单的,只需要重写update_angle()和两种绘制函数即可。

这种东西在GitHub上也有源码,但我在看了“硬禾学堂”的相关源码后彻底放弃了理解作者思路的尝试,或许自己从头想还会更简单一点……吧?

不过看这些源码也不能是毫无收获。至少我知道了当同时出现两种可行的路线时,所有人都不约而同地用随机数代替复杂的重力判断。

sand.h
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;

/* 函数声明 */
void update_angle(void);

void init_block(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
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 sandclock_Random(int n)
{
    srand(SysTick->VAL+n);                      /* 半主机模式下使用time函数会报错,在这里用系统定时器的值替代 */
    return rand() % 100;
}

/*
通过读取加速度来计算姿态角。
至于陀螺仪谁爱写谁写。
*/
void update_angle(void)
{
    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_i=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;
    }
}




下面我们来看看具体的各个函数:

宏定义

C
/* 宏定义 */
#define SAND_NUM  520    //沙子的数量
#define BOX_LEN 30    //沙漏每部分的长度有几个单位,不包括边缘,详情见init_block()
#define SAND_LEN 5      //沙子的长度

可能会觉得疑惑的应该只有BOX_LEN了:这里需要提一嘴我们是如何判断数组边缘的:如果你想要创建一个30*30的数组,那么程序实际上创建的是32*32的数组。多余的部分将在初始化函数里被赋值为-1作为边缘(或是别的什么也行,只要非0就行)。

结构体

C
/* 结构体 */
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数组里存储了每个沙子的位置。

全局变量

C
/* 全局变量 */
extern BoxAngel boxangel;
extern SandBlock block1,block2;
extern int sand_clock_time;

boxangel:设备当前的姿态角;

blockx:沙漏上下两个块;

sand_clock_time:沙漏剩余时间,即当前处于上方的块里剩余的沙子数量;根据重力方向而自动改变。

函数

获取重力方向、在屏幕上绘制图形属于硬件依赖,在此不过多赘述。感兴趣者可以查看正点原子和江协科大的视频。

init_block()

C
/*
初始化沙漏块
*/
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);

C
/*
更新位置,如果某个沙子的重力方向上没有其它沙子,也不处于底边,则移动。
由于我们把边缘处全部赋值为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_i=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);

C
/*
沙子流逝。
会根据重力判断从哪个流向哪个
*/
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;
    }
}

没什么好说的。

  1. 虽然我给它命名为“姿态角”,但它和真正的姿态角还是有些不同的。真正的姿态角需要结合重力加速度传感器以及角速度加速度传感器(即俗称的六轴加速度传感器)经过一系列运算才能得出期间当前的姿态角。而我们的数据完全来自于重力。 ↩︎

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


目录