gologiusの巣

プログラミングなどの技術メモです。誰かの役に立てるとうれしいです。

【Python】 loggerのログが重複する

ログが再起動のたびに増えていくバグと、運命の出会いを果たしたのでメモします。

Pythonのログの取り方をお勉強しました

以下の記事で、ログの取り方について学びました。

ログ出力のための print と import logging はやめてほしい

超絶的に雑な解釈ですが、以下のようなものだと理解しました

  • print → エラーなのか警告なのかログなのか分からない。ログとる際に使うべきでない
  • logging → グローバル変数的な立ち位置。
  • logger → ローカル変数的な立ち位置。

※詳しくは元記事を参考にしてください・・・。

お、同じログがいっぱいでるぞ~

以下の記事で解決

uyamazak.hatenablog.com

再実行するたびにログが増えていく

上記の記事のようなログ用モジュールを作成して、実行する。

超具体的には、Anacondaで環境構築して、SpyderをIDEとして利用しており、 実行時にF5を使用すると、実行のたびにどんどん増える

# 一回目
[log info] ログテスト
# 二回目
[log info] ログテスト
[log info] ログテスト
#三回目
[log info] ログテスト
[log info] ログテスト
[log info] ログテスト

f:id:gologius:20180606214449p:plain

解決策

終了時に、killLoggers()のようにハンドラを全削除してあげます

from logging import getLogger, Formatter, FileHandler,StreamHandler, DEBUG, shutdown
from logging.handlers import RotatingFileHandler

loggers = {}

def getModuleLogger(moduleName):
    if moduleName is None:
        moduleName = __name__

    if loggers.get(moduleName):
        return loggers.get(moduleName)

    formatter = Formatter('[%(asctime)s | '
                          '%(name)s | '
                          '%(levelname)s] '
                          '%(message)s')

    streamHandler = StreamHandler()
    streamHandler.setFormatter(formatter)
    streamHandler.setLevel(DEBUG)

    fileHandler = RotatingFileHandler("download.log", maxBytes=5000, backupCount=3)    
    fileHandler.setFormatter(formatter)
    fileHandler.setLevel(DEBUG)

    logger = getLogger(moduleName)
    logger.setLevel(DEBUG)
    logger.addHandler(streamHandler)
    logger.addHandler(fileHandler)

    logger.propagate = False
    loggers[moduleName] = logger

    return logger

def killLoggers():

    for l in loggers:
        logger = loggers.get(l)
        for h in logger.handlers:
            logger.removeHandler(h)

    shutdown()

    return

これで重複出力はなくなります。

以上。

Selenium+Pythonにて、アラートが出る新規ウインドウに遷移したい

※Teratailで私が質問した問題を、結局自分で解決した際のメモです

Python - Selenium+Pythonにて、アラートが出る新規ウインドウに遷移したい(125969)|teratail

※2018/06/03追記

どうやらヘッドレスモード(GUI、ウインドウを表示しないモード)にすると、いちいち前のウインドウに遷移する必要がなくなるようです。 記事の最期の方に記載しておきます。

※2018/06/27追記 ↑とか書いちゃったのですが、実際には

  • 新規ウインドウが出るようなボタンをクリック
  • (新規ウインドウでアラートが出るまで待つ。)
  • switch_to_window()で新規ウインドウをアクティブにする

の流れで処理すると動くようです

やりたいこと

  • リンクをクリックする→新規ウインドウが開く(※ただしウインドウが開くと即アラートが出る)

という動きをSeleniumPythonで実現したいと考えています。

実際にはその先のボタンを押したいのですけどね。

下記のリンクで例を見ることが出来ます。

テストページ | gologiusのページ

問題点

調べているとどうも

  • アラートを消すためには、アラートが出ているウインドウに遷移しなければならない
  • ウインドウに遷移するためには、アラートを消さなければならない

と、デッドロックみたいな状況になっているようです。

以下ソースの★1、★2の部分です。

ソース

# -*- coding: utf-8 -*-
"""
Created on Wed Mar  7 22:16:01 2018

@author: 
"""

from selenium import webdriver
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException

import datetime
import traceback

###########################################################################
def waitOpenWindow(driver, beforeWinNum, maxWaitSec = 10):
    """
    指定された時間まで、ウインドウが開くのを待つ
    """

    beginTime = datetime.datetime.now()
    endTime = beginTime + datetime.timedelta(seconds=maxWaitSec)

    while(datetime.datetime.now() <= endTime):

        afterWinNum = len(driver.window_handles[-1])
        if afterWinNum == beforeWinNum:
            continue
        elif  afterWinNum > beforeWinNum:
            #ウインドウ数が増える=新規ウインドウが開いた
            return True , "指定時間内に新規ウインドウが開きました"
        else:
            return False, "ウインドウが外部から閉じられた可能性があります"

    return False, "指定時間内に新規ウインドウが開きませんでした"

###########################################################################
def waitAlert(driver, maxWaitSec = 10):
    """
    指定された時間まで、アラートが開くのを待つ    
    """

    try:
        wait = WebDriverWait(driver, maxWaitSec)
        wait.until(expected_conditions.alert_is_present())
    except TimeoutException as time_e:
        return 0, "アラート表示待機中にタイムアウトしました"
    except Exception as e:
        print(traceback.print_exc)
        return -1, "アラート表示待機中に予期しないエラーが発生しました"

    return 1, "アラートが表示されています : " + Alert(driver).text

###########################################################################
#関数定義
def access():

    try :
        print("begin process")

        driver = webdriver.Chrome("chromedriver_win32\chromedriver.exe")
        driver.implicitly_wait(10)
        driver.get("https://gologius.github.io/test.html")

        print("画面名", driver.title)
        winNum = len(driver.window_handles)

        #リンクをクリック
        driver.find_element_by_link_text("javascriptによる小ウインドウ表示").click()

        #ウインドウ表示待機     
        print("画面遷移待機中")
        waitResult, msg = waitOpenWindow(driver, winNum)
        if waitResult == False:
            return False, "ウインドウ表示に失敗しました"
        print(msg)

        #新規ウインドウに遷移する ★1
        win = driver.window_handles[-1] #リストの最後=最後に開いたウインドウ
        driver.switch_to.window(win)
        print("画面名", driver.title)
        winNum = len(driver.window_handles)

        #アラート待機 ★2
        print("アラート表示待機中")
        resultCode, msg = waitAlert(driver)
        if resultCode == 1:
            Alert(driver).accept()
        print(msg)

        print("end process")

    except:
        print("予期しないエラーが発生しました")
        print(traceback.format_exc())
        #ログ出力
        driver.quit()

    return True, "アクセス成功"

###########################################################################
#実行

result, msg = access()
print(msg)

解決方法

ボタンクリック後、「遷移前のウインドウ」にdriver.switch_to_window(win)で遷移すると、 その後のアラート処理、ウインドウ遷移が実行できるようです

win1 = driver.window_handles[-1]

driver.find_element_by_link_text("javascriptによる小ウインドウ表示").click()

#遷移前のウインドウに遷移する
driver.switch_to_window(win1)

#アラート処理

#ウインドウ遷移
win2 = driver.window_handles[-1]
driver.switch_to_window(win2)

理由は不明ですが、推測するに、新規ウインドウが表示される際に、

  • 新規ウインドウが開く際に、そのウインドウがアクティブになろうとする
  • Alertのせいでアクティブになりきれない →無限ループの原因
  • 遷移前のウインドウをアクティブに(switch_to_window)する
  • Alert(driver).accept()なども実行可能になる

みたいな気がします。


※2018/06/03追記↓

ヘッドレスモードで起動すると、この話は無視できる・・・?

        chromeOptions = webdriver.ChromeOptions()
        chromeOptions.add_argument('--headless') #ブラウザのGUIが表示されなくなる 
        driver = webdriver.Chrome("hogehoge/chromedriver.exe", chrome_options=chromeOptions)

雑感

上記のような不具合を抜きにしてもSelenium便利。

ちなみに検証用サイトはgithubioで作成しました。 テストページ | gologiusのページ

JavaScript使えるのでこういう際にサクッと作れるので便利。 ただJSの書き方を忘れていたのはナイショ...(/ω\)

【Akeytsu】 Reverse Footがうまく設定できない

Akeytsuを使用していて発生した問題についてメモ

Reverse Footとは

f:id:gologius:20180504170536p:plain

リファレンスの動画見た方がよくわかるのですが、足用のIKです。

問題発生

  • 片足には設定できるがもう片足に設定できない
  • 挙動がおかしい(膝を曲げて頂きたいのですが、曲げてくれません)

f:id:gologius:20180504171111p:plain

解決方法

これ、つま先のボーン(足のボーンの末端)に対して設定しないといけません。

上記二つの問題はこれで解決できます。やったね!

f:id:gologius:20180504171300p:plain

静止してくれない場合

f:id:gologius:20180504173841g:plain

上記のようになる場合、対象のフレームすべてにおいて、右クリックでリングを赤にすると解消されます。

f:id:gologius:20180504173906g:plain

まとめ

つまずいた点をメモりました。 Akeytsuは日本語情報が少ないので、小さなことでもメモっていきたいですね。 というかここ一年でこのブログ以外に日本語情報がないのですが・・・

また、以下の本が簡素で分かりやすい説明になっています(図が異様に雑な気がしますが・・・)。

おしまい。

【Unity】Playable APIを用いてモーション遷移時に補間に考慮したいこと

※都度更新予定

モーションを補間させながら遷移させる場合、Playable APIには関数は用意されていません(Unity2017.3現在)。 よって、PlayableAPIを使用する場合、自分でコードを書かなければなりません。 まぁ今後関数が用意される可能性もありますが・・・

というわけで、PlayableAPIでモーション遷移させる場合に、 注意したほうがさそうな場合について記載します。

個人的にはモーション切り替えパターンは

  • とにかく指定したモーションにスムーズに移行してほしいパターン(棒立ち→走り始める、など)
  • 上半身が別のモーションによって上書きされるパターン(走る→走りながら撃つ、など)
  • 指定したモーションの最初のフレームから最後のフレームまで再生してほしいパターン(武器をちゃんと振ってほしい場合など)
  • 遷移前の時間を引き継いでほしいパターン(前方向に歩く→右方向に歩く場合など。)

この四つのパターンにに分けられると思っています。今回はこの四つのパターンを実装したサンプルプロジェクトを作成しました。

サンプルプロジェクト

というわけで作成したサンプルプロジェクトを置いておきます。 github.com

実体はMotionPlayerコンポーネントです。Unityちゃんにアタッチされてます。 UIからモーション再生方法をいろいろ弄れます。 ちなみに逆再生も可能。

次からモーションの切り替えパターンの説明となります。

とにかく指定したモーションにスムーズに移行してほしいパターン

切り替え時にPlayableへのinputWeightを変更しながら、モーション切り替えをします(サンプルプロジェクトのMotionMixer. crossFadeCoroutine()が該当)

・・・という基本的な考え方をこちらの記事から学びました。 tsubakit1.hateblo.jp

上半身が別のモーションによって上書きされるパターン

例えば、

layerMixer.SetLayerMaskFromAvatarMask(layerIndex=1, upperMask);

のように、Layer1に上半身のマスクを割り当てた状態で

layerMixer.SetInputWeight(layer=1, weight=1f);

のようにweight=1fにすれば上半身のモーションは上書きされますし、weight=0fにすれば、layer=0に適用されているモーションが上半身にも適用されます。

ただ、このweight切り替えの際にいきなり値を変えると、モーションが補間されすに切り替わってしまいます。 なので

private IEnumerator crossFadeLayerWeight(int layer, float duration, bool enable)
 {
     float waitTime = Time.time + duration;
     yield return new WaitWhile(() =>
     {

         float diff = waitTime - Time.time;
         float rate = Mathf.Clamp01(diff / duration);
         float weight = (enable) ? 1 - rate : rate;
         layerMixer.SetInputWeight(layer, weight);

         if (diff <= 0)
             return false;
         else
             return true;
     });
 }

のようにして値を補間させながら切り替えましょう(詳しくはMotionPlayer.setLayerEnabled( )を参照)。

※サンプルプロジェクトの場合「上半身上書き」にチェックを入れ、遷移時間のスライダを0以上にすると、遷移が見れます。

指定したモーションの最初のフレームから最後のフレームまで再生してほしいパターン

※サンプルプロジェクトの場合「完全遷移まで待機」にチェックを入れる、に該当します。

例えば武器を 1. 振り上げて 2. 振り下ろす

という二つで一つのモーションに遷移したい場合、遷移前のモーションがミックスされてしまうと、 2.振り下ろす のモーションしか再生されてないようにみえる可能性があります。

f:id:gologius:20180331223208p:plain

ですので、遷移後のモーションの一フレーム目の姿勢になるまで、再生を待ってあげる必要があります。

f:id:gologius:20180331223210p:plain

またせる処理はコルーチンを使えば大して難しくないですね。以下のように遷移先のモーション再生を停止させておき、一定時間後にモーション再生を再開します(詳しくはMotionMixer. crossFadeCoroutine()参照)

     if (waitCrossFade)
      nowPlayable.SetSpeed(0f); //遷移先のモーション時間を止めておく

      //inputWeightの変更 略

       if (waitCrossFade)
       {
           float play_speed = playParam.reverse ? -1f : 1f;
           nowPlayable.SetSpeed(play_speed); //遷移先のモーション再生時間を元に戻す
       }

遷移前の時間を引き継いでほしいパターン

※サンプルプロジェクトの場合「アニメーション時間同期」にチェックを入れる、に該当します。

例えば走るモーションが、正面、左、右の3パターンあり、すべて同じ周期で走っているとします。 この場合時間同期させながら遷移させないと、片足で移動しているように見えてしまいます。

f:id:gologius:20180331224118g:plain

なので、再生する時に、遷移前のモーションの再生中時間を

nowPlayable.SetTime(beforePlayable.GetTime());

のようにして引き継いであげる必要があります。 サンプルではMotionMixer.recconect()内に処理があります。

f:id:gologius:20180331224255g:plain

まとめ

  • Playable APIでモーション遷移する際のパターンについて書きました
  • いかんせん動画か資料がないとわかりづらい内容なので、記事を書く準備が面倒でしたね・・・
  • この記事は追記、更新しまくると思います

【Python】【Selenium】 Webサイトから自動でファイルDLする その1

2019/03 超追記

概要

「Webサイトにアクセス→ログイン→ボタンを押してファイルをDL」

みたいな処理を自動化したいと思いました。

とりあえず基本を説明し、どんどん応用編に進んでいきたいと思います。

環境構築

PythonSeleniumを使用します。 Pythonは環境構築が楽で、パッケージがいろいろあるのでいいですよね。

OSはWin10です。 ブラウザはクロームを使用します。

Pythonの環境

初めて環境構築する場合は、 以下のリンクからインストーラをDLして実行すると、 Python、エディタ(Spyder)など全部入ります。

初めて入れるならPython 3.*版にしましょう。 2.*版は2020年にサポートが終了します。 * https://www.anaconda.com/download/

ちなみに私はPython2のAnacondaが入っている状態でさらにPython3版をインストールしたため、 パスがめちゃくちゃになりました。

解決方法としては、環境変数を適当に消してから再インストールすると正常に動作するようになりました。

Seleniumのインストール

コマンドプロンプト

pip install selenium

以上。

次、ここからwebdriverをDLして、適当なディレクトリに置いておきます。 * https://sites.google.com/a/chromium.org/chromedriver/home

パスは後で使用するので覚えておきましょう。

ちなみに、これをDLしてない or スクリプト上でパス指定が間違っている場合、以下のエラーが出ます。

WebDriverException: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home

基本その1 DOM要素の取得

DOM要素(HTMLタグで囲まれた要素)を取得して、クリックや入力を行います。

※headタグ省略。実際のソースを見たい方は上記のURL参照

<body>
    <h1>Selenium使用例 初級</h1>

    <h2 id="result">html要素選択結果がここにセットされます</h2>
    <h2>ランダム文字列→ <span id="random_text"></span></h2>

    <div>
        <ui>
            <li><input id ="aaa" type="button" value="ID指定" onclick="show_result('ID指定');"/></li>
            <li><input class="bbb" type="button" value="CLASS指定" onclick="show_result('CLASS指定');"/></li>
            <li><input name="ccc" type="button" value="NAME指定" onclick="show_result('NAME指定');"/></li>
            <li><input type="button" value="XPATH指定" onclick="show_result('XPATH指定');"/></li>
            <li><input type="button" value="内部テキスト指定" onclick="show_result('内部テキスト指定');"/></li>
            <li><a href="#" onclick="show_result('リンク名称指定');">リンク名称指定をする</a></li>
        </ui>
    </div>

    <form>
    <p>性別は?</p>
    <input type="radio" name="gender" value="male">男性
    <input type="radio" name="gender" value="female">女性

    <p>年齢は?</p>
    <select name="old">
        <option value="item1">0才-10才</option>
        <option value="item2">11才-50才</option>
        <option value="item3">51才-</option>
    </select>

    <p>名前は?</p>
    <input type="text" name="yourname">

    <p>このサイトはテストサイトということは理解していますよね?</p>
    <input type="checkbox" name="understand" value="yes">理解してます

    </form>
</body>
$(document).ready( function(){
    show_random_text();
    show_random_texts();
});

function show_result(result_text){
    elem = $('#result'); 
    elem.text(result_text);
}

function show_random_text(){
    
    random_text = get_random_text();
    
    elem = $('#random_text'); 
    elem.text(random_text);
}

function show_random_texts(){
    
    var a = $('#random_texts').find('li')
    
    a.each(function(index, elem){
        $(elem).text(get_random_text());
    })
}

function get_random_text(){
    return Math.random().toString(36).slice(-8)
}
    
function show_alert(){
    
    var date = new Date();
    var hour = date.getHours();
    var min = date.getMinutes();
    var sec = date.getSeconds();

    window.alert("現在は" + hour + "時" + min + "分" + sec + "秒です");
    
    show_result("アラートを表示しました");
}

各DOM要素をいろいろな方法で指定、操作します。

# -*- coding: utf-8 -*-
"""
Created on Sat Mar  9 16:28:14 2019

@author: gologius
"""

from selenium import webdriver
import time

CHROME_DRIVER_PATH = "chromedriver_win32\chromedriver.exe"
URL = "https://gologius.github.io/test/selenium_lecture_1.html"

driver = webdriver.Chrome(CHROME_DRIVER_PATH)
driver.implicitly_wait(10) #sec
WAIT_TIME = 1

def process_1():
    """
    シンプルな要素選択
    """
    driver.get(URL)

    #要素を指定→クリック
    elm = driver.find_element_by_id("aaa")
    elm.click()
    time.sleep(WAIT_TIME)

    elm = driver.find_element_by_class_name("bbb")    
    elm.click()
    time.sleep(WAIT_TIME)

    elm = driver.find_element_by_name("ccc")
    elm.click()
    time.sleep(WAIT_TIME)

    elm = driver.find_element_by_xpath("//div/ui/li/input[@value='XPATH指定']")
    elm.click()
    time.sleep(WAIT_TIME)

    elm = driver.find_element_by_xpath("//input[contains(@onclick, '内部テキスト')]")
    elm.click()    
    time.sleep(WAIT_TIME)

    elm = driver.find_element_by_link_text("リンク名称指定をする")
    elm.click()    
    time.sleep(WAIT_TIME)

    #要素の値を取得
    elm = driver.find_element_by_id("random_text")    
    print(elm.text)
    elm = driver.find_element_by_id("aaa")
    print(elm.get_attribute("onclick"))

    #ラジオボタンを指定→クリック
    elm = driver.find_element_by_xpath("//input[@value='female']")
    elm.click()    
    time.sleep(WAIT_TIME)

    #プルダウンを指定→クリック
    elm = driver.find_element_by_xpath("//select/option[contains(@value, 'item2')]")
    elm.click()
    time.sleep(WAIT_TIME)

    #テキストボックスを指定→値入力
    elm = driver.find_element_by_name("yourname")
    elm.send_keys("吾輩は猫である")
    time.sleep(WAIT_TIME)

    #チェックボックスを指定→クリック
    elm = driver.find_element_by_xpath("//input[@value='yes']")
    elm.click()    
    time.sleep(WAIT_TIME)

    return

process_1()    

実行すると下記のように、自動で要素をクリック、入力していきます。 f:id:gologius:20190311234815g:plain

詳細説明

driver.implicitly_wait(WAIT_SEC) はかなり重要です。 要素がなくても'WAIT_SEC'秒だけ待ち続けます。 これを指定することで、読み込みが完了しておらず、要素がないのでエラーになる、といった事態を避けられます。

また、driver.find_element_by_xpath()xpathを指定して要素を取得できます。 xpathの説明はこの記事が詳しいです。 上の例だと、

  • //input[@id='page'] → ページ全体(//)の中で<input id="page"></input>を取得
  • //a[@onclick='download(); → ページ全体(//)の中で<a onclick="download"></a>を取得

となります。 要素取得後はsend_keys()で値セット、click()で要素をクリックしています。 ちなみにsend_keys()は複数同時入力(Ctrl+Cなど)に対応しているので、複数形(keys)になってるみたいです。今回は使ってませんが。

まとめ

個人的にはdriver.implicitly_wait()が便利。 いちいち読み込み完了を検知するスクリプトを書かなくともよいのは便利。

その2に続く

次回は、ウインドウ、アラート、ファイルダウンロードを主に説明します

【Unity】一週間GameJamに参加した話

ゲーム作ったので報告しますね

概要

一週間でゲームを作るイベントです

Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

今回のテーマは「当てる」です

今回の目標

  • とりあえず完成させる
  • UniRxで書いてみたい
  • TimeLine使いたい

とりあえずこんなところですね。 最近は製作途中のものが多かったので、なにか完成物を作りたいと思いました。

また、UniRxは最近使い始めてなかなかいい感じに理解できてきたので、 使いたいと思っていました。 今回のソースの解説とか後日やりたいですね(やるとはいっていない)。

あとはTimeLineですね。前までいちいちAnimationClipとAnimationControlllerを作ってたのですが、 ファイル増えすぎて嫌だと思ってました(厳密には内部でAnimationClipが作成されているみたいですが)。

ゲームを考える

当たるといわれると恐らく誰もが「シューティングゲーム」を思い浮かぶでしょう。

・・・ならばその逆をいきます。自分から「当たりに」いきます。 僕の思い付きが面白いかは別にして、人とは違うオリジナルな発想というのは大事だと思うのですよ。

そしてコンセプトが決まったので

  • 誰が当たるか
  • どうやって当たるか
  • 何に当たるか
  • 当たったらどうなるか

を決める必要があります。これによってゲームの面白さがきまるといっても過言ではありません。

時間的な都合もあって

  • 誰が当たるか → 昔作った棒人間
  • どうやって当たるか → スタイリッシュなポーズで
  • 何に当たるか → メタセコイアの機能で「文字の3D化」があったのでこれで
  • 当たったらどうなるか → 音がだんだん変わっていって、もっとコンボをつなげたくなるように

になりました。

できたもの

動画は製作途中のものしかありません。

アタレ、サラバアタエラレン | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

トラブル

WebGLでビルド後、実行するとエラーで死ぬ。 ブラウザの開発者ツールからコンソールを見ると、AudioPlayableかエラー吐いていました。 おそらくTimeLineが悪いのだと推測しました。 この時点で日曜日17時50分。遅刻確定 (締め切りは日曜日20時です。過ぎてもペナルティ等はありませんが)

どうもバージョンアップで治るようです。イベントに参加する前にはアップデートしましょう(戒め)。

https://forum.unity.com/threads/timeline-animations-on-webgl-from-assetbundle-dont-play-at-all.500185/

なおこのアップデート後にLayerMixerがおかしくなりました(以前の記事参照)。

gologius.hatenadiary.com

とりあえずこのゲームには不要だったので、LayerMixerを経由しないことで対応しました。

反省

ほんとは当たったときのエフェクトとかもっと作りこみたかったです。 ヒットストップ、画面振動などなど・・・

またゲームとして、難易度アップや、敵のランダム性もなかったです。 最初はくる文字の色も変える予定だったのですが・・・。

まとめ

反省点もいろいろあります。

が、やらないより、とりあえず一つのものを作りきる、という経験は大切だと思います。

今後もこの精神でちょくちょく参加したいです。 f:id:gologius:20180301220231p:plain

【Unity】 Playable APIで上半身と下半身を別々のモーションをさせる【改良版】

※2018/03/03追記:バージョンアップでモーションが動かなくなる不具合を修正(最後に説明)

やりたいこと

前回

前回の問題は

  • RigがHumanoidの場合、挙動がおかしい
  • AnimationLayerMixerPlayable の理解が微妙
  • (記事には書いてないけど) そもそもAvaterMaskは一つでいいのでは?
    • AnimatorControllerを使う場合、AvaterMaskは上半身分のみでよい
    • 実装をもうちょいきれいにできるのでは?

でした。解決していきましょう。

実装結果

実際に実装したものがこちら(左:Humanoid、 右がGeneric)

サンプルプロジェクトはこちら

GitHub - gologius/PlayabkeMask: Playable APIでマスクをするテスト

使用方法

1「上半身分」のAvaterMaskを作成する ※前回は下半身分も作成しましたが、いらないことが判明しました。 f:id:gologius:20171120233424p:plain

f:id:gologius:20171120233300p:plain

2 Animatorがアタッチされている場所にMotionPlayerをアタッチ f:id:gologius:20180104005628p:plain

3 先ほど作成したAvaterMaskをMotionPlayerにD&Dでセットする

4 MotionPlayer.play(AnimationClip clip, int layer, bool loop)を何らかの方法で呼ぶ

  • サンプル プロジェクトでは、MotionTest内から呼び出してます
  • AnimationClipもここで指定

解説

f:id:gologius:20180104012133p:plain

  • ※この理解であっているかは不明

  • AnimationMixerPlayableはモーション切り替え時に、変更前モーションと変更後モーションをいい感じでブレンドするためのミキサー。

  • SetLayerMaskFromAvatarMask(1, upperMask)で、LayerIndexとAvaterMask(上書きする部位)を指定している
    • ※ここでいうLayerIndexはAnimationLayerMixerPlayableInputIndexと(おそらく)同じ
    • InputIndexは、ConnectInput()で指定するもの
  • なので、SetLayerMaskFromAvatarMask()したレイヤ番号と同じInputIndexに、上半身用のモーションを流しこめば、 【A】ベースとなるモーション、の該当部分(サンプルの場合上半身)が、【B】のモーションに上書きされる

疑問

AnimationLayerMixerPlayableのWeight全く弄っていないのですが、いいんでしょうかね。。。

※【2018/03/03追記】...という疑問が2018/01時点ではあったんですが、アップデートで見事に修正されました。 恐らく、関数定義がUnity2017.1時点では

AnimationLayerPlayable(int inputIndex, ...... float weight = 1f)

となってたんでしょうが、Unity2017.3では

AnimationLayerPlayable(int inputIndex, ...... float weight = 0f)

になっており、明示的にweightを指定してあげないと、全くモーションが反映されない模様です。 サンプルも修正しました