zoukankan      html  css  js  c++  java
  • 僵尸捉猫

    2/3D游戏:2D

    辅助插件:原生

    游戏制作难度系数:

    游戏教程网址:http://www.raywenderlich.com/61532/unity-2d-tutorial-getting-started

    1、设置背景轮番播放

    因为此游戏是横版游戏,所以要设置游戏背景轮番播放 

        Transform mycamera;
        float width;
        void Start()
        {
            SpriteRenderer render = transform.GetComponent<SpriteRenderer>();//获取背景图的SpriteRenderer
            width = render.sprite.bounds.size.x;//得到背景图的宽度
            mycamera = Camera.main.transform;
        }
        void Update()
        {
            if (mycamera.position.x > transform.position.x + width) //当摄像机的位置超过第二章背景图的中心位置时
            {
                //把第一张背景图的位置设置到第二张背景图的后面去
                Vector3 xx = transform.position;
                xx.x += 2.0f * width;
                transform.position = xx;
            }
        }

     

    摄像机的移动

     Vector3 position;
        void Start()
        {
            position = Camera.main.transform.position;
        }
        void Update()
        {
            position.x += Time.deltaTime;
            transform.position = position;
        }

    2、不要在OnBecameInvisible里面用Camera.main 

    一旦程序关闭,摄像机是找不到的,而OnBecameInvisible最后会在程序结束时执行一次
    所以最好不要在OnBecameInvisible方法里面使用Camera.main

    如果用,最好在Start里面用字段指向came=Camera.main,然后在OnBecameInvisible方法里面一开始判断camera是否为null

    3、 rigidbody2D.velocity

    给刚体一个速度,该速度是有一个X和Y方向向量组件指定(在二维物理没有Z轴)。该值通常不直接设置而使用添加力。如果使用此属性,由于阻力的影响速度也会逐渐衰减。

    有了这个速度,物体就会按照设置一直运动!

    4、设置对象无敌隐身效果

    void Update()
    {
    if (IsTouchEnemy)//如果触碰到敌人 { wudiTime += Time.deltaTime; float tt = wudiTime % .3f; renderer.enabled = tt > 0.15f ? true : false; if (wudiTime > 1.6f) //超过一定时间,设置不隐身 { IsTouchEnemy = false; wudiTime = 0f; renderer.enabled = true; //自身不再隐身 } }
    }

    时间wudiTime是一个不断增长的时间,wudiTime求余0.3得出的余数tt是关键,经调试,余数tt是一个有规律的数值,如图:

       

    等于号左边的是wudiTime的值,右边是wudiTime%0.3之后所得到的余数tt,可以发现,tt遵从一个规律,从0.0...缓慢增长到大约0.3之后又从0.0...缓慢增长到大约0.3,依此反复,如果你%3的话,那么余数tt会缓慢从0.0...增长到大约3之后又从0.0...增长到大约3,依此反复!与正负无关!这大概是数学的魅力吧,所以依据此,我们就可以设置一个断断续续的隐身无敌状态了!

    5、设置对象旋转

    因为此游戏是2D游戏,所以使用LookAt不太恰当,只能使用Rotation进行旋转,我们的需求:当用户点击屏幕上某个位置后,此对象要慢慢面向鼠标点击的位置

    void Update()
    {
        currentPosition = go.transform.position;//对象当前的位置
        if (Input.GetMouseButton(0))
        {
            Vector3 positon = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            targetPosition = positon - currentPosition; // 两个点确定一个方向向量
        }
        float angle = Mathf.Atan2(targetPosition.y, targetPosition.x) * Mathf.Rad2Deg;
        go.transform.rotation = Quaternion.Slerp(go.transform.rotation, Quaternion.Euler(0, 0, angle), Time.deltaTime * turnSpeed);
    } 
    Mathf.Rad2Deg:弧度转换为角度, Mathf.Atan2:找到(起始为零点,终点为targetPosition的2D向量)与(x轴)之间的角度,Mathf.Atan2将返回角度的弧度

    经测试,此计算旋转角度的方法可以用于“切水果游戏”的计算刀光角度!两者差不多,只不过“切水果游戏”是根据起始点和终止点两个点来算的,而本游戏是根据这两个点形成的方向向量来算的!

    控制僵尸行走: 

            currentPosition = go.transform.position;
            if (Input.GetMouseButton(0))
            {
                Vector3 positon = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                targetPosition = positon - currentPosition; // 两个点确定一个方向向量
                targetPosition.z = 0;
                targetPosition.Normalize();//targetPosition 主要是确定僵尸行走的方向
            }
    
            Vector3 target = targetPosition * moveSpeed + currentPosition; //targetPosition * moveSpeed 确定僵尸行走的快慢
            Vector3 walkdistance = Vector3.Lerp(currentPosition, target, Time.deltaTime);
            go.transform.position = walkdistance;
           
             //此方式和上面的是一样的
            //go.transform.position += targetPosition * moveSpeed * Time.deltaTime;
    
            float angle = Mathf.Atan2(targetPosition.y, targetPosition.x) * Mathf.Rad2Deg;
            go.transform.rotation = Quaternion.Slerp(go.transform.rotation, Quaternion.Euler(0, 0, angle), Time.deltaTime * turnSpeed);

     6、控制对象在屏幕之内 

       /// <summary>
        /// 检测是否出边界
        /// </summary>
        void IsOutBound()
        {
            Vector3 currentPosition = transform.position; //当前对象的位置
            float maxY = Camera.main.orthographicSize; //获取屏幕的最大高度
            float maxX = Camera.main.aspect * Camera.main.orthographicSize;//获取屏幕的最大宽度
            Vector3 cameraPosition = Camera.main.transform.position;//获取摄像机的位置
            if (currentPosition.x > maxX + cameraPosition.x || currentPosition.x < -maxX + cameraPosition.x) //如果对象的水平位置大于屏幕的宽度或小于屏幕的宽度
            {
                float i = Mathf.Clamp(currentPosition.x, -maxX + cameraPosition.x, maxX + cameraPosition.x);
                currentPosition.x = i;
                transform.position = currentPosition;
                targetPosition.x = -targetPosition.x;
            }
            if (currentPosition.y > maxY + cameraPosition.y || currentPosition.y < -maxY + cameraPosition.y)//如果对象的垂直位置大于屏幕的高度或小于屏幕的高度
            {
                float i = Mathf.Clamp(currentPosition.y, -maxY + cameraPosition.y, maxY + cameraPosition.y);
                currentPosition.y = i;
                transform.position = currentPosition;
                targetPosition.y = -targetPosition.y;
            }
        }

     7、背景图高度自适应

    Camera的orthographicSize定义了视图的尺寸.它的值是从视图中心到视图顶部的距离.换句话说这个值等于视图一半高度.视图的宽度基于视图的长宽比计算

     我们需要将背景图片从上到下完全占满整个屏幕,并允许其水平滚动.背景图像的高度是640px,我们取一半,即320px.不过这并不是完全正确的.

    Project视图里选择background的父级查看Import Settings.
    在精灵渲染器(Sprite Renderer)的”像素到单位(Pixels to Units)”默认值是100,如下图:

    Unity,”单位(Units)”并不一定对应到屏幕上的像素.通常物体的大小都是相对于彼此的,可以假设单位为任何计量单位,1 unit=1.对于精灵,Unity以像素为单位来确定大小.

    例如,准备将一个500px宽度的图像导成精灵.下表显示了将它用不同的”Pixels to Units”时所呈现出的精灵在Y轴向上的差别.

    不同”像素到单位(Pixels to Units)”的对比图.

    背景图片原始高为640,并且背景图background精灵的”像素到单位(Pixels to Units)”比例为100,并且scale中的y值为1.0,所以在Hierarchy视图中它将呈现为6.4个units高(640/100*1.0).正交相机的Size属性值是屏幕高度的一半,所以我们要设置相机尺寸(Size)为3.2个单位即可!

    此方法只限于背景图使用的是Sprite Renderer来渲染的,并且摄像机是orthographicSize正交模式

    如果像“疯狂喷气机”那种游戏的背景是使用Quad来设置背景图片的话,如果想设置高度自适应的话,那么背景图片的高度的Y轴Scale就要设置为图片原始高度除以100(像素到单位)就可以了,摄像机的尺寸设置为图片原始高度除以100,再除以2即可!

    8、图片纹理大小处理及占用内存调节

     

    从图片的Import settings面板中可以看出一张图片的纹理的尺寸,颜色信息和内存使用情况,如果背景图的实际像素为2048x640,如果MaxSize设置小于这个值为1024x320,就相当于缩小了图片,那么游戏中图片最大尺寸就会为1024x320,如果MaxSize设置大于2048x640,如果为4096x1280的话,那么游戏中图片最大还是为2048x640,另外,Format设置图片是否压缩,真彩色或16bits,图片内存的占用就依据这个的,但是压缩不一定内存占用就少,要看具体的图片,所以这三种方式都试一遍就知道了!一般unity发出黄色警告的,就不要设置此项了!

    9、代码设置僵尸行走动画图片轮流播放

    float framesPerSecond=10f;
    void Update()
    {
         int index = (int)(Time.timeSinceLevelLoad * framesPerSecond);
         index = index % sprites.Length;
         spriteRenderer.sprite = sprites[ index ];
    } 

    你知道动画数组成员不会是无限数量的,当动画帧数组播放一遍后你需要循环回到开始,通过执行模数(%)操作,即做两个数字之间的整除取余.换句话说你将得到0~数组总成员数之间的索引来控制动画帧的播放. 

    数学规律:(正整数)Z=m%n,得出的Z的模数范围是 [ 0 ~(n-1)] 

    因为Time.timeSinceLevelLoad输出的是递增的小数,如:0.322   0.333  0.344   0.355 . 0.366   0.377  ...

    framesPerSecond为10时: 

    a、如果乘以10,那么index(m)获得的正数就是 3  3  3  4  4  4  ..... 

    b、再除以sprites.Length(4)得到余数,则为  3 3 3 1 1 1 ....

    framesPerSecond为100时: 

    a、如果乘以100,那么index(m)获得的正数就是32  33  34   35   36   37 .... 

    b、再除以sprites.Length(4)得到余数,则为  0 1 2 3 0 1 .... 

    从上可以看出10的情况,播放的动画就慢些,而100的话,则动画转换的就快些,framesPerSecond控制的是走的快慢

    此算法也可用于对象的隐身,自行测试,感叹数学之美啊!

    10、Sprite Packing合并到图集

    目的:减少draw calls提高游戏性能

    Project视图中找到cat.png并打开Import Settings,这里注意名为Packing的标签属性. Packing标签用来定义精灵加入到纹理图集后的名称,可以是任何你想要的字符串.
    Packing标签中输入它的名字”toons”:

    然后点击应用按钮保存设置,用同样的方法将enemy和zombie也命名一下.
    然后从”WindowSprite Packer”菜单中打开”Sprite Packer”,如果是4.3.2版本,有可能是” WindowSprite Packer (Developer Preview)”这样的菜单,那么将会显示下面这样的错误提示:

     

    正如你所见默认是禁用Sprite packing的,我们到”EditProject SettingsEditor”中把Sprite Packer的模式选为”Always Enabled”,像下图这样:

    当选择”Always Enabled”后,你可以在build时候看到一个”only packing your Sprites for builds”选项.

    再次从”WindowSprite Packer”菜单打开Sprite Packer窗口,如下图:

    黄色的文字是提醒你用的是一个功能的预览版.以后正式版就不会看到这个提醒文字了.

    在这个窗口的左上方点击Pack按钮,你会看到精灵都排列在窗口里了,如下图:

    再次运行游戏到游戏视图查看运行状态信息,你将看到现在只用了两个draw calls,比原来省了两个.

    这样做的好处是显而易见的,精灵共享使用了材质球,优化了性能,同时做这个优化如此简单,何乐不为.

    Sprite Packer的选项和问题:

    Sprite Packer窗口顶部包含一个控制栏,如下图:

    1:视图中看到的是目前的图集.第一个下拉菜单是你使用过的Packing标签名称.可以选择它查看它的内容.
    2:如果Sprite Packer帮你整合图集超过一张图,你需要通过第二个下拉菜单切换其它图集.
    3:默认精灵在图集中的分配方法是按照DefaultPackerPolicy进行的,可以通过调整DefaultPackerPolicy来自定义分配方法,不过这个高级功能本教程就不介绍了.

    有时候Unity会将我们本来要整合到一个图集拆分创建成多个地图集,并把名字加了序号.造成这个问题的原因是精灵纹理压缩格式不同.

    例如:zombie.png我们设置的是16位颜色(16 bits),而enemy.png和cat.png是用的压缩格式(Compressed),那么Unity整合图集时,将会创建出两个图集,名称分别是”toons (Group 1)”和”toons (Group 2)”:

    尽管这三个精灵有相同的Packing标签,但Unity仍旧创建了多个图集.为了确保图集最优化,我们要确保准备整合到同一个图集中的精灵的压缩格式相同.

    正常情况下,将尽可能的减少精灵图集的总数,除非精灵太多,一张图集存不下,Unity会再次自动拆分图集,我们要用Packing标签合理安排精灵归属,尽可能做到精灵图集的最优化.

    这里有个合并图集插件:https://www.codeandweb.com/texturepacker

    一个人家自己写的插件教程:https://www.youtube.com/watch?v=nDzZ_5cOFR0

    11、Animation动画

    沿着时间轴顶部,你可以看到由秒数、冒号帧数组成的标签。

    数值从零开始计数,所以0:02表示动画的第一秒的第三帧。我想用1:02做为示例,但是我怕“第二秒的第三帧”的说法可能会让人困惑。

    Samples字段定义了一个动画剪辑的帧的速率,它默认值是60FPS。修改这个值为10,然后你会就注意到时间轴从之前的1:00变为了从0:00到0:09:

    如果你在Project浏览器中简单选择一组精灵,然后把它们拖拽到Scene或Hierarchy视图,Unity会和之前做的一样,去创建动画剪辑、动画片绘制者和动画控制器。不过,它也在场景中创建了一个游戏对象,并把所有一切与它连接了起来!

    12、Animator

    1.      开始和结束标识:这些图标表明了过渡的开始和结束点。它们看起来和>|和|<很像。

    2.      一段高亮的蓝色区域清楚的表明了动画片段中和过渡的相关的部分。

    3.      一个指示器,标明了当前位置,当你在预览面板预览过渡效果时,在Inspector的底部(图中没有显示)。

    4.      矩形块展示了与过渡相关的动画片段。如果其中一个片段循环了,它可能会出现很多次,如果需要覆盖过渡期间。蓝色高亮的区域示意了片段的那一部分和到哪种程度,这2个动画在任意时间点会如何影响。

    5.      我不确定这个是什么意思。它看上去好像是是个图形,显示了动画片段是如何影响最终的弯曲值,但是我不确定。不管如何,我一般忽略它,也没有任何问题。

    Exit Time:就是当特定比例的第一个动画播放完后触发才去触发过渡。

    改变这个Exit Time到0.0.1,Exit Time的值域是0到1,所以0.0.1指在播放到1%的上一个动画后开始进行过渡。

    Trigger参数和Bool类型很像,除了当你设置一个Trigger时,当它触发一个过渡后,这个Trigger会在过渡结束后自动重设它的值。 

    AnyState:

     这个其实并非一个状态,他是一个特殊用来创建个可以随时随地发生的过渡。

    例如,假定你在写一个游戏,在这个游戏中,玩家总是可以有一个能力,可以使用一个武器,不管当前玩家处于什么动画状态,都可以尝试开火,然后调用他的开火动画FireWeapon

    不用创建一个从所有状态过渡到FireWeapon的状态,你可以只需要创建一个从Any State到FireWeapon的过渡。

    然后,当这个过渡的条件满足后-在这种假设的情况下。可能当FirePressed Bool类型参数为True-然后当这个过渡被触发,不管当前Animator控制器在所在何种状态,这个过渡都会被执行。

    SubState:

    除了动画片段之外,Animator控制器中的状态可以是一个子状态机。子状态机可以让复杂的状态机更加结构简化。也就是通过将某一分支的状态包含在一个单一的状态节点内。

    例如,想像一组动画片段,组成一次攻击,例如瞄准和开火。不用将他们全部呈现在Animator控制器中,你可以将他们包含在一个子状态机中。接下来的图片显示了一个假定的僵尸的Animator控制器,,它可以从走动变为攻击状态:

    你像使用普通状态那样连接或者连接到子状态机,除了每次会弹出一个需要你指定特定状态的对话框。

    Blend Trees: 

    Blend Trees是一种特殊的状态,你可以添加到Animator控制器。他们通常将多个动画融合在一块来创造一种新的动画。例如,你可能需要一个走路和跑动的动画,你可以通过使用Blend Tree来创建一个新的动画,基于速度。 

    Blend Trees是非常复杂,需要专门的教程来学习。为了让你有一个理解,Unity的2D Character 控制器课程包含了很多使用Blend Tree来选择合适的Sprite用来作为2D任务,基于这个人物特性的速度。

    Layers:

     

    你可以使用Layers来实现3D人物中的复杂动画。例如,你可以有一个层用来控制一个3D人物的脚来实现走动动画,通过另外一个层来实现这个人物的设计动画,然后通过某种规则来混合这种动画。

    例:在一个角色控制器的Base Layer层有一个默认的qigong动画,动画效果如下:

    现在我们在新添一层,命名为Stopfoot,此层有一个walk动画(如下)和一个新创建的动画,当然这个动画什么也不播放,设置一个参数bool值,根据此值来重定向到walk动画,然后给此层添加一个遮罩,遮罩设置如下:

      

    当bool值设置为真,并且Stopfoot层的Weight权重值为1时,播放的效果如下:

     

    可以看到,人物手的动画,已经完全被Stopfoot层的walk动画给控制了,而脚的动画却还是由BaseLayer层控制,这就是Layer的用法!

    当Weight权重值越接近0的话,Stopfoot层对于手的控制将会变得削弱,0的时候就完全没有控制权了!

    层设置权重代码:Animator.SetLayerWeight(int layerIndex,float weight); //Base Layer层的layerIndex为0

    Overrider:会覆盖其他层的动画

    Additive:不会清除其它层的动画,而是在其它层的基础上再新加入自己的动画曲线

    脚本:

    Animator anim = GetComponent<Animator>();
    anim.SetBool("InConga", true);
    bool isInConga = anim.GetBool("InConga");

    最好的方式是缓存Animator的组件在一个类成员变量中,而不是每次都去读取它。此外,如果速度很关键,可以使用Animator.StringToHash来生成一个int,从string类型的”InConga“,然后使用这个方法系列中接受int的版本。如下:

      int i = Animator.StringToHash("InConga");
      animator.SetBool(i, true);

    子状态机相当于把相联接的动画放在一起,组成一组!对动画进行分组的话,条理比较清晰!

    混合树主要是使多个动画之间可以进行融合播放   http://docs.unity3d.com/Manual/class-BlendTree.html

    的作用主要是控制身体不同部位进行动画播放   http://docs.unity3d.com/Manual/AnimationLayers.html

    官方Animator教程地址:http://unity3d.com/unity/animation

     13、静态对撞机缺点

    静态对撞机,为什么他们潜在的对你不好,你已经添加了对撞机敌人和猫,但是还有一些你做了,你可能没有意识到。你无意中告诉unity这些对撞机是静态的,这意味着他们不会在scene里移动,他们(主语是指对撞机)不会在运行时被添加,删除,启用或禁用。不幸的是,这些假设是不正确的。敌人在僵尸跳康茄舞将和您将禁用猫的对撞机一旦僵尸触摸它。你需要告诉unity这些对撞机不是静态的。但首先,为什么它重要吗?unity建立一个包含所有对撞机的物理模拟场景,并优化这个过程处理不同对撞机并认为是静态的。如果一个静态对撞机在运行时更改,需要重建unity物理模拟,可以是一个昂贵的操作(即缓慢),也可以导致物理奇怪的行为。所以,当你知道你会移动GameObjects,确保他们没有静态对撞机。你怎么知道对撞机是动态的(即不是静态的)?在unity中,如果GameObject有对撞机但没有rigidbody,那么那些对撞机被认为是静态的。有刚体:动态的,无刚体:静态的总结:运行时,只要你需要对对撞机进行操作(启用,禁用,添加,删除),那么就必须把此对撞机设为动态对撞机!这意味着你只需要添加rigidbodies给猫和敌人。在层次结构中选择猫并添加一个Rigidbody 2 d组件通过选择组件物理2 d Rigidbody 2 d在unity的菜单。与猫在层次结构中选择复选框标签是RigidBody 2 d运动组件内部Inspector,如下所示:

    勾选Is Kinematic表明你将通过脚本控制对象的运动而不是依靠物理引擎来移动它。

    14、背景图设置注意点

    记得在这个系列教程的第一部分中有提到,背景精灵的宽度是2048像素(在文件夹里看到的实际宽度,而不是下图的MaxSize宽度)并且你用单位长度为100的ratio来引用这个背景精灵。也就是说将一个背景精灵的 x 位置设置为20.48将会正好把这个背景精灵放置在第一张背景图的右边

    其他

    (1)导入图片:

           当设置为3D模式时,Unity假设你将导入的图片文件创建为纹理类型,即Texture Type为Texture类型,主要是用于模型的材质贴图

           当设置为2D时,Unity假定你想要的导入的资源为Sprite类型,即Texture Type为Sprite(2D / uGUI)类型,主要用于SpriteRenderer

           而GUI绘制的图片的Texture Type类型为GUI(Editor/Legacy)

    (2)2D模式下,当把一张图片拉入Unity,为什么会产生两张图片?如下

           

          父级的cat是纹理资源.它将关联到你导入的原始美术资源文件cat.png,以及控制着如何从这个纹理资源创建Sprites,你可以看到它有个文件内容的缩略图(即子级)

          子级的cat是Unity导入cat.png时创建的Sprite资源.现在只有一个子项,因为Unity只从文件里创建了一个Sprite

          注:Unity渲染Sprite对象实际上是由一个Texture2D生成的,图像信息实际是存储于图片文件中,你也可以动态的创建自己需要的Texture2D对象来运行时生成Sprite

    (3)Tags、Layers和Sorting Layers的区别:

           Tags:是对元素的标记(标签)、标签可以用于标识游戏对象。标签必须在使用之前在标签管理器里面先声明。通过设置tag,可以方便地通过GameObject.FindWithTag() 来寻找对象。动态指定对象的标签:gameObject.tag = "Player";

            Layers:游戏对象所在的层,层的范围是在[0…31]之间。层可以用于摄像机的选择性渲染或者忽略光线投射。动态指定对象所处的层:gameObject.layer = 2;一般对象默 认处于Default层,而Camera的Culling Mask中可以设置渲染(拍摄)哪些Layers。一般设置为Everything,即照射所有层!

            Sorting Layers(Sprite Renderer):此设置是配合Order In Layer来使用的,Sorting Layers用于精灵优先渲染的图层(即设置精灵在哪一层)。Order In Layer:精灵基于其设定层的优 先级来渲染。值越小渲染顺序越靠前。如果两个对象在不同层的话,哪一层优先级高,哪一层就越后渲染(绘制),如果在同一层的话,Order In Layer值越小渲染顺序越靠前

     (4)切片精灵表

             如果得到的图片是这样的(其实是一组行走逐帧动画),我们可以选中此精灵,在Inspector面板中设置sprite mode   为Multiple,然后点击sprite editor来编辑图片即可,后面的操作看教程!

    (5)控制对象的localscale的正负,是可以控制对象的旋转的,例如localscale的x为1时,对象面向右边,为-1时对象面向左边

    (6)事实证明如果一个动画剪辑修改了一个对象的 Transform 的任何一个方面,事实上它正在修改整个Transform。准确的说:只有position是可以修改的,Rotation和Scale是不可以被修改的!但是如果动画剪辑中已经设置了position属性,那么代码中更改position的值也是没用的!例如动画控制猫咪的位置,照上面的说法,我们是无法改变猫咪的旋转的,解决方案如下:

    你需要让你的猫咪成为其他 GameObject 的子结点(child)。然后你会在这个子结点上运行这段动画,只是修改父节点的位置和回转角度(rotation)。

    (7)父亲节点和孩子节点的获取

    获取父亲节点:gameObject.transform.parent,gameObject.GetComponent(s)InParent,gameObject.transform.GetComponent(s)InParent

    获取孩子节点: gameObject.transform.GetChild,gameObject.GetComponent(s)InChildren,gameObject.transform.GetComponent(s)InChildren,遍历gameObject.transform

     

  • 相关阅读:
    Discuz中解决jquery 冲突的方法 绝对简单
    关于MySql has gone away问题的解决
    关于水晶报表的一些错误
    biweb后台添加上传下载功能
    ajax 异步插入图片到数据库(多图上传)
    ajax 异步插入图片到数据库(单图上传)
    使用ajax异步提交表单数据(史上最完整的版本)
    系统管理-软件管理
    系统管理-计划任务-系统日志
    Django-ondelete
  • 原文地址:https://www.cnblogs.com/MrZivChu/p/zoom.html
Copyright © 2011-2022 走看看