皆さんは、Microsoft Agentというものご存じでしょうか?
Microsoft Agentと聞くとイマイチ、思い浮かばない人も多いのではないでしょうか?
このMicrosoft AgentはOffice Assistantに使用されています。
このような、キャラクターがいました。
覚えてる人もいるのではないでしょうか?
これに使用されていた、プログラムのことです。
こちらのお話をしていこうと思います。
こちらは、Windows7以降からは、非推奨となっています。
そのため、実行はWindowsMEなどでやりましょう。
WindowsなのでコンパイラーはMSVC、Visual studioからそのままコンパイルしちゃいましょう。
まずは公式で書いてある基本的なインタラクションに関して学びましょう。
自動的に実行または支援するキャラクター(BOTなど)に使用すること。
ユーザーによる制御を維持するという設計原則に反してはいけない。
このアプリケーションは拡張機能として使用し、キャラクターのみを必要とする機能や操作を実装することは避ける。
キャラクターと対話を選択できるようにする。ユーザーは、許可を得た場合にのみ、キャラクターを終了して、そして復帰できるようにする必要があります。ユーザーにキャラクターの対話を強制すると、重要な悪影響が生じる可能性があります。
Micorosft Agentには非表示コマンドと表示コマンドが自動的に含まれており、独自のインターフェイスにこれらの機能のサポートを含めることができます。
これは、ユーザーに「お前を消す方法」などと質問させないための部分です。
ユーザーはパソコン内で唐突にバスケやテニスなどをしたりして、明らかに彼らがしたいことを勝手にしてるような形で動くのではなく、そのアプリへの拡張としての機能であるべきと言っています。
そして、自動的で支援えをするキャラクターであり、自由意志を持つべきではないと言ってます。
キャラクターとの対話を選択でき、そのキャラクターが勝手に表示されたり、強制的に表れないようにするべきと言ってます。
品質、適切性、タイミングは、インターフェイスデザインでフィードバックを提供する際に考慮すべき重要な要素です。
インタラクティブなキャラクターを組み込むと、自然な形式のフィードバックの機械で増えるとともに、社会的インタラクションが成立していると思え、ユーザーは満足してくれます。
そのため、顔や非言語な機能などを利用して、その気分や意図を伝えるとよいでしょう。
人間には、環境の変化、特に動き、音量、コントラストの変化に注意を向ける定位反射があります。
そのため、ユーザーの気を散らさないように、キャラクターの動きが常に大胆に、そして大げさではない必要があります。
アイドリング動作を実装する場合、呼吸する、あたりを見回すなどのほうが良いとされるでしょう。
逆に、大げさ、大胆な行動は、ユーザーの注意を引きたい場合に行うとよいでしょう。
ユーザーは、ちゃんとした「スタンバイ」の位置を持つ必要があります。userの気を散らしたりしない場所に移動し、邪魔にならないようにしましょう。
これは、ユーザーの刺激に関するお話ですね。
人間は刺激を好みません。会社内で太鼓を叩き、みんなの前で歌を熱唱すると周りの人は嫌になりますよね。
これは、人間が強い刺激を受けることによる猛烈なストレスが関係しています。
ユーザーへのキャラクターの刺激も同様に、急に太鼓を叩く、大声で叫ぶなどはユーザーに強い不快感を引き起こすのです。
そして、ユーザーが邪魔と思う位置に勝手に表れないようにし、常に動き回るなどを避けるべきと書いてあります。
メニューやダイアログボックスなどの予測可能なインターフェイスになりますが、キャラクターの応答を適切に変化させることで、より自然なインターフェイスが実現します。キャラクターが杖にまったく同じようにユーザーに話しかける場合、例えば、常に同じ言葉を言う場合、ユーザーはそのキャラクターを退屈で、無関心で、失礼な人だと判断する可能性があります。
そのため、言葉遣い、身振り、表情を適切に変える必要があります。
これは、ユーザーがイライラするような会話にしようというお話です。
キャラ 「何か質問はありますか?」
ユーザー「シャットダウンの方法」
キャラ 「シャットダウンの方法は以下の通りです...」
ユーザー「ウイルスの消し方」
キャラ 「ファイルの削除の方法は以下の通りです...」
ユーザー「お前を消す方法」
キャラ 「ファイルの削除の方法は以下の通りです...」
を繰り返されると、人間はイライラする可能性があるということです。
そのため、
キャラ 「何か質問はありますか?」
ユーザー「シャットダウンの方法」
キャラ 「わかりました!シャットダウンの方法は以下の情報があります!!」
ユーザー「ウイルスの消し方」
キャラ 「調べてみました!ファイルの削除の方法は以下の通りです...」
ユーザー「お前を消す方法」
キャラ 「なるほど!ファイルの削除の方法は以下の通りですよ!」
のように、変化をもたらすことでストレスを減らせるということです。
では、キャラクターの仕組みを考えてみましょう。
では、プログラムを書いていきましょう。
まず、こちらあまりにも古いものなのでいろいろと複雑で何が起きるかわかりません。
自分が何をしているかわからなければ実装しないでください。
自己責任で実装してください。
まず、Microsoft Agent Software Development Kitを使用します。
https://archive.org/details/microsoftagentsoftwaredevelopmentkit
この中の
D:/SDK/Include/
内のヘッダーファイルをプロジェクトに組み込みます。
そしたら、プログラムのヘッダー内で
#include "AgtSvr.h"
#include "AgtSvr_i.c"
を定義します。
この時代のソフトですし、最初にCOMの対応のチェックもします。
int InitCOM()
{
if (FAILED(CoInitialize(NULL))) {
MessageBox(NULL,
_T("There was an error initializing COM."),
L"Agent",
MB_OK | MB_ICONERROR);
return -1;
}
}
combaseapi.hのcombaseapi.hをコールします。
CoCreateInstance(
_In_ REFCLSID rclsid,
_In_opt_ LPUNKNOWN pUnkOuter,
_In_ DWORD dwClsContext,
_In_ REFIID riid,
_COM_Outptr_ _At_(*ppv, _Post_readable_size_(_Inexpressible_(varies))) LPVOID FAR * ppv
);
ここで、公式docsでは以下のように設定するように言われています。このまま書きます。
hRes = CoCreateInstance(CLSID_AgentServer,
NULL,
CLSCTX_SERVER,
IID_IAgentEx,
(LPVOID *)&pAgentEx);
そしたら、お掃除もしていきましょう。
void CleanUp()
{
if (pAgentEx)
{
pAgentEx->Release();
}
}
そしたら、エラー処理を書いていきます。
if (FAILED(hRes)) {
HRESULT hRes = CoCreateInstance(CLSID_AgentServer,
NULL,
CLSCTX_SERVER,
IID_IAgentEx,
(LPVOID*)&pAgentEx);
MessageBox(NULL,
L"Failed to initialize agent.",
L"Agent",
MB_OK | MB_ICONERROR | MB_TOPMOST);
CoUninitialize();
return -1;
}
そしたら、キャラクターの読み込みを開始していきます。
まず、Loadメソッドを使用して読み込み、IAgentCharacterExインターフェイスを取得します。
キャラクターのファイルは、acsファイルです。
ここから好きなagentをダウンロードしましょう。今回はDOLPHIN.ACSをダウンロードします。
まず、
LPWSTRで、キャラクターの文字列を書きます。
今回は絶対パスで読み込ませたいです。そのため、GetCurrentDirectoryA+"DOLPHIN.ACS"にします。
#include "shlwapi.h"
GetCurrentDirectoryA(MAX_PATH,characterPath);
PathCombineA(characterPath, characterPath, "DOLPHIN.ACS");
そしたら、念のためにprintfしておきます。
printf("Loading character in %8s", characterPath);
その後、
HRESULT Load(
VARIANT vLoadKey, // data provider
long * pdwCharID, // address of a variable for character ID
long * pdwReqID // address of a variable for request ID
);
このvLoadKeyは、VARIANTなのでVariantInitでvPathを初期化します。
VariantInit(&vPath);
vPath.bstrVal = SysAllocString(kpwzCharacter);
hRes = pAgentEx->Load(vPath, &lCharID, &lRequestID);
SysAllocStringは、Wide型しか読み込んでくれないのでWideにします。
int iLen = MultiByteToWideChar(CP_ACP, 0, characterPath, -1, NULL,0);
LPWSTR kpwzCharacter = new wchar_t[iLen];
MultiByteToWideChar(CP_ACP,0, characterPath, -1, kpwzCharacter, iLen);
その後、エラー処理を書きます。
if (FAILED(hRes)) {
MessageBox(NULL,
L"Failed to load agent.",
L"Agent",
MB_OK | MB_ICONERROR | MB_TOPMOST);
CoUninitialize();
return -1;
}
次にpAgentExからIAgentCharacterExを貰います。
hRes = pAgentEx->GetCharacterEx(lCharID, &pCharacterEx)
そしたら次に、
pCharacterEx->Show
でいよいよ表示します。
hRes = pCharacterEx->Show(FALSE, &lRequestID);
そしたら、喋らせてみましょう。
bszSpeak = SysAllocString(L"Hello World!");
hRes = pCharacterEx->Speak(bszSpeak, NULL, &lRequestID);
SysFreeString(bszSpeak);
そしたら、ちゃんとお掃除もします。
void CleanUp()
{
if (pCharacterEx) {
pCharacterEx->Release();
pAgentEx->Unload(lCharID);
}
if (pAgentEx)
{
pAgentEx->Release();
}
VariantClear(&vPath);
}
ではエントリーポイントを書いていきます。
int main()
{
InitCOM();
LoadAgent();
CleanUp();
return 0;
}
こんな感じに簡単に書けました。
全体はこんな感じです。
#pragma once
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
#include "shlwapi.h"
#include "AgtSvr.h"
#include "AgtSvr_i.c"
LPSTR characterPath;
VARIANT vPath;
IAgentEx* pAgentEx;
IAgentCharacterEx* pCharacterEx = NULL;
BSTR bszSpeak;
long lCharID;
long lRequestID;
int InitCOM();
int LoadAgent();
void CleanUp();
int InitCOM()
{
if (FAILED(CoInitialize(NULL))) {
MessageBox(NULL,
_T("There was an error initializing COM."),
L"Agent",
MB_OK | MB_ICONERROR);
return -1;
}
}
int LoadAgent() {
HRESULT hRes = CoCreateInstance(CLSID_AgentServer,
NULL,
CLSCTX_SERVER,
IID_IAgentEx,
(LPVOID*)&pAgentEx);
if (FAILED(hRes)) {
MessageBox(NULL,
L"Failed to initialize agent.",
L"Agent",
MB_OK | MB_ICONERROR | MB_TOPMOST);
CoUninitialize();
return -1;
}
char characterPath[MAX_PATH];
GetCurrentDirectoryA(MAX_PATH,characterPath);
PathCombineA(characterPath, characterPath, "DOLPHIN.ACS");
printf("Loading character in %8s", characterPath);
VariantInit(&vPath);
vPath.vt = VT_BSTR;
int iLen = MultiByteToWideChar(CP_ACP, 0, characterPath, -1, NULL,0);
LPWSTR kpwzCharacter = new wchar_t[iLen];
MultiByteToWideChar(CP_ACP,0, characterPath, -1, kpwzCharacter, iLen);
vPath.bstrVal = SysAllocString(kpwzCharacter);
hRes = pAgentEx->Load(vPath, &lCharID, &lRequestID);
if (FAILED(hRes)) {
MessageBox(NULL,
L"Failed to load character.",
L"Agent",
MB_OK | MB_ICONERROR | MB_TOPMOST);
CoUninitialize();
return -1;
}
hRes = pAgentEx->GetCharacterEx(lCharID, &pCharacterEx);
if (FAILED(hRes))
{
printf("Faild to get character");
}
hRes = pCharacterEx->Show(FALSE, &lRequestID);
if (FAILED(hRes))
{
printf("Faild to show the character");
}
bszSpeak = SysAllocString(L"Hello World!");
hRes = pCharacterEx->Speak(bszSpeak, NULL, &lRequestID);
if (FAILED(hRes))
{
printf("Faild to speak the character");
}
printf("\nDone!");
SysFreeString(bszSpeak);
return 1;
}
void CleanUp()
{
if (pCharacterEx) {
pCharacterEx->Release();
pAgentEx->Unload(lCharID);
}
if (pAgentEx)
{
pAgentEx->Release();
}
VariantClear(&vPath);
}
では、ビルドして起動してみましょう。
再定義に関するlnk2005が出たので
(/FORCE:MULTIPLE)を追加しました。
実行してみると、何も表示されませんね。
そのため、すぐ終了しないように
Sleep(10000);
をLoadAgentの最後に追加しましょう。
すると、カイルくんが現れたことが分かると思います。
では次はカイルくんを動作させましょう。
通知シンクというのを作る必要があります。
以下のNotify.cppとNotify.hをコピペします。
#include "Notify.h"
//==========================================================================
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
// PURPOSE.
//
// Copyright (C) 1997 - 2000 Microsoft Corporation. All Rights Reserved.
//
//--------------------------------------------------------------------------
//
// AgentNotifySink
//
// Boilerplate IDispatch implementation with stubs for all
// IAgentNotifySink methods except for RequestComplete.
//
//==========================================================================
extern long g_lDone;
// IUnknown methods
STDMETHODIMP AgentNotifySink::QueryInterface(REFIID riid, LPVOID* ppv) {
*ppv = NULL;
if ((riid == IID_IUnknown) || (riid == IID_IAgentNotifySink))
*ppv = this;
if (*ppv == NULL)
return E_NOINTERFACE;
((LPUNKNOWN)*ppv)->AddRef();
return NOERROR;
}
STDMETHODIMP_(ULONG) AgentNotifySink::AddRef() {
return ++m_cRefs;
}
STDMETHODIMP_(ULONG) AgentNotifySink::Release() {
if (--m_cRefs != 0)
return m_cRefs;
delete this;
return 0;
}
// IDispatch methods
STDMETHODIMP AgentNotifySink::GetTypeInfoCount(UINT* pctInfo) {
*pctInfo = 1;
return NOERROR;
}
STDMETHODIMP AgentNotifySink::GetTypeInfo(UINT itInfo, LCID lcid, ITypeInfo** ppTypeInfo) {
HRESULT hRes;
ITypeLib* pLib;
*ppTypeInfo = NULL;
if (itInfo != 0)
return TYPE_E_ELEMENTNOTFOUND;
if (ppTypeInfo == NULL)
return E_POINTER;
if ((PRIMARYLANGID(lcid) != LANG_NEUTRAL) &&
(PRIMARYLANGID(lcid) != LANG_ENGLISH))
return DISP_E_UNKNOWNLCID;
hRes = LoadRegTypeLib(LIBID_AgentServerObjects,
1,
0,
PRIMARYLANGID(lcid),
&pLib);
if (FAILED(hRes))
return hRes;
hRes = pLib->GetTypeInfoOfGuid(IID_IAgentNotifySink, ppTypeInfo);
pLib->Release();
if (FAILED(hRes))
return hRes;
(*ppTypeInfo)->AddRef();
return NOERROR;
}
STDMETHODIMP AgentNotifySink::GetIDsOfNames(REFIID riid, OLECHAR** rgszNames, UINT cNames, LCID lcid, DISPID* rgDispID) {
HRESULT hRes;
ITypeInfo* pInfo;
// REFIID must be NULL
if (riid != IID_NULL)
return ResultFromScode(DISP_E_UNKNOWNINTERFACE);
// Get the TypeInfo for the specified lcid
hRes = GetTypeInfo(0, lcid, &pInfo);
if (FAILED(hRes))
return hRes;
// Use the TypeInfo to get the DISPIDs of the specified names.
// That's the whole point here. Let TypeInfo do the work so
// we don't have to.
hRes = pInfo->GetIDsOfNames(rgszNames, cNames, rgDispID);
pInfo->Release();
return hRes;
}
STDMETHODIMP AgentNotifySink::Invoke(DISPID dispID, REFIID riid, LCID lcid,
unsigned short wFlags, DISPPARAMS* pDispParams,
VARIANT* pVarResult, EXCEPINFO* pExcepInfo,
UINT* puArgErr) {
HRESULT hRes;
ITypeInfo* pInfo;
// The riid parameter is always supposed to be IID_NULL
if (riid != IID_NULL)
return DISP_E_UNKNOWNINTERFACE;
// Get the type info for the specified lcid
hRes = GetTypeInfo(0, lcid, &pInfo);
if (FAILED(hRes))
return hRes;
// Clear exceptions
SetErrorInfo(0L, NULL);
hRes = pInfo->Invoke(this,
dispID,
wFlags,
pDispParams,
pVarResult,
pExcepInfo,
puArgErr);
pInfo->Release();
return hRes;
}
// IAgentNotifySink methods
STDMETHODIMP AgentNotifySink::Command(long dwCommandID, IUnknown* punkUserInput) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::ActivateInputState(long dwCharID, long bActivated) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::Restart() {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::Shutdown() {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::VisibleState(long dwCharID, long bVisible, long lCause) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::Click(long dwCharID, short fwKeys, long X, long Y) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::DblClick(long dwCharID, short fwKeys, long X, long Y) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::DragStart(long dwCharID, short fwKeys, long X, long Y) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::DragComplete(long dwCharID, short fwKeys, long X, long Y) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::RequestStart(long dwRequestID) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::RequestComplete(long dwRequestID, long hrStatus) {
// When we get g_lDone exit the sample app
if (dwRequestID == g_lDone)
PostQuitMessage(0);
return NOERROR;
}
STDMETHODIMP AgentNotifySink::BookMark(long dwBookMarkID) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::Idle(long dwCharID, long bStart) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::Move(long dwCharID, long X, long Y, long lCause) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::Size(long dwCharID, long lWidth, long lHeight) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::BalloonVisibleState(long dwCharID, long bVisible) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::HelpComplete(long dwCharID, long dwCommandID, long dwCause) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::ListeningState(long dwCharacterID, long bListenState, long dwCause) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::DefaultCharacterChange(BSTR bszGUID) {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::AgentPropertyChange() {
return NOERROR;
}
STDMETHODIMP AgentNotifySink::ActiveClientChange(long dwCharID, long lStatus) {
return NOERROR;
}
#include "AgtSvr.h"
class MainDlg;
// Agent Notify Sink Implementation
class AgentNotifySink : public IAgentNotifySinkEx {
public:
entNotifySink() : m_cRefs(0) {}
~AgentNotifySink() {}
STDMETHODIMP QueryInterface(REFIID, LPVOID FAR*);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
STDMETHODIMP GetTypeInfoCount(UINT*);
STDMETHODIMP GetTypeInfo(UINT, LCID, ITypeInfo**);
STDMETHODIMP GetIDsOfNames(REFIID, OLECHAR**, UINT, LCID, DISPID*);
STDMETHODIMP Invoke(DISPID, REFIID, LCID, WORD, DISPPARAMS*, VARIANT*, EXCEPINFO*, UINT*);
STDMETHODIMP Command(long dwCommandID, IUnknown* punkUserInput);
STDMETHODIMP ActivateInputState(long dwCharID, long bActivated);
STDMETHODIMP Restart();
STDMETHODIMP Shutdown();
STDMETHODIMP VisibleState(long dwCharID, long bVisible, long lCause);
STDMETHODIMP Click(long dwCharID, short fwKeys, long x, long y);
STDMETHODIMP DblClick(long dwCharID, short fwKeys, long x, long y);
STDMETHODIMP DragStart(long dwCharID, short fwKeys, long x, long y);
STDMETHODIMP DragComplete(long dwCharID, short fwKeys, long x, long y);
STDMETHODIMP RequestStart(long dwRequestID);
STDMETHODIMP RequestComplete(long dwRequestID, long hrStatus);
STDMETHODIMP BookMark(long dwBookMarkID);
STDMETHODIMP Idle(long dwCharID, long bStart);
STDMETHODIMP Move(long dwCharID, long x, long y, long lCause);
STDMETHODIMP Size(long dwCharID, long lWidth, long lHeight);
STDMETHODIMP BalloonVisibleState(long dwCharID, long bVisible);
STDMETHODIMP HelpComplete(long dwCharID, long dwCommandID, long dwCause);
STDMETHODIMP ListeningState(long dwCharID, long bListening, long dwCause);
STDMETHODIMP Suspend();
STDMETHODIMP DefaultCharacterChange(BSTR bszGUID);
STDMETHODIMP AgentPropertyChange();
STDMETHODIMP ActiveClientChange(long dwCharID, long bActive);
private:
ULONG m_cRefs;
MainDlg* m_pMainDlg;
};
そしたら、Agent.cppに行き、
IAgentNotifySink* pSinkEx;
long lNotifySinkID = -1;
MSG msg;
をヘッダーに書きます。
そして、
pSinkEx = (AgentNotifySink*)malloc(sizeof(AgentNotifySink));
pSinkEx->AddRef();
hRes = pAgentEx->Register((IUnknown*)pSinkEx, &lNotifySinkID);
そしたら、リターンの前に
while (GetMessage(&msg, NULL, 0, 0) > 0)
DispatchMessage(&msg);
を書いてループさせます。
をVariantInit(&vPath);の後らへんに書きます。
そしたら、お掃除コードを書きます。
if (pSink) {
if (lNotifySinkID != -1)
pAgentEx->Unregister(lNotifySinkID);
pSink->Release();
}
これでおしまいです。
そしたら、常に表示されるるのが分かります。
次はパート2でやります。